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; /// /// Validates whether a user is required to authenticate via SSO based on organization policies. /// public class SsoRequestValidator( IPolicyService _policyService, IFeatureService _featureService, IUserSsoOrganizationIdentifierQuery _userSsoOrganizationIdentifierQuery, IPolicyRequirementQuery _policyRequirementQuery) : ISsoRequestValidator { /// /// 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. /// /// The user attempting to authenticate. /// The token request containing grant type and other authentication details. /// The validator context to be updated with SSO requirement status and error results if applicable. /// true if the user can proceed with authentication; false if SSO is required and the user must be redirected to SSO flow. public async Task 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; } /// /// 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. /// /// user trying to login /// magic string identifying the grant type requested /// true if sso required; false if not required or already in process private async Task 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(user.Id)) .SsoRequired : await _policyService.AnyPoliciesApplicableToUserAsync( user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed); if (ssoRequired) { return true; } // Default - SSO is not required return false; } /// /// Sets the customResponse in the context with the error result for the SSO validation failure. /// /// The validator context to update with error details. /// The error message to return to the client. 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 { { CustomResponseConstants.ResponseKeys.ErrorModel, new ErrorResponseModel(errorMessage) } }; // Include organization identifier in the response if available if (!string.IsNullOrEmpty(ssoOrganizationIdentifier)) { context.CustomResponse[CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier] = ssoOrganizationIdentifier; } } }