1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 16:42:50 -05:00

Defect/PM-1196 - SSO with Email 2FA Flow - Email Required error fixed (#2874)

* PM-1196 - Created first draft solution for solving SSO with Email 2FA serverside.  Per architectural review discussion, will be replacing OTP use with expiring tokenable implementation in order to decouple the OTP implementation from the need for an auth factor when arriving on the email 2FA screen post SSO.

* PM-1196 - Refactored OTP solution to leverage newly created SsoEmail2faSessionTokenable. Working now but some code cleanup required. Might revisit whether or not we still send down email alongside the token or not to make the SendEmailLoginAsync method more streamlined.

* PM-1196 - Send down email separately on token rejection b/c of 2FA required so that 2FA Controller send email login can be refactored to be much cleaner with email required.

* PM-1196 - Fix lint issues w/ dotnet format.

* PM-1196 - More formatting issue fixes.

* PM-1196 - Remove unnecessary check as email is required again on TwoFactorEmailRequestModel

* PM-1196 - Update SsoEmail2faSessionTokenable to expire after just over 2 min to match client side auth service expiration of 2 min with small buffer.

* PM-1196 - Fix lint issue w/ dotnet format.

* PM-1196 - Per PR feedback, move CustomTokenRequestValidator constructor param to new line

* PM-1196 - Per PR feedback, update ThrowDelayedBadRequestExceptionAsync to return a task so that it can be awaited and so that the calling code can handle any exceptions that occur during its execution

* PM-1196 - Per PR feedback, refactor SsoEmail2faSessionTokenable to leverage TimeSpan vs double for token expiration lifetime.
This commit is contained in:
Jared Snider
2023-05-04 15:12:03 -04:00
committed by GitHub
parent b87846f97f
commit 2ac513e15a
8 changed files with 181 additions and 56 deletions

View File

@ -6,6 +6,7 @@ using Bit.Core;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -16,6 +17,7 @@ using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Identity;
@ -40,6 +42,7 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly IPolicyRepository _policyRepository;
private readonly IUserRepository _userRepository;
private readonly IPolicyService _policyService;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
public BaseRequestValidator(
UserManager<User> userManager,
@ -57,7 +60,8 @@ public abstract class BaseRequestValidator<T> where T : class
GlobalSettings globalSettings,
IPolicyRepository policyRepository,
IUserRepository userRepository,
IPolicyService policyService)
IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
{
_userManager = userManager;
_deviceRepository = deviceRepository;
@ -75,6 +79,7 @@ public abstract class BaseRequestValidator<T> where T : class
_policyRepository = policyRepository;
_userRepository = userRepository;
_policyService = policyService;
_tokenDataFactory = tokenDataFactory;
}
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
@ -92,7 +97,7 @@ public abstract class BaseRequestValidator<T> where T : class
var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString();
var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1";
var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
!string.IsNullOrWhiteSpace(twoFactorProvider);
!string.IsNullOrWhiteSpace(twoFactorProvider);
var valid = await ValidateContextAsync(context, validatorContext);
var user = validatorContext.User;
@ -100,6 +105,7 @@ public abstract class BaseRequestValidator<T> where T : class
{
await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice);
}
if (!valid || isBot)
{
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
@ -150,14 +156,16 @@ public abstract class BaseRequestValidator<T> where T : class
await BuildErrorResultAsync("No device information provided.", false, context, user);
return;
}
await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember);
}
else
{
SetSsoResult(context, new Dictionary<string, object>
{{
"ErrorModel", new ErrorResponseModel("SSO authentication is required.")
}});
SetSsoResult(context,
new Dictionary<string, object>
{
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
});
}
}
@ -240,13 +248,23 @@ public abstract class BaseRequestValidator<T> where T : class
providers.Add(((byte)provider.Key).ToString(), infoDict);
}
SetTwoFactorResult(context,
new Dictionary<string, object>
{
{ "TwoFactorProviders", providers.Keys },
{ "TwoFactorProviders2", providers },
{ "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) }
});
var twoFactorResultDict = new Dictionary<string, object>
{
{ "TwoFactorProviders", providers.Keys },
{ "TwoFactorProviders2", providers },
{ "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) },
};
// If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token
if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email))
{
twoFactorResultDict.Add("SsoEmail2faSessionToken",
_tokenDataFactory.Protect(new SsoEmail2faSessionTokenable(user)));
twoFactorResultDict.Add("Email", user.Email);
}
SetTwoFactorResult(context, twoFactorResultDict);
if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email)
{
@ -272,10 +290,7 @@ public abstract class BaseRequestValidator<T> where T : class
await Task.Delay(2000); // Delay for brute force.
SetErrorResult(context,
new Dictionary<string, object>
{{
"ErrorModel", new ErrorResponseModel(message)
}});
new Dictionary<string, object> { { "ErrorModel", new ErrorResponseModel(message) } });
}
protected abstract void SetTwoFactorResult(T context, Dictionary<string, object> customResponse);
@ -296,8 +311,8 @@ public abstract class BaseRequestValidator<T> where T : class
}
var individualRequired = _userManager.SupportsUserTwoFactor &&
await _userManager.GetTwoFactorEnabledAsync(user) &&
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
await _userManager.GetTwoFactorEnabledAsync(user) &&
(await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
Organization firstEnabledOrg = null;
var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id))
@ -346,7 +361,8 @@ public abstract class BaseRequestValidator<T> where T : class
PolicyType.RequireSso);
// Owners and Admins are exempt from this policy
if (orgPolicy != null && orgPolicy.Enabled &&
(_globalSettings.Sso.EnforceSsoPolicyForAllUsers || (userOrg.Type != OrganizationUserType.Owner && userOrg.Type != OrganizationUserType.Admin)))
(_globalSettings.Sso.EnforceSsoPolicyForAllUsers ||
(userOrg.Type != OrganizationUserType.Owner && userOrg.Type != OrganizationUserType.Admin)))
{
return false;
}
@ -361,7 +377,7 @@ public abstract class BaseRequestValidator<T> where T : class
private bool OrgUsing2fa(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
{
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
}
private bool OrgCanUseSso(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
@ -408,6 +424,7 @@ public abstract class BaseRequestValidator<T> where T : class
{
return false;
}
return await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(type), token);
case TwoFactorProviderType.OrganizationDuo:
@ -457,18 +474,13 @@ public abstract class BaseRequestValidator<T> where T : class
}
else if (type == TwoFactorProviderType.Email)
{
return new Dictionary<string, object>
{
["Email"] = token
};
return new Dictionary<string, object> { ["Email"] = token };
}
else if (type == TwoFactorProviderType.YubiKey)
{
return new Dictionary<string, object>
{
["Nfc"] = (bool)provider.MetaData["Nfc"]
};
return new Dictionary<string, object> { ["Nfc"] = (bool)provider.MetaData["Nfc"] };
}
return null;
case TwoFactorProviderType.OrganizationDuo:
if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization))
@ -479,6 +491,7 @@ public abstract class BaseRequestValidator<T> where T : class
["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user)
};
}
return null;
default:
return null;

