mirror of
https://github.com/bitwarden/server.git
synced 2026-02-01 06:33:17 +08:00
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
127 lines
6.1 KiB
C#
127 lines
6.1 KiB
C#
using Bit.Core;
|
|
using Bit.Core.AdminConsole.Enums;
|
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
|
using Bit.Core.AdminConsole.Services;
|
|
using Bit.Core.Auth.Sso;
|
|
using Bit.Core.Entities;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.Models.Api;
|
|
using Bit.Core.Services;
|
|
using Bit.Identity.IdentityServer.RequestValidationConstants;
|
|
using Duende.IdentityModel;
|
|
using Duende.IdentityServer.Validation;
|
|
|
|
namespace Bit.Identity.IdentityServer.RequestValidators;
|
|
|
|
/// <summary>
|
|
/// Validates whether a user is required to authenticate via SSO based on organization policies.
|
|
/// </summary>
|
|
public class SsoRequestValidator(
|
|
IPolicyService _policyService,
|
|
IFeatureService _featureService,
|
|
IUserSsoOrganizationIdentifierQuery _userSsoOrganizationIdentifierQuery,
|
|
IPolicyRequirementQuery _policyRequirementQuery) : ISsoRequestValidator
|
|
{
|
|
/// <summary>
|
|
/// Validates the SSO requirement for a user attempting to authenticate.
|
|
/// Sets context.SsoRequired to indicate whether SSO is required.
|
|
/// If SSO is required, sets the validation error result and custom response in the context.
|
|
/// </summary>
|
|
/// <param name="user">The user attempting to authenticate.</param>
|
|
/// <param name="request">The token request containing grant type and other authentication details.</param>
|
|
/// <param name="context">The validator context to be updated with SSO requirement status and error results if applicable.</param>
|
|
/// <returns>true if the user can proceed with authentication; false if SSO is required and the user must be redirected to SSO flow.</returns>
|
|
public async Task<bool> ValidateAsync(User user, ValidatedTokenRequest request, CustomValidatorRequestContext context)
|
|
{
|
|
context.SsoRequired = await RequireSsoAuthenticationAsync(user, request.GrantType);
|
|
|
|
if (!context.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 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);
|
|
return false;
|
|
}
|
|
|
|
await SetContextCustomResponseSsoErrorAsync(context, SsoConstants.RequestErrors.SsoRequiredDescription);
|
|
return false;
|
|
}
|
|
|
|
/// <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
|
|
/// using the SSO flow so they are allowed to continue.
|
|
/// </summary>
|
|
/// <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>
|
|
private async Task<bool> RequireSsoAuthenticationAsync(User user, string grantType)
|
|
{
|
|
if (grantType == OidcConstants.GrantTypes.AuthorizationCode ||
|
|
grantType == OidcConstants.GrantTypes.ClientCredentials)
|
|
{
|
|
// SSO is not required for users already using SSO to authenticate which uses the authorization_code grant type,
|
|
// or logging-in via API key which is the client_credentials grant type.
|
|
// Allow user to continue request validation
|
|
return false;
|
|
}
|
|
|
|
// 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
|
|
: await _policyService.AnyPoliciesApplicableToUserAsync(
|
|
user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
|
|
|
if (ssoRequired)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Default - SSO is not required
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the customResponse in the context with the error result for the SSO validation failure.
|
|
/// </summary>
|
|
/// <param name="context">The validator context to update with error details.</param>
|
|
/// <param name="errorMessage">The error message to return to the client.</param>
|
|
private async Task SetContextCustomResponseSsoErrorAsync(CustomValidatorRequestContext context, string errorMessage)
|
|
{
|
|
var ssoOrganizationIdentifier = await _userSsoOrganizationIdentifierQuery.GetSsoOrganizationIdentifierAsync(context.User.Id);
|
|
|
|
context.ValidationErrorResult = new ValidationResult
|
|
{
|
|
IsError = true,
|
|
Error = OidcConstants.TokenErrors.InvalidGrant,
|
|
ErrorDescription = errorMessage
|
|
};
|
|
|
|
context.CustomResponse = new Dictionary<string, object>
|
|
{
|
|
{ CustomResponseConstants.ResponseKeys.ErrorModel, new ErrorResponseModel(errorMessage) }
|
|
};
|
|
|
|
// Include organization identifier in the response if available
|
|
if (!string.IsNullOrEmpty(ssoOrganizationIdentifier))
|
|
{
|
|
context.CustomResponse[CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier] = ssoOrganizationIdentifier;
|
|
}
|
|
}
|
|
}
|