mirror of
https://github.com/bitwarden/server
synced 2026-01-02 08:33:48 +00:00
Merge branch 'main' into auth/pm-22975/client-version-validator
This commit is contained in:
@@ -100,167 +100,16 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeSupportForSsoRequiredUsers))
|
||||
var validators = DetermineValidationOrder(context, request, validatorContext);
|
||||
var allValidationSchemesSuccessful = await ProcessValidatorsAsync(validators);
|
||||
if (!allValidationSchemesSuccessful)
|
||||
{
|
||||
var validators = DetermineValidationOrder(context, request, validatorContext);
|
||||
var allValidationSchemesSuccessful = await ProcessValidatorsAsync(validators);
|
||||
if (!allValidationSchemesSuccessful)
|
||||
{
|
||||
// Each validation task is responsible for setting its own non-success status, if applicable.
|
||||
return;
|
||||
}
|
||||
await BuildSuccessResultAsync(validatorContext.User, context, validatorContext.Device,
|
||||
validatorContext.RememberMeRequested);
|
||||
// Each validation task is responsible for setting its own non-success status, if applicable.
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 1. We need to check if the user is legitimate via the contextually appropriate mechanism
|
||||
// (webauthn, password, custom token, etc.).
|
||||
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;
|
||||
}
|
||||
|
||||
// 1.5 Now check the version number of the client. Do this after ValidateContextAsync so that
|
||||
// we prevent account enumeration. If we were to do this before ValidateContextAsync, then attackers
|
||||
// could use a known invalid client version and make a request for a user (before we know if they have
|
||||
// demonstrated ownership of the account via correct credentials) and identify if they exist by getting
|
||||
// an error response back from the validator saying the user is not compatible with the client.
|
||||
var clientVersionValid = await ValidateClientVersionAsync(context, validatorContext);
|
||||
if (!clientVersionValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Decide if this user belongs to an organization that requires SSO.
|
||||
// TODO: Clean up Feature Flag: Remove this if block: PM-28281
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired))
|
||||
{
|
||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
|
||||
if (validatorContext.SsoRequired)
|
||||
{
|
||||
SetSsoResult(context,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var ssoValid = await _ssoRequestValidator.ValidateAsync(user, request, validatorContext);
|
||||
if (!ssoValid)
|
||||
{
|
||||
// SSO is required
|
||||
SetValidationErrorResult(context, validatorContext);
|
||||
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 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
await BuildSuccessResultAsync(validatorContext.User, context, validatorContext.Device,
|
||||
validatorContext.RememberMeRequested);
|
||||
}
|
||||
|
||||
protected async Task FailAuthForLegacyUserAsync(User user, T context)
|
||||
@@ -291,6 +140,11 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
return
|
||||
[
|
||||
() => ValidateGrantSpecificContext(context, validatorContext),
|
||||
// Now check the version number of the client. Do this after ValidateContextAsync so that
|
||||
// we prevent account enumeration. If we were to do this before ValidateContextAsync, then attackers
|
||||
// could use a known invalid client version and make a request for a user (before we know if they have
|
||||
// demonstrated ownership of the account via correct credentials) and identify if they exist by getting
|
||||
// an error response back from the validator saying the user is not compatible with the client.
|
||||
() => ValidateClientVersionAsync(context, validatorContext),
|
||||
() => ValidateTwoFactorAsync(context, request, validatorContext),
|
||||
() => ValidateSsoAsync(context, request, validatorContext),
|
||||
@@ -305,6 +159,11 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
return
|
||||
[
|
||||
() => ValidateGrantSpecificContext(context, validatorContext),
|
||||
// Now check the version number of the client. Do this after ValidateContextAsync so that
|
||||
// we prevent account enumeration. If we were to do this before ValidateContextAsync, then attackers
|
||||
// could use a known invalid client version and make a request for a user (before we know if they have
|
||||
// demonstrated ownership of the account via correct credentials) and identify if they exist by getting
|
||||
// an error response back from the validator saying the user is not compatible with the client.
|
||||
() => ValidateClientVersionAsync(context, validatorContext),
|
||||
() => ValidateSsoAsync(context, request, validatorContext),
|
||||
() => ValidateTwoFactorAsync(context, request, validatorContext),
|
||||
@@ -426,17 +285,22 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
if (validatorContext.TwoFactorRequired &&
|
||||
validatorContext.TwoFactorRecoveryRequested)
|
||||
{
|
||||
SetSsoResult(context, new Dictionary<string, object>
|
||||
{
|
||||
{ "ErrorModel", new ErrorResponseModel("Two-factor recovery has been performed. SSO authentication is required.") }
|
||||
});
|
||||
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.") }
|
||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -717,7 +581,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
/// <param name="user">user trying to login</param>
|
||||
/// <param name="grantType">magic string identifying the grant type requested</param>
|
||||
/// <returns>true if sso required; false if not required or already in process</returns>
|
||||
[Obsolete("This method is deprecated and will be removed in future versions, PM-28281. Please use the SsoRequestValidator scheme instead.")]
|
||||
[Obsolete(
|
||||
"This method is deprecated and will be removed in future versions, PM-28281. Please use the SsoRequestValidator scheme instead.")]
|
||||
private async Task<bool> RequireSsoLoginAsync(User user, string grantType)
|
||||
{
|
||||
if (grantType == "authorization_code" || grantType == "client_credentials")
|
||||
|
||||
@@ -48,8 +48,6 @@ public class SsoRequestValidator(
|
||||
// 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 the feature flag RecoveryCodeSupportForSsoRequiredUsers is set to false then this code is unreachable since
|
||||
// Two Factor validation occurs after SSO validation in that scenario.
|
||||
if (context.TwoFactorRequired && context.TwoFactorRecoveryRequested)
|
||||
{
|
||||
await SetContextCustomResponseSsoErrorAsync(context, SsoConstants.RequestErrors.SsoTwoFactorRecoveryDescription);
|
||||
@@ -63,10 +61,10 @@ public class SsoRequestValidator(
|
||||
/// <summary>
|
||||
/// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are
|
||||
/// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement.
|
||||
/// If the GrantType is authorization_code or client_credentials we know the user is trying to login
|
||||
/// If the GrantType is authorization_code or client_credentials we know the user is trying to log in
|
||||
/// using the SSO flow so they are allowed to continue.
|
||||
/// </summary>
|
||||
/// <param name="user">user trying to login</param>
|
||||
/// <param name="user">user trying to log in</param>
|
||||
/// <param name="grantType">magic string identifying the grant type requested</param>
|
||||
/// <returns>true if sso required; false if not required or already in process</returns>
|
||||
private async Task<bool> RequireSsoAuthenticationAsync(User user, string grantType)
|
||||
|
||||
Reference in New Issue
Block a user