View File

@ -1,5 +1,6 @@
using System.Security.Claims;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -7,6 +8,7 @@ using Bit.Core.IdentityServer;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using IdentityModel;
using IdentityServer4.Extensions;
using IdentityServer4.Validation;
@ -37,11 +39,12 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository,
IUserRepository userRepository,
IPolicyService policyService)
IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository, policyService)
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository, policyService, tokenDataFactory)
{
_userManager = userManager;
_ssoConfigRepository = ssoConfigRepository;
@ -73,11 +76,13 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
CustomValidatorRequestContext validatorContext)
{
var email = context.Result.ValidatedRequest.Subject?.GetDisplayName()
?? context.Result.ValidatedRequest.ClientClaims?.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email)?.Value;
?? context.Result.ValidatedRequest.ClientClaims
?.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Email)?.Value;
if (!string.IsNullOrWhiteSpace(email))
{
validatorContext.User = await _userManager.FindByEmailAsync(email);
}
return validatorContext.User != null;
}
@ -111,6 +116,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
context.Result.CustomResponse["ApiUseKeyConnector"] = true;
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
return;
}

View File

@ -1,11 +1,13 @@
using System.Security.Claims;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tokens;
using Bit.Core.Utilities;
using IdentityServer4.Models;
using IdentityServer4.Validation;
@ -39,11 +41,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
ICaptchaValidationService captchaValidationService,
IAuthRequestRepository authRequestRepository,
IUserRepository userRepository,
IPolicyService policyService)
IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, policyRepository,
userRepository, policyService)
userRepository, policyService, tokenDataFactory)
{
_userManager = userManager;
_userService = userService;