mirror of
https://github.com/bitwarden/server
synced 2026-01-02 00:23:40 +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:
23
src/Core/Auth/Sso/IUserSsoOrganizationIdentifierQuery.cs
Normal file
23
src/Core/Auth/Sso/IUserSsoOrganizationIdentifierQuery.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Sso;
|
||||
|
||||
/// <summary>
|
||||
/// Query to retrieve the SSO organization identifier that a user is a confirmed member of.
|
||||
/// </summary>
|
||||
public interface IUserSsoOrganizationIdentifierQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the SSO organization identifier for a confirmed organization user.
|
||||
/// If there is more than one organization a User is associated with, we return null. If there are more than one
|
||||
/// organization there is no way to know which organization the user wishes to authenticate with.
|
||||
/// Owners and Admins who are not subject to the SSO required policy cannot utilize this flow, since they may have
|
||||
/// multiple organizations with different SSO configurations.
|
||||
/// </summary>
|
||||
/// <param name="userId">The ID of the <see cref="User"/> to retrieve the SSO organization for. _Not_ an <see cref="OrganizationUser"/>.</param>
|
||||
/// <returns>
|
||||
/// The organization identifier if the user is a confirmed member of an organization with SSO configured,
|
||||
/// otherwise null
|
||||
/// </returns>
|
||||
Task<string?> GetSsoOrganizationIdentifierAsync(Guid userId);
|
||||
}
|
||||
38
src/Core/Auth/Sso/UserSsoOrganizationIdentifierQuery.cs
Normal file
38
src/Core/Auth/Sso/UserSsoOrganizationIdentifierQuery.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Auth.Sso;
|
||||
|
||||
/// <summary>
|
||||
/// TODO : PM-28846 review data structures as they relate to this query
|
||||
/// Query to retrieve the SSO organization identifier that a user is a confirmed member of.
|
||||
/// </summary>
|
||||
public class UserSsoOrganizationIdentifierQuery(
|
||||
IOrganizationUserRepository _organizationUserRepository,
|
||||
IOrganizationRepository _organizationRepository) : IUserSsoOrganizationIdentifierQuery
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<string?> GetSsoOrganizationIdentifierAsync(Guid userId)
|
||||
{
|
||||
// Get all confirmed organization memberships for the user
|
||||
var organizationUsers = await _organizationUserRepository.GetManyByUserAsync(userId);
|
||||
|
||||
// we can only confidently return the correct SsoOrganizationIdentifier if there is exactly one Organization.
|
||||
// The user must also be in the Confirmed status.
|
||||
var confirmedOrgUsers = organizationUsers.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed);
|
||||
if (confirmedOrgUsers.Count() != 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var confirmedOrgUser = confirmedOrgUsers.Single();
|
||||
var organization = await _organizationRepository.GetByIdAsync(confirmedOrgUser.OrganizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return organization.Identifier;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
|
||||
using Bit.Core.Auth.Sso;
|
||||
using Bit.Core.Auth.UserFeatures.DeviceTrust;
|
||||
using Bit.Core.Auth.UserFeatures.Registration;
|
||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
@@ -29,6 +28,7 @@ public static class UserServiceCollectionExtensions
|
||||
services.AddWebAuthnLoginCommands();
|
||||
services.AddTdeOffboardingPasswordCommands();
|
||||
services.AddTwoFactorQueries();
|
||||
services.AddSsoQueries();
|
||||
}
|
||||
|
||||
public static void AddDeviceTrustCommands(this IServiceCollection services)
|
||||
@@ -69,4 +69,9 @@ public static class UserServiceCollectionExtensions
|
||||
{
|
||||
services.AddScoped<ITwoFactorIsEnabledQuery, TwoFactorIsEnabledQuery>();
|
||||
}
|
||||
|
||||
private static void AddSsoQueries(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IUserSsoOrganizationIdentifierQuery, UserSsoOrganizationIdentifierQuery>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ public static class FeatureFlagKeys
|
||||
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
||||
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
||||
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
||||
public const string RedirectOnSsoRequired = "pm-1632-redirect-on-sso-required";
|
||||
|
||||
/* Autofill Team */
|
||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace Bit.Identity.IdentityServer.RequestValidationConstants;
|
||||
|
||||
public static class CustomResponseConstants
|
||||
{
|
||||
public static class ResponseKeys
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies the error model returned in the custom response when an error occurs.
|
||||
/// </summary>
|
||||
public static string ErrorModel => "ErrorModel";
|
||||
/// <summary>
|
||||
/// This Key is used when a user is in a single organization that requires SSO authentication. The identifier
|
||||
/// is used by the client to speed the redirection to the correct IdP for the user's organization.
|
||||
/// </summary>
|
||||
public static string SsoOrganizationIdentifier => "SsoOrganizationIdentifier";
|
||||
}
|
||||
}
|
||||
|
||||
public static class SsoConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// These are messages and errors we return when SSO Validation is unsuccessful
|
||||
/// </summary>
|
||||
public static class RequestErrors
|
||||
{
|
||||
public static string SsoRequired => "sso_required";
|
||||
public static string SsoRequiredDescription => "Sso authentication is required.";
|
||||
public static string SsoTwoFactorRecoveryDescription => "Two-factor recovery has been performed. SSO authentication is required.";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -36,6 +36,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
ISsoRequestValidator ssoRequestValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
@@ -56,6 +57,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
eventService,
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
ssoRequestValidator,
|
||||
organizationUserRepository,
|
||||
logger,
|
||||
currentContext,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using Bit.Core.Entities;
|
||||
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 interface ISsoRequestValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the SSO requirement for a user attempting to authenticate. Sets the error state in the <see cref="CustomValidatorRequestContext.CustomResponse"/> if SSO is required.
|
||||
/// </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>
|
||||
Task<bool> ValidateAsync(User user, ValidatedTokenRequest request, CustomValidatorRequestContext context);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
ISsoRequestValidator ssoRequestValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
@@ -50,6 +51,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
eventService,
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
ssoRequestValidator,
|
||||
organizationUserRepository,
|
||||
logger,
|
||||
currentContext,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||
ISsoRequestValidator ssoRequestValidator,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ILogger<CustomTokenRequestValidator> logger,
|
||||
ICurrentContext currentContext,
|
||||
@@ -59,6 +60,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
eventService,
|
||||
deviceValidator,
|
||||
twoFactorAuthenticationValidator,
|
||||
ssoRequestValidator,
|
||||
organizationUserRepository,
|
||||
logger,
|
||||
currentContext,
|
||||
|
||||
@@ -26,6 +26,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
||||
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
||||
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
|
||||
services.AddTransient<ISsoRequestValidator, SsoRequestValidator>();
|
||||
services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();
|
||||
services.AddTransient<ISendAuthenticationMethodValidator<ResourcePassword>, SendPasswordRequestValidator>();
|
||||
services.AddTransient<ISendAuthenticationMethodValidator<EmailOtp>, SendEmailOtpRequestValidator>();
|
||||
|
||||
Reference in New Issue
Block a user