mirror of
https://github.com/bitwarden/server
synced 2025-12-24 04:03:25 +00:00
fix(base-request-validator) [PM-21153] Recovery Code Not Functioning for SSO-required Users (#6481)
* chore(feature-flag-keys) [PM-21153]: Add feature flag key for BaseRequestValidator changes. * fix(base-request-validator) [PM-21153]: Add validation state model for composable validation scenarios. * fix(base-request-validator) [PM-21153]: Update BaseRequestValidator to allow validation scenarios to be composable. * fix(base-request-validator) [PM-21153]: Remove validation state object in favor of validator context, per team discussion. * feat(base-request-validator) [PM-21153]: Update tests to use issue feature flag, both execution paths. * fix(base-request-validator) [PM-21153]: Fix a null dictionary check. * chore(base-request-validator) [PM-21153]: Add unit tests around behavior addressed in this feature. * chore(base-request-validator) [PM-21153]: Update comments for clarity. * chore(base-request-validator-tests) [PM-21153]: Update verbiage for tests. * fix(base-request-validator) [PM-21153]: Update validators to no longer need completed scheme management, use 2FA flag for recovery scenarios. * fix(base-request-validator-tests) [PM-21153]: Customize CustomValidatorRequestContext fixture to allow for setting of request-specific flags as part of the request validation (not eagerly truthy).
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Security.Claims;
|
||||
@@ -68,7 +69,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
IMailService mailService,
|
||||
IUserAccountKeysQuery userAccountKeysQuery
|
||||
)
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userService = userService;
|
||||
@@ -93,125 +94,141 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
// 1. We need to check if the user's master password hash is correct.
|
||||
var valid = await ValidateContextAsync(context, validatorContext);
|
||||
var user = validatorContext.User;
|
||||
if (!valid)
|
||||
if (FeatureService.IsEnabled(FeatureFlagKeys.RecoveryCodeSupportForSsoRequiredUsers))
|
||||
{
|
||||
await UpdateFailedAuthDetailsAsync(user);
|
||||
|
||||
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Decide if this user belongs to an organization that requires SSO.
|
||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
|
||||
if (validatorContext.SsoRequired)
|
||||
{
|
||||
SetSsoResult(context,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Check if 2FA is required.
|
||||
(validatorContext.TwoFactorRequired, var twoFactorOrganization) =
|
||||
await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request);
|
||||
|
||||
// This flag is used to determine if the user wants a rememberMe token sent when
|
||||
// authentication is successful.
|
||||
var returnRememberMeToken = false;
|
||||
|
||||
if (validatorContext.TwoFactorRequired)
|
||||
{
|
||||
var twoFactorToken = request.Raw["TwoFactorToken"];
|
||||
var twoFactorProvider = request.Raw["TwoFactorProvider"];
|
||||
var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||
|
||||
// 3a. Response for 2FA required and not provided state.
|
||||
if (!validTwoFactorRequest ||
|
||||
!Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
|
||||
var validators = DetermineValidationOrder(context, request, validatorContext);
|
||||
var allValidationSchemesSuccessful = await ProcessValidatorsAsync(validators);
|
||||
if (!allValidationSchemesSuccessful)
|
||||
{
|
||||
var resultDict = await _twoFactorAuthenticationValidator
|
||||
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
||||
if (resultDict == null)
|
||||
// Each validation task is responsible for setting its own non-success status, if applicable.
|
||||
return;
|
||||
}
|
||||
await BuildSuccessResultAsync(validatorContext.User, context, validatorContext.Device,
|
||||
validatorContext.RememberMeRequested);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 1. We need to check if the user's master password hash is correct.
|
||||
var valid = await ValidateContextAsync(context, validatorContext);
|
||||
var user = validatorContext.User;
|
||||
if (!valid)
|
||||
{
|
||||
await UpdateFailedAuthDetailsAsync(user);
|
||||
|
||||
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Decide if this user belongs to an organization that requires SSO.
|
||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
|
||||
if (validatorContext.SsoRequired)
|
||||
{
|
||||
SetSsoResult(context,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Check if 2FA is required.
|
||||
(validatorContext.TwoFactorRequired, var twoFactorOrganization) =
|
||||
await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request);
|
||||
|
||||
// This flag is used to determine if the user wants a rememberMe token sent when
|
||||
// authentication is successful.
|
||||
var returnRememberMeToken = false;
|
||||
|
||||
if (validatorContext.TwoFactorRequired)
|
||||
{
|
||||
var twoFactorToken = request.Raw["TwoFactorToken"];
|
||||
var twoFactorProvider = request.Raw["TwoFactorProvider"];
|
||||
var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||
|
||||
// 3a. Response for 2FA required and not provided state.
|
||||
if (!validTwoFactorRequest ||
|
||||
!Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
|
||||
{
|
||||
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
|
||||
var resultDict = await _twoFactorAuthenticationValidator
|
||||
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
||||
if (resultDict == null)
|
||||
{
|
||||
await BuildErrorResultAsync("No two-step providers enabled.", false, context, user);
|
||||
return;
|
||||
}
|
||||
|
||||
// Include Master Password Policy in 2FA response.
|
||||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
|
||||
SetTwoFactorResult(context, resultDict);
|
||||
return;
|
||||
}
|
||||
|
||||
// Include Master Password Policy in 2FA response.
|
||||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
|
||||
SetTwoFactorResult(context, resultDict);
|
||||
var twoFactorTokenValid =
|
||||
await _twoFactorAuthenticationValidator
|
||||
.VerifyTwoFactorAsync(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken);
|
||||
|
||||
// 3b. Response for 2FA required but request is not valid or remember token expired state.
|
||||
if (!twoFactorTokenValid)
|
||||
{
|
||||
// The remember me token has expired.
|
||||
if (twoFactorProviderType == TwoFactorProviderType.Remember)
|
||||
{
|
||||
var resultDict = await _twoFactorAuthenticationValidator
|
||||
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
||||
|
||||
// Include Master Password Policy in 2FA response
|
||||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
|
||||
SetTwoFactorResult(context, resultDict);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendFailedTwoFactorEmail(user, twoFactorProviderType);
|
||||
await UpdateFailedAuthDetailsAsync(user);
|
||||
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 3c. When the 2FA authentication is successful, we can check if the user wants a
|
||||
// rememberMe token.
|
||||
var twoFactorRemember = request.Raw["TwoFactorRemember"] == "1";
|
||||
// Check if the user wants a rememberMe token.
|
||||
if (twoFactorRemember
|
||||
// if the 2FA auth was rememberMe do not send another token.
|
||||
&& twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||
{
|
||||
returnRememberMeToken = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check if the user is logging in from a new device.
|
||||
var deviceValid = await _deviceValidator.ValidateRequestDeviceAsync(request, validatorContext);
|
||||
if (!deviceValid)
|
||||
{
|
||||
SetValidationErrorResult(context, validatorContext);
|
||||
await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn);
|
||||
return;
|
||||
}
|
||||
|
||||
var twoFactorTokenValid =
|
||||
await _twoFactorAuthenticationValidator
|
||||
.VerifyTwoFactorAsync(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken);
|
||||
|
||||
// 3b. Response for 2FA required but request is not valid or remember token expired state.
|
||||
if (!twoFactorTokenValid)
|
||||
// 5. Force legacy users to the web for migration.
|
||||
if (UserService.IsLegacyUser(user) && request.ClientId != "web")
|
||||
{
|
||||
// The remember me token has expired.
|
||||
if (twoFactorProviderType == TwoFactorProviderType.Remember)
|
||||
{
|
||||
var resultDict = await _twoFactorAuthenticationValidator
|
||||
.BuildTwoFactorResultAsync(user, twoFactorOrganization);
|
||||
|
||||
// Include Master Password Policy in 2FA response
|
||||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
|
||||
SetTwoFactorResult(context, resultDict);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendFailedTwoFactorEmail(user, twoFactorProviderType);
|
||||
await UpdateFailedAuthDetailsAsync(user);
|
||||
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user);
|
||||
}
|
||||
await FailAuthForLegacyUserAsync(user, context);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3c. When the 2FA authentication is successful, we can check if the user wants a
|
||||
// rememberMe token.
|
||||
var twoFactorRemember = request.Raw["TwoFactorRemember"] == "1";
|
||||
// Check if the user wants a rememberMe token.
|
||||
if (twoFactorRemember
|
||||
// if the 2FA auth was rememberMe do not send another token.
|
||||
&& twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||
// TODO: PM-24324 - This should be its own validator at some point.
|
||||
// 6. Auth request handling
|
||||
if (validatorContext.ValidatedAuthRequest != null)
|
||||
{
|
||||
returnRememberMeToken = true;
|
||||
validatorContext.ValidatedAuthRequest.AuthenticationDate = DateTime.UtcNow;
|
||||
await _authRequestRepository.ReplaceAsync(validatorContext.ValidatedAuthRequest);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check if the user is logging in from a new device.
|
||||
var deviceValid = await _deviceValidator.ValidateRequestDeviceAsync(request, validatorContext);
|
||||
if (!deviceValid)
|
||||
{
|
||||
SetValidationErrorResult(context, validatorContext);
|
||||
await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn);
|
||||
return;
|
||||
await BuildSuccessResultAsync(user, context, validatorContext.Device, returnRememberMeToken);
|
||||
}
|
||||
|
||||
// 5. Force legacy users to the web for migration.
|
||||
if (UserService.IsLegacyUser(user) && request.ClientId != "web")
|
||||
{
|
||||
await FailAuthForLegacyUserAsync(user, context);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: PM-24324 - This should be its own validator at some point.
|
||||
// 6. Auth request handling
|
||||
if (validatorContext.ValidatedAuthRequest != null)
|
||||
{
|
||||
validatorContext.ValidatedAuthRequest.AuthenticationDate = DateTime.UtcNow;
|
||||
await _authRequestRepository.ReplaceAsync(validatorContext.ValidatedAuthRequest);
|
||||
}
|
||||
|
||||
await BuildSuccessResultAsync(user, context, validatorContext.Device, returnRememberMeToken);
|
||||
}
|
||||
|
||||
protected async Task FailAuthForLegacyUserAsync(User user, T context)
|
||||
@@ -223,6 +240,302 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
|
||||
protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);
|
||||
|
||||
/// <summary>
|
||||
/// Composer for validation schemes.
|
||||
/// </summary>
|
||||
/// <param name="context">The current request context.</param>
|
||||
/// <param name="request"><see cref="Duende.IdentityServer.Validation.ValidatedTokenRequest" /></param>
|
||||
/// <param name="validatorContext"><see cref="Bit.Identity.IdentityServer.CustomValidatorRequestContext" /></param>
|
||||
/// <returns>A composed array of validation scheme delegates to evaluate in order.</returns>
|
||||
private Func<Task<bool>>[] DetermineValidationOrder(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
if (RecoveryCodeRequestForSsoRequiredUserScenario())
|
||||
{
|
||||
// Support valid requests to recover 2FA (with account code) for users who require SSO
|
||||
// by organization membership.
|
||||
// This requires an evaluation of 2FA validity in front of SSO, and an opportunity for the 2FA
|
||||
// validation to perform the recovery as part of scheme validation based on the request.
|
||||
return
|
||||
[
|
||||
() => ValidateMasterPasswordAsync(context, validatorContext),
|
||||
() => ValidateTwoFactorAsync(context, request, validatorContext),
|
||||
() => ValidateSsoAsync(context, request, validatorContext),
|
||||
() => ValidateNewDeviceAsync(context, request, validatorContext),
|
||||
() => ValidateLegacyMigrationAsync(context, request, validatorContext),
|
||||
() => ValidateAuthRequestAsync(validatorContext)
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
// The typical validation scenario.
|
||||
return
|
||||
[
|
||||
() => ValidateMasterPasswordAsync(context, validatorContext),
|
||||
() => ValidateSsoAsync(context, request, validatorContext),
|
||||
() => ValidateTwoFactorAsync(context, request, validatorContext),
|
||||
() => ValidateNewDeviceAsync(context, request, validatorContext),
|
||||
() => ValidateLegacyMigrationAsync(context, request, validatorContext),
|
||||
() => ValidateAuthRequestAsync(validatorContext)
|
||||
];
|
||||
}
|
||||
|
||||
bool RecoveryCodeRequestForSsoRequiredUserScenario()
|
||||
{
|
||||
var twoFactorProvider = request.Raw["TwoFactorProvider"];
|
||||
var twoFactorToken = request.Raw["TwoFactorToken"];
|
||||
|
||||
// Both provider and token must be present;
|
||||
// Validity of the token for a given provider will be evaluated by the TwoFactorAuthenticationValidator.
|
||||
if (string.IsNullOrWhiteSpace(twoFactorProvider) || string.IsNullOrWhiteSpace(twoFactorToken))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!int.TryParse(twoFactorProvider, out var providerValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return providerValue == (int)TwoFactorProviderType.RecoveryCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the validation schemes sequentially.
|
||||
/// Each validator is responsible for setting error context responses on failure and adding itself to the
|
||||
/// validatorContext's CompletedValidationSchemes (only) on success.
|
||||
/// Failure of any scheme to validate will short-circuit the collection, causing the validation error to be
|
||||
/// returned and further schemes to not be evaluated.
|
||||
/// </summary>
|
||||
/// <param name="validators">The collection of validation schemes as composed in <see cref="DetermineValidationOrder" /></param>
|
||||
/// <returns>true if all schemes validated successfully, false if any failed.</returns>
|
||||
private static async Task<bool> ProcessValidatorsAsync(params Func<Task<bool>>[] validators)
|
||||
{
|
||||
foreach (var validator in validators)
|
||||
{
|
||||
if (!await validator())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the user's Master Password hash.
|
||||
/// </summary>
|
||||
/// <param name="context">The current request context.</param>
|
||||
/// <param name="validatorContext"><see cref="Bit.Identity.IdentityServer.CustomValidatorRequestContext" /></param>
|
||||
/// <returns>true if the scheme successfully passed validation, otherwise false.</returns>
|
||||
private async Task<bool> ValidateMasterPasswordAsync(T context, CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
var valid = await ValidateContextAsync(context, validatorContext);
|
||||
var user = validatorContext.User;
|
||||
if (valid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await UpdateFailedAuthDetailsAsync(user);
|
||||
|
||||
await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the user's organization-enforced Single Sign-on (SSO) requirement.
|
||||
/// </summary>
|
||||
/// <param name="context">The current request context.</param>
|
||||
/// <param name="request"><see cref="Duende.IdentityServer.Validation.ValidatedTokenRequest" /></param>
|
||||
/// <param name="validatorContext"><see cref="Bit.Identity.IdentityServer.CustomValidatorRequestContext" /></param>
|
||||
/// <returns>true if the scheme successfully passed validation, otherwise false.</returns>
|
||||
/// <seealso cref="DetermineValidationOrder" />
|
||||
private async Task<bool> ValidateSsoAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(validatorContext.User, request.GrantType);
|
||||
if (!validatorContext.SsoRequired)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Users without SSO requirement requesting 2FA recovery will be fast-forwarded through login and are
|
||||
// presented with their 2FA management area as a reminder to re-evaluate their 2FA posture after recovery and
|
||||
// review their new recovery token if desired.
|
||||
// SSO users cannot be assumed to be authenticated, and must prove authentication with their IdP after recovery.
|
||||
// As described in validation order determination, if TwoFactorRequired, the 2FA validation scheme will have been
|
||||
// evaluated, and recovery will have been performed if requested.
|
||||
// We will send a descriptive message in these cases so clients can give the appropriate feedback and redirect
|
||||
// to /login.
|
||||
if (validatorContext.TwoFactorRequired &&
|
||||
validatorContext.TwoFactorRecoveryRequested)
|
||||
{
|
||||
SetSsoResult(context, new Dictionary<string, object>
|
||||
{
|
||||
{ "ErrorModel", new ErrorResponseModel("Two-factor recovery has been performed. SSO authentication is required.") }
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
SetSsoResult(context,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the user's Multi-Factor Authentication (2FA) scheme.
|
||||
/// </summary>
|
||||
/// <param name="context">The current request context.</param>
|
||||
/// <param name="request"><see cref="Duende.IdentityServer.Validation.ValidatedTokenRequest" /></param>
|
||||
/// <param name="validatorContext"><see cref="Bit.Identity.IdentityServer.CustomValidatorRequestContext" /></param>
|
||||
/// <returns>true if the scheme successfully passed validation, otherwise false.</returns>
|
||||
private async Task<bool> ValidateTwoFactorAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
(validatorContext.TwoFactorRequired, var twoFactorOrganization) =
|
||||
await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(validatorContext.User, request);
|
||||
|
||||
if (!validatorContext.TwoFactorRequired)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var twoFactorToken = request.Raw["TwoFactorToken"];
|
||||
var twoFactorProvider = request.Raw["TwoFactorProvider"];
|
||||
var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) &&
|
||||
!string.IsNullOrWhiteSpace(twoFactorProvider);
|
||||
|
||||
// 3a. Response for 2FA required and not provided state.
|
||||
if (!validTwoFactorRequest ||
|
||||
!Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
|
||||
{
|
||||
var resultDict = await _twoFactorAuthenticationValidator
|
||||
.BuildTwoFactorResultAsync(validatorContext.User, twoFactorOrganization);
|
||||
if (resultDict == null)
|
||||
{
|
||||
await BuildErrorResultAsync("No two-step providers enabled.", false, context, validatorContext.User);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Include Master Password Policy in 2FA response.
|
||||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(validatorContext.User));
|
||||
SetTwoFactorResult(context, resultDict);
|
||||
return false;
|
||||
}
|
||||
|
||||
var twoFactorTokenValid =
|
||||
await _twoFactorAuthenticationValidator
|
||||
.VerifyTwoFactorAsync(validatorContext.User, twoFactorOrganization, twoFactorProviderType,
|
||||
twoFactorToken);
|
||||
|
||||
// 3b. Response for 2FA required but request is not valid or remember token expired state.
|
||||
if (!twoFactorTokenValid)
|
||||
{
|
||||
// The remember me token has expired.
|
||||
if (twoFactorProviderType == TwoFactorProviderType.Remember)
|
||||
{
|
||||
var resultDict = await _twoFactorAuthenticationValidator
|
||||
.BuildTwoFactorResultAsync(validatorContext.User, twoFactorOrganization);
|
||||
|
||||
// Include Master Password Policy in 2FA response
|
||||
resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(validatorContext.User));
|
||||
SetTwoFactorResult(context, resultDict);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendFailedTwoFactorEmail(validatorContext.User, twoFactorProviderType);
|
||||
await UpdateFailedAuthDetailsAsync(validatorContext.User);
|
||||
await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context,
|
||||
validatorContext.User);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3c. Given a valid token and a successful two-factor verification, if the provider type is Recovery Code,
|
||||
// recovery will have been performed as part of 2FA validation. This will be relevant for, e.g., SSO users
|
||||
// who are requesting recovery, but who will still need to log in after 2FA recovery.
|
||||
if (twoFactorProviderType == TwoFactorProviderType.RecoveryCode)
|
||||
{
|
||||
validatorContext.TwoFactorRecoveryRequested = true;
|
||||
}
|
||||
|
||||
// 3d. When the 2FA authentication is successful, we can check if the user wants a
|
||||
// rememberMe token.
|
||||
var twoFactorRemember = request.Raw["TwoFactorRemember"] == "1";
|
||||
// Check if the user wants a rememberMe token.
|
||||
if (twoFactorRemember
|
||||
// if the 2FA auth was rememberMe do not send another token.
|
||||
&& twoFactorProviderType != TwoFactorProviderType.Remember)
|
||||
{
|
||||
validatorContext.RememberMeRequested = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates whether the user is logging in from a known device.
|
||||
/// </summary>
|
||||
/// <param name="context">The current request context.</param>
|
||||
/// <param name="request"><see cref="Duende.IdentityServer.Validation.ValidatedTokenRequest" /></param>
|
||||
/// <param name="validatorContext"><see cref="Bit.Identity.IdentityServer.CustomValidatorRequestContext" /></param>
|
||||
/// <returns>true if the scheme successfully passed validation, otherwise false.</returns>
|
||||
private async Task<bool> ValidateNewDeviceAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
var deviceValid = await _deviceValidator.ValidateRequestDeviceAsync(request, validatorContext);
|
||||
if (deviceValid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
SetValidationErrorResult(context, validatorContext);
|
||||
await LogFailedLoginEvent(validatorContext.User, EventType.User_FailedLogIn);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates whether the user should be denied access on a given non-Web client and sent to the Web client
|
||||
/// for Legacy migration.
|
||||
/// </summary>
|
||||
/// <param name="context">The current request context.</param>
|
||||
/// <param name="request"><see cref="Duende.IdentityServer.Validation.ValidatedTokenRequest" /></param>
|
||||
/// <param name="validatorContext"><see cref="Bit.Identity.IdentityServer.CustomValidatorRequestContext" /></param>
|
||||
/// <returns>true if the scheme successfully passed validation, otherwise false.</returns>
|
||||
private async Task<bool> ValidateLegacyMigrationAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
if (!UserService.IsLegacyUser(validatorContext.User) || request.ClientId == "web")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await FailAuthForLegacyUserAsync(validatorContext.User, context);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates and updates the auth request's timestamp.
|
||||
/// </summary>
|
||||
/// <param name="validatorContext"><see cref="Bit.Identity.IdentityServer.CustomValidatorRequestContext" /></param>
|
||||
/// <returns>true on evaluation and/or completed update of the AuthRequest.</returns>
|
||||
private async Task<bool> ValidateAuthRequestAsync(CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
// TODO: PM-24324 - This should be its own validator at some point.
|
||||
if (validatorContext.ValidatedAuthRequest != null)
|
||||
{
|
||||
validatorContext.ValidatedAuthRequest.AuthenticationDate = DateTime.UtcNow;
|
||||
await _authRequestRepository.ReplaceAsync(validatorContext.ValidatedAuthRequest);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for building the response to the client when the user has successfully authenticated.
|
||||
@@ -256,7 +569,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
/// <param name="user">used to associate the failed login with a user</param>
|
||||
/// <returns>void</returns>
|
||||
[Obsolete("Consider using SetValidationErrorResult to set the validation result, and LogFailedLoginEvent " +
|
||||
"to log the failure.")]
|
||||
"to log the failure.")]
|
||||
protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user)
|
||||
{
|
||||
if (user != null)
|
||||
@@ -268,7 +581,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
if (_globalSettings.SelfHosted)
|
||||
{
|
||||
_logger.LogWarning(Constants.BypassFiltersEventId,
|
||||
"Failed login attempt. Is2FARequest: {Is2FARequest} IpAddress: {IpAddress}", twoFactorRequest, CurrentContext.IpAddress);
|
||||
"Failed login attempt. Is2FARequest: {Is2FARequest} IpAddress: {IpAddress}", twoFactorRequest,
|
||||
CurrentContext.IpAddress);
|
||||
}
|
||||
|
||||
await Task.Delay(2000); // Delay for brute force.
|
||||
@@ -292,21 +606,26 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
formattedMessage = string.Format("Failed login attempt. {0}", $" {CurrentContext.IpAddress}");
|
||||
break;
|
||||
case EventType.User_FailedLogIn2fa:
|
||||
formattedMessage = string.Format("Failed login attempt, 2FA invalid.{0}", $" {CurrentContext.IpAddress}");
|
||||
formattedMessage = string.Format("Failed login attempt, 2FA invalid.{0}",
|
||||
$" {CurrentContext.IpAddress}");
|
||||
break;
|
||||
default:
|
||||
formattedMessage = "Failed login attempt.";
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.LogWarning(Constants.BypassFiltersEventId, "{FailedLoginMessage}", formattedMessage);
|
||||
}
|
||||
|
||||
await Task.Delay(2000); // Delay for brute force.
|
||||
}
|
||||
|
||||
[Obsolete("Consider using SetValidationErrorResult instead.")]
|
||||
protected abstract void SetTwoFactorResult(T context, Dictionary<string, object> customResponse);
|
||||
|
||||
[Obsolete("Consider using SetValidationErrorResult instead.")]
|
||||
protected abstract void SetSsoResult(T context, Dictionary<string, object> customResponse);
|
||||
|
||||
[Obsolete("Consider using SetValidationErrorResult instead.")]
|
||||
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);
|
||||
|
||||
@@ -317,6 +636,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
/// <param name="context">The current grant or token context</param>
|
||||
/// <param name="requestContext">The modified request context containing material used to build the response object</param>
|
||||
protected abstract void SetValidationErrorResult(T context, CustomValidatorRequestContext requestContext);
|
||||
|
||||
protected abstract Task SetSuccessResult(T context, User user, List<Claim> claims,
|
||||
Dictionary<string, object> customResponse);
|
||||
|
||||
@@ -343,7 +663,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
// Check if user belongs to any organization with an active SSO policy
|
||||
var ssoRequired = FeatureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
? (await PolicyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(user.Id))
|
||||
.SsoRequired
|
||||
.SsoRequired
|
||||
: await PolicyService.AnyPoliciesApplicableToUserAsync(
|
||||
user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
if (ssoRequired)
|
||||
@@ -385,7 +705,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
{
|
||||
if (FeatureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail))
|
||||
{
|
||||
await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemptType, DateTime.UtcNow, CurrentContext.IpAddress);
|
||||
await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemptType, DateTime.UtcNow,
|
||||
CurrentContext.IpAddress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,16 +737,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
// We need this because we check for changes in the stamp to determine if we need to invalidate token refresh requests,
|
||||
// in the `ProfileService.IsActiveAsync` method.
|
||||
// If we don't store the security stamp in the persisted grant, we won't have the previous value to compare against.
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new Claim(Claims.SecurityStamp, user.SecurityStamp)
|
||||
};
|
||||
var claims = new List<Claim> { new Claim(Claims.SecurityStamp, user.SecurityStamp) };
|
||||
|
||||
if (device != null)
|
||||
{
|
||||
claims.Add(new Claim(Claims.Device, device.Identifier));
|
||||
claims.Add(new Claim(Claims.DeviceType, device.Type.ToString()));
|
||||
}
|
||||
|
||||
return claims;
|
||||
}
|
||||
|
||||
@@ -437,7 +756,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
/// <param name="context">The current request context.</param>
|
||||
/// <param name="device">The device used for authentication.</param>
|
||||
/// <param name="sendRememberToken">Whether to send a 2FA remember token.</param>
|
||||
private async Task<Dictionary<string, object>> BuildCustomResponse(User user, T context, Device device, bool sendRememberToken)
|
||||
private async Task<Dictionary<string, object>> BuildCustomResponse(User user, T context, Device device,
|
||||
bool sendRememberToken)
|
||||
{
|
||||
var customResponse = new Dictionary<string, object>();
|
||||
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
|
||||
@@ -459,7 +779,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
customResponse.Add("KdfIterations", user.KdfIterations);
|
||||
customResponse.Add("KdfMemory", user.KdfMemory);
|
||||
customResponse.Add("KdfParallelism", user.KdfParallelism);
|
||||
customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context)));
|
||||
customResponse.Add("UserDecryptionOptions",
|
||||
await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context)));
|
||||
|
||||
if (sendRememberToken)
|
||||
{
|
||||
@@ -467,6 +788,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
|
||||
customResponse.Add("TwoFactorToken", token);
|
||||
}
|
||||
|
||||
return customResponse;
|
||||
}
|
||||
|
||||
@@ -474,7 +796,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
/// <summary>
|
||||
/// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents
|
||||
/// </summary>
|
||||
private async Task<UserDecryptionOptions> CreateUserDecryptionOptionsAsync(User user, Device device, ClaimsPrincipal subject)
|
||||
private async Task<UserDecryptionOptions> CreateUserDecryptionOptionsAsync(User user, Device device,
|
||||
ClaimsPrincipal subject)
|
||||
{
|
||||
var ssoConfig = await GetSsoConfigurationDataAsync(subject);
|
||||
return await UserDecryptionOptionsBuilder
|
||||
|
||||
Reference in New Issue
Block a user