mirror of
https://github.com/bitwarden/server
synced 2025-12-24 04:03:25 +00:00
[PM-1632] Redirect on SsoRequired - return SsoOrganizationIdentifier (#6597)
feat: add SSO request validation and organization identifier lookup - Implement SsoRequestValidator to validate SSO requirements - Add UserSsoOrganizationIdentifierQuery to fetch organization identifiers - Create SsoOrganizationIdentifier custom response for SSO redirects - Add feature flag (RedirectOnSsoRequired) for gradual rollout - Register validators and queries in dependency injection - Create RequestValidationConstants to reduce magic strings - Add comprehensive test coverage for validation logic - Update BaseRequestValidator to consume SsoRequestValidator
This commit is contained in:
@@ -34,6 +34,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||
private readonly ISsoRequestValidator _ssoRequestValidator;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ILogger _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
@@ -43,7 +44,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
|
||||
protected ICurrentContext CurrentContext { get; }
|
||||
protected IPolicyService PolicyService { get; }
|
||||
protected IFeatureService FeatureService { get; }
|
||||
protected IFeatureService _featureService { get; }
|
||||
protected ISsoConfigRepository SsoConfigRepository { get; }
|
||||
protected IUserService _userService { get; }
|
||||
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
||||
@@ -56,6 +57,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
ISsoRequestValidator ssoRequestValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ILogger logger,
|
||||
ICurrentContext currentContext,
|
||||
@@ -76,13 +78,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
_eventService = eventService;
|
||||
_deviceValidator = deviceValidator;
|
||||
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
||||
_ssoRequestValidator = ssoRequestValidator;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_logger = logger;
|
||||
CurrentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
PolicyService = policyService;
|
||||
_userRepository = userRepository;
|
||||
FeatureService = featureService;
|
||||
_featureService = featureService;
|
||||
SsoConfigRepository = ssoConfigRepository;
|
||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||
PolicyRequirementQuery = policyRequirementQuery;
|
||||
@@ -94,7 +97,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
if (FeatureService.IsEnabled(FeatureFlagKeys.RecoveryCodeSupportForSsoRequiredUsers))
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeSupportForSsoRequiredUsers))
|
||||
{
|
||||
var validators = DetermineValidationOrder(context, request, validatorContext);
|
||||
var allValidationSchemesSuccessful = await ProcessValidatorsAsync(validators);
|
||||
@@ -120,15 +123,29 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
|
||||
// 2. Decide if this user belongs to an organization that requires SSO.
|
||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
|
||||
if (validatorContext.SsoRequired)
|
||||
// TODO: Clean up Feature Flag: Remove this if block: PM-28281
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired))
|
||||
{
|
||||
SetSsoResult(context,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||
});
|
||||
return;
|
||||
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.
|
||||
@@ -355,36 +372,51 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
private async Task<bool> ValidateSsoAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(validatorContext.User, request.GrantType);
|
||||
if (!validatorContext.SsoRequired)
|
||||
// TODO: Clean up Feature Flag: Remove this if block: PM-28281
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
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>
|
||||
// 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;
|
||||
}
|
||||
|
||||
SetSsoResult(context,
|
||||
new Dictionary<string, object>
|
||||
else
|
||||
{
|
||||
var ssoValid = await _ssoRequestValidator.ValidateAsync(validatorContext.User, request, validatorContext);
|
||||
if (ssoValid)
|
||||
{
|
||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||
});
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
SetValidationErrorResult(context, validatorContext);
|
||||
return ssoValid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -651,6 +683,7 @@ 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.")]
|
||||
private async Task<bool> RequireSsoLoginAsync(User user, string grantType)
|
||||
{
|
||||
if (grantType == "authorization_code" || grantType == "client_credentials")
|
||||
@@ -661,7 +694,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)
|
||||
var ssoRequired = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
? (await PolicyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(user.Id))
|
||||
.SsoRequired
|
||||
: await PolicyService.AnyPoliciesApplicableToUserAsync(
|
||||
@@ -703,7 +736,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
|
||||
private async Task SendFailedTwoFactorEmail(User user, TwoFactorProviderType failedAttemptType)
|
||||
{
|
||||
if (FeatureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail))
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.FailedTwoFactorEmail))
|
||||
{
|
||||
await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemptType, DateTime.UtcNow,
|
||||
CurrentContext.IpAddress);
|
||||
|
||||
Reference in New Issue
Block a user