mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +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.DeviceTrust;
|
||||||
using Bit.Core.Auth.UserFeatures.Registration;
|
using Bit.Core.Auth.UserFeatures.Registration;
|
||||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||||
@@ -29,6 +28,7 @@ public static class UserServiceCollectionExtensions
|
|||||||
services.AddWebAuthnLoginCommands();
|
services.AddWebAuthnLoginCommands();
|
||||||
services.AddTdeOffboardingPasswordCommands();
|
services.AddTdeOffboardingPasswordCommands();
|
||||||
services.AddTwoFactorQueries();
|
services.AddTwoFactorQueries();
|
||||||
|
services.AddSsoQueries();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddDeviceTrustCommands(this IServiceCollection services)
|
public static void AddDeviceTrustCommands(this IServiceCollection services)
|
||||||
@@ -69,4 +69,9 @@ public static class UserServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddScoped<ITwoFactorIsEnabledQuery, TwoFactorIsEnabledQuery>();
|
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 MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
||||||
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
||||||
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
||||||
|
public const string RedirectOnSsoRequired = "pm-1632-redirect-on-sso-required";
|
||||||
|
|
||||||
/* Autofill Team */
|
/* Autofill Team */
|
||||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
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 IEventService _eventService;
|
||||||
private readonly IDeviceValidator _deviceValidator;
|
private readonly IDeviceValidator _deviceValidator;
|
||||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||||
|
private readonly ISsoRequestValidator _ssoRequestValidator;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
@@ -43,7 +44,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
|
|
||||||
protected ICurrentContext CurrentContext { get; }
|
protected ICurrentContext CurrentContext { get; }
|
||||||
protected IPolicyService PolicyService { get; }
|
protected IPolicyService PolicyService { get; }
|
||||||
protected IFeatureService FeatureService { get; }
|
protected IFeatureService _featureService { get; }
|
||||||
protected ISsoConfigRepository SsoConfigRepository { get; }
|
protected ISsoConfigRepository SsoConfigRepository { get; }
|
||||||
protected IUserService _userService { get; }
|
protected IUserService _userService { get; }
|
||||||
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
||||||
@@ -56,6 +57,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -76,13 +78,14 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_deviceValidator = deviceValidator;
|
_deviceValidator = deviceValidator;
|
||||||
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
_twoFactorAuthenticationValidator = twoFactorAuthenticationValidator;
|
||||||
|
_ssoRequestValidator = ssoRequestValidator;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
CurrentContext = currentContext;
|
CurrentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
PolicyService = policyService;
|
PolicyService = policyService;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
FeatureService = featureService;
|
_featureService = featureService;
|
||||||
SsoConfigRepository = ssoConfigRepository;
|
SsoConfigRepository = ssoConfigRepository;
|
||||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||||
PolicyRequirementQuery = policyRequirementQuery;
|
PolicyRequirementQuery = policyRequirementQuery;
|
||||||
@@ -94,7 +97,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||||
CustomValidatorRequestContext validatorContext)
|
CustomValidatorRequestContext validatorContext)
|
||||||
{
|
{
|
||||||
if (FeatureService.IsEnabled(FeatureFlagKeys.RecoveryCodeSupportForSsoRequiredUsers))
|
if (_featureService.IsEnabled(FeatureFlagKeys.RecoveryCodeSupportForSsoRequiredUsers))
|
||||||
{
|
{
|
||||||
var validators = DetermineValidationOrder(context, request, validatorContext);
|
var validators = DetermineValidationOrder(context, request, validatorContext);
|
||||||
var allValidationSchemesSuccessful = await ProcessValidatorsAsync(validators);
|
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.
|
// 2. Decide if this user belongs to an organization that requires SSO.
|
||||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
|
// TODO: Clean up Feature Flag: Remove this if block: PM-28281
|
||||||
if (validatorContext.SsoRequired)
|
if (!_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired))
|
||||||
{
|
{
|
||||||
SetSsoResult(context,
|
validatorContext.SsoRequired = await RequireSsoLoginAsync(user, request.GrantType);
|
||||||
new Dictionary<string, object>
|
if (validatorContext.SsoRequired)
|
||||||
{
|
{
|
||||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
SetSsoResult(context,
|
||||||
});
|
new Dictionary<string, object>
|
||||||
return;
|
{
|
||||||
|
{ "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.
|
// 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,
|
private async Task<bool> ValidateSsoAsync(T context, ValidatedTokenRequest request,
|
||||||
CustomValidatorRequestContext validatorContext)
|
CustomValidatorRequestContext validatorContext)
|
||||||
{
|
{
|
||||||
validatorContext.SsoRequired = await RequireSsoLoginAsync(validatorContext.User, request.GrantType);
|
// TODO: Clean up Feature Flag: Remove this if block: PM-28281
|
||||||
if (!validatorContext.SsoRequired)
|
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
|
// 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
|
// 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.
|
// review their new recovery token if desired.
|
||||||
// SSO users cannot be assumed to be authenticated, and must prove authentication with their IdP after recovery.
|
// 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
|
// As described in validation order determination, if TwoFactorRequired, the 2FA validation scheme will have been
|
||||||
// evaluated, and recovery will have been performed if requested.
|
// 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
|
// We will send a descriptive message in these cases so clients can give the appropriate feedback and redirect
|
||||||
// to /login.
|
// to /login.
|
||||||
if (validatorContext.TwoFactorRequired &&
|
if (validatorContext.TwoFactorRequired &&
|
||||||
validatorContext.TwoFactorRecoveryRequested)
|
validatorContext.TwoFactorRecoveryRequested)
|
||||||
{
|
{
|
||||||
SetSsoResult(context, new Dictionary<string, object>
|
SetSsoResult(context, new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "ErrorModel", new ErrorResponseModel("Two-factor recovery has been performed. SSO authentication is required.") }
|
{ "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;
|
return false;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
SetSsoResult(context,
|
{
|
||||||
new Dictionary<string, object>
|
var ssoValid = await _ssoRequestValidator.ValidateAsync(validatorContext.User, request, validatorContext);
|
||||||
|
if (ssoValid)
|
||||||
{
|
{
|
||||||
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
return true;
|
||||||
});
|
}
|
||||||
return false;
|
|
||||||
|
SetValidationErrorResult(context, validatorContext);
|
||||||
|
return ssoValid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -651,6 +683,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
/// <param name="user">user trying to login</param>
|
/// <param name="user">user trying to login</param>
|
||||||
/// <param name="grantType">magic string identifying the grant type requested</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>
|
/// <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)
|
private async Task<bool> RequireSsoLoginAsync(User user, string grantType)
|
||||||
{
|
{
|
||||||
if (grantType == "authorization_code" || grantType == "client_credentials")
|
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
|
// 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))
|
? (await PolicyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(user.Id))
|
||||||
.SsoRequired
|
.SsoRequired
|
||||||
: await PolicyService.AnyPoliciesApplicableToUserAsync(
|
: await PolicyService.AnyPoliciesApplicableToUserAsync(
|
||||||
@@ -703,7 +736,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
|||||||
|
|
||||||
private async Task SendFailedTwoFactorEmail(User user, TwoFactorProviderType failedAttemptType)
|
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,
|
await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemptType, DateTime.UtcNow,
|
||||||
CurrentContext.IpAddress);
|
CurrentContext.IpAddress);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger<CustomTokenRequestValidator> logger,
|
ILogger<CustomTokenRequestValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -56,6 +57,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
|||||||
eventService,
|
eventService,
|
||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
|
ssoRequestValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
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,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger<ResourceOwnerPasswordValidator> logger,
|
ILogger<ResourceOwnerPasswordValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -50,6 +51,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
|||||||
eventService,
|
eventService,
|
||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
|
ssoRequestValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
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,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger<CustomTokenRequestValidator> logger,
|
ILogger<CustomTokenRequestValidator> logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -59,6 +60,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
|||||||
eventService,
|
eventService,
|
||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
|
ssoRequestValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
||||||
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
||||||
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
|
services.AddTransient<ITwoFactorAuthenticationValidator, TwoFactorAuthenticationValidator>();
|
||||||
|
services.AddTransient<ISsoRequestValidator, SsoRequestValidator>();
|
||||||
services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();
|
services.AddTransient<ILoginApprovingClientTypes, LoginApprovingClientTypes>();
|
||||||
services.AddTransient<ISendAuthenticationMethodValidator<ResourcePassword>, SendPasswordRequestValidator>();
|
services.AddTransient<ISendAuthenticationMethodValidator<ResourcePassword>, SendPasswordRequestValidator>();
|
||||||
services.AddTransient<ISendAuthenticationMethodValidator<EmailOtp>, SendEmailOtpRequestValidator>();
|
services.AddTransient<ISendAuthenticationMethodValidator<EmailOtp>, SendEmailOtpRequestValidator>();
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Auth.Sso;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Auth.UserFeatures.Sso;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class UserSsoOrganizationIdentifierQueryTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasSingleConfirmedOrganization_ReturnsIdentifier(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.OrganizationId = organization.Id;
|
||||||
|
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
organization.Identifier = "test-org-identifier";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns([organizationUser]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("test-org-identifier", result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organization.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasNoOrganizations_ReturnsNull(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns(Array.Empty<OrganizationUser>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasMultipleConfirmedOrganizations_ReturnsNull(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUser organizationUser1,
|
||||||
|
OrganizationUser organizationUser2)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser1.UserId = userId;
|
||||||
|
organizationUser1.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
organizationUser2.UserId = userId;
|
||||||
|
organizationUser2.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns([organizationUser1, organizationUser2]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasOnlyInvitedOrganization_ReturnsNull(
|
||||||
|
OrganizationUserStatusType status,
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.Status = status;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns([organizationUser]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.GetByIdAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_UserHasMixedStatusOrganizations_OnlyOneConfirmed_ReturnsIdentifier(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser confirmedOrgUser,
|
||||||
|
OrganizationUser invitedOrgUser,
|
||||||
|
OrganizationUser revokedOrgUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
confirmedOrgUser.UserId = userId;
|
||||||
|
confirmedOrgUser.OrganizationId = organization.Id;
|
||||||
|
confirmedOrgUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
|
||||||
|
invitedOrgUser.UserId = userId;
|
||||||
|
invitedOrgUser.Status = OrganizationUserStatusType.Invited;
|
||||||
|
|
||||||
|
revokedOrgUser.UserId = userId;
|
||||||
|
revokedOrgUser.Status = OrganizationUserStatusType.Revoked;
|
||||||
|
|
||||||
|
organization.Identifier = "mixed-status-org";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns(new[] { confirmedOrgUser, invitedOrgUser, revokedOrgUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("mixed-status-org", result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organization.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_OrganizationNotFound_ReturnsNull(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns([organizationUser]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organizationUser.OrganizationId)
|
||||||
|
.Returns((Organization)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organizationUser.OrganizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_OrganizationIdentifierIsNull_ReturnsNull(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.OrganizationId = organization.Id;
|
||||||
|
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
organization.Identifier = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns(new[] { organizationUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organization.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetSsoOrganizationIdentifierAsync_OrganizationIdentifierIsEmpty_ReturnsEmpty(
|
||||||
|
SutProvider<UserSsoOrganizationIdentifierQuery> sutProvider,
|
||||||
|
Guid userId,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
organizationUser.UserId = userId;
|
||||||
|
organizationUser.OrganizationId = organization.Id;
|
||||||
|
organizationUser.Status = OrganizationUserStatusType.Confirmed;
|
||||||
|
organization.Identifier = string.Empty;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(userId)
|
||||||
|
.Returns(new[] { organizationUser });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns(organization);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.GetSsoOrganizationIdentifierAsync(userId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(string.Empty, result);
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetManyByUserAsync(userId);
|
||||||
|
await sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.GetByIdAsync(organization.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,14 +44,17 @@ internal class CustomValidatorRequestContextCustomization : ICustomization
|
|||||||
/// <see cref="CustomValidatorRequestContext.TwoFactorRecoveryRequested"/>, and
|
/// <see cref="CustomValidatorRequestContext.TwoFactorRecoveryRequested"/>, and
|
||||||
/// <see cref="CustomValidatorRequestContext.SsoRequired" /> should initialize false,
|
/// <see cref="CustomValidatorRequestContext.SsoRequired" /> should initialize false,
|
||||||
/// and are made truthy in context upon evaluation of a request. Do not allow AutoFixture to eagerly make these
|
/// and are made truthy in context upon evaluation of a request. Do not allow AutoFixture to eagerly make these
|
||||||
/// truthy; that is the responsibility of the <see cref="Bit.Identity.IdentityServer.RequestValidators.BaseRequestValidator{T}" />
|
/// truthy; that is the responsibility of the <see cref="Bit.Identity.IdentityServer.RequestValidators.BaseRequestValidator{T}" />.
|
||||||
|
/// ValidationErrorResult and CustomResponse should also be null initially; they are hydrated during the validation process.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Customize(IFixture fixture)
|
public void Customize(IFixture fixture)
|
||||||
{
|
{
|
||||||
fixture.Customize<CustomValidatorRequestContext>(composer => composer
|
fixture.Customize<CustomValidatorRequestContext>(composer => composer
|
||||||
.With(o => o.RememberMeRequested, false)
|
.With(o => o.RememberMeRequested, false)
|
||||||
.With(o => o.TwoFactorRecoveryRequested, false)
|
.With(o => o.TwoFactorRecoveryRequested, false)
|
||||||
.With(o => o.SsoRequired, false));
|
.With(o => o.SsoRequired, false)
|
||||||
|
.With(o => o.ValidationErrorResult, () => null)
|
||||||
|
.With(o => o.CustomResponse, () => null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ using Bit.Identity.IdentityServer;
|
|||||||
using Bit.Identity.IdentityServer.RequestValidators;
|
using Bit.Identity.IdentityServer.RequestValidators;
|
||||||
using Bit.Identity.Test.Wrappers;
|
using Bit.Identity.Test.Wrappers;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Duende.IdentityModel;
|
||||||
using Duende.IdentityServer.Validation;
|
using Duende.IdentityServer.Validation;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -42,6 +43,7 @@ public class BaseRequestValidatorTests
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IDeviceValidator _deviceValidator;
|
private readonly IDeviceValidator _deviceValidator;
|
||||||
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator;
|
||||||
|
private readonly ISsoRequestValidator _ssoRequestValidator;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly FakeLogger<BaseRequestValidatorTests> _logger;
|
private readonly FakeLogger<BaseRequestValidatorTests> _logger;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
@@ -65,6 +67,7 @@ public class BaseRequestValidatorTests
|
|||||||
_eventService = Substitute.For<IEventService>();
|
_eventService = Substitute.For<IEventService>();
|
||||||
_deviceValidator = Substitute.For<IDeviceValidator>();
|
_deviceValidator = Substitute.For<IDeviceValidator>();
|
||||||
_twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
|
_twoFactorAuthenticationValidator = Substitute.For<ITwoFactorAuthenticationValidator>();
|
||||||
|
_ssoRequestValidator = Substitute.For<ISsoRequestValidator>();
|
||||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
_logger = new FakeLogger<BaseRequestValidatorTests>();
|
_logger = new FakeLogger<BaseRequestValidatorTests>();
|
||||||
_currentContext = Substitute.For<ICurrentContext>();
|
_currentContext = Substitute.For<ICurrentContext>();
|
||||||
@@ -85,6 +88,7 @@ public class BaseRequestValidatorTests
|
|||||||
_eventService,
|
_eventService,
|
||||||
_deviceValidator,
|
_deviceValidator,
|
||||||
_twoFactorAuthenticationValidator,
|
_twoFactorAuthenticationValidator,
|
||||||
|
_ssoRequestValidator,
|
||||||
_organizationUserRepository,
|
_organizationUserRepository,
|
||||||
_logger,
|
_logger,
|
||||||
_currentContext,
|
_currentContext,
|
||||||
@@ -151,6 +155,7 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
@@ -162,9 +167,9 @@ public class BaseRequestValidatorTests
|
|||||||
|
|
||||||
// 4 -> set up device validator to fail
|
// 4 -> set up device validator to fail
|
||||||
requestContext.KnownDevice = false;
|
requestContext.KnownDevice = false;
|
||||||
tokenRequest.GrantType = "password";
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
_deviceValidator
|
_deviceValidator
|
||||||
.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
|
.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
.Returns(Task.FromResult(false));
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
// 5 -> not legacy user
|
// 5 -> not legacy user
|
||||||
@@ -192,6 +197,7 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
@@ -203,12 +209,13 @@ public class BaseRequestValidatorTests
|
|||||||
|
|
||||||
// 4 -> set up device validator to pass
|
// 4 -> set up device validator to pass
|
||||||
_deviceValidator
|
_deviceValidator
|
||||||
.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
|
.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
.Returns(Task.FromResult(true));
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
// 5 -> not legacy user
|
// 5 -> not legacy user
|
||||||
_userService.IsLegacyUser(Arg.Any<string>())
|
_userService.IsLegacyUser(Arg.Any<string>())
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
{
|
{
|
||||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
@@ -236,6 +243,7 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
@@ -262,12 +270,13 @@ public class BaseRequestValidatorTests
|
|||||||
|
|
||||||
// 4 -> set up device validator to pass
|
// 4 -> set up device validator to pass
|
||||||
_deviceValidator
|
_deviceValidator
|
||||||
.ValidateRequestDeviceAsync(Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>())
|
.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
.Returns(Task.FromResult(true));
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
// 5 -> not legacy user
|
// 5 -> not legacy user
|
||||||
_userService.IsLegacyUser(Arg.Any<string>())
|
_userService.IsLegacyUser(Arg.Any<string>())
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
{
|
{
|
||||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
@@ -299,6 +308,7 @@ public class BaseRequestValidatorTests
|
|||||||
// Arrange
|
// Arrange
|
||||||
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(featureFlagValue);
|
||||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
// 1 -> to pass
|
// 1 -> to pass
|
||||||
_sut.isValid = true;
|
_sut.isValid = true;
|
||||||
|
|
||||||
@@ -319,10 +329,19 @@ public class BaseRequestValidatorTests
|
|||||||
|
|
||||||
// 2 -> will result to false with no extra configuration
|
// 2 -> will result to false with no extra configuration
|
||||||
// 3 -> set two factor to be required
|
// 3 -> set two factor to be required
|
||||||
|
requestContext.User.TwoFactorProviders = "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}";
|
||||||
_twoFactorAuthenticationValidator
|
_twoFactorAuthenticationValidator
|
||||||
.RequiresTwoFactorAsync(Arg.Any<User>(), tokenRequest)
|
.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||||
|
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.BuildTwoFactorResultAsync(requestContext.User, null)
|
||||||
|
.Returns(Task.FromResult(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "TwoFactorProviders", new[] { "0", "1" } },
|
||||||
|
{ "TwoFactorProviders2", new Dictionary<string, object>{{"Email", null}} }
|
||||||
|
}));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await _sut.ValidateAsync(context);
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
@@ -330,7 +349,10 @@ public class BaseRequestValidatorTests
|
|||||||
Assert.True(context.GrantResult.IsError);
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
|
||||||
// Assert that the auth request was NOT consumed
|
// Assert that the auth request was NOT consumed
|
||||||
await _authRequestRepository.DidNotReceive().ReplaceAsync(Arg.Any<AuthRequest>());
|
await _authRequestRepository.DidNotReceive().ReplaceAsync(authRequest);
|
||||||
|
|
||||||
|
// Assert that the error is for 2fa
|
||||||
|
Assert.Equal("Two-factor authentication required.", context.GrantResult.ErrorDescription);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -420,6 +442,7 @@ public class BaseRequestValidatorTests
|
|||||||
{ "TwoFactorProviders", new[] { "0", "1" } },
|
{ "TwoFactorProviders", new[] { "0", "1" } },
|
||||||
{ "TwoFactorProviders2", new Dictionary<string, object>() }
|
{ "TwoFactorProviders2", new Dictionary<string, object>() }
|
||||||
};
|
};
|
||||||
|
|
||||||
_twoFactorAuthenticationValidator
|
_twoFactorAuthenticationValidator
|
||||||
.BuildTwoFactorResultAsync(user, null)
|
.BuildTwoFactorResultAsync(user, null)
|
||||||
.Returns(Task.FromResult(twoFactorResultDict));
|
.Returns(Task.FromResult(twoFactorResultDict));
|
||||||
@@ -428,6 +451,8 @@ public class BaseRequestValidatorTests
|
|||||||
await _sut.ValidateAsync(context);
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
Assert.Equal("Two-factor authentication required.", context.GrantResult.ErrorDescription);
|
||||||
|
|
||||||
// Verify that the failed 2FA email was NOT sent for remember token expiration
|
// Verify that the failed 2FA email was NOT sent for remember token expiration
|
||||||
await _mailService.DidNotReceive()
|
await _mailService.DidNotReceive()
|
||||||
.SendFailedTwoFactorAttemptEmailAsync(Arg.Any<string>(), Arg.Any<TwoFactorProviderType>(),
|
.SendFailedTwoFactorAttemptEmailAsync(Arg.Any<string>(), Arg.Any<TwoFactorProviderType>(),
|
||||||
@@ -1243,6 +1268,343 @@ public class BaseRequestValidatorTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is DISABLED, the legacy SSO validation path is used.
|
||||||
|
/// This validates the deprecated RequireSsoLoginAsync method is called and SSO requirement
|
||||||
|
/// is checked using the old PolicyService.AnyPoliciesApplicableToUserAsync approach.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(true)]
|
||||||
|
[BitAutoData(false)]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Disabled_UsesLegacySsoValidation(
|
||||||
|
bool recoveryCodeFeatureEnabled,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(false);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
// SSO is required via legacy path
|
||||||
|
_policyService.AnyPoliciesApplicableToUserAsync(
|
||||||
|
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||||
|
Assert.Equal("SSO authentication is required.", errorResponse.Message);
|
||||||
|
|
||||||
|
// Verify legacy path was used
|
||||||
|
await _policyService.Received(1).AnyPoliciesApplicableToUserAsync(
|
||||||
|
requestContext.User.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||||
|
|
||||||
|
// Verify new SsoRequestValidator was NOT called
|
||||||
|
await _ssoRequestValidator.DidNotReceive().ValidateAsync(
|
||||||
|
Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>(), Arg.Any<CustomValidatorRequestContext>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is ENABLED, the new ISsoRequestValidator is used
|
||||||
|
/// instead of the legacy RequireSsoLoginAsync method.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(true)]
|
||||||
|
[BitAutoData(false)]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_UsesNewSsoRequestValidator(
|
||||||
|
bool recoveryCodeFeatureEnabled,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
// Configure SsoRequestValidator to indicate SSO is required
|
||||||
|
_ssoRequestValidator.ValidateAsync(
|
||||||
|
Arg.Any<User>(),
|
||||||
|
Arg.Any<ValidatedTokenRequest>(),
|
||||||
|
Arg.Any<CustomValidatorRequestContext>())
|
||||||
|
.Returns(Task.FromResult(false)); // false = SSO required
|
||||||
|
|
||||||
|
// Set up the ValidationErrorResult that SsoRequestValidator would set
|
||||||
|
requestContext.ValidationErrorResult = new ValidationResult
|
||||||
|
{
|
||||||
|
IsError = true,
|
||||||
|
Error = "sso_required",
|
||||||
|
ErrorDescription = "SSO authentication is required."
|
||||||
|
};
|
||||||
|
requestContext.CustomResponse = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
|
||||||
|
// Verify new SsoRequestValidator was called
|
||||||
|
await _ssoRequestValidator.Received(1).ValidateAsync(
|
||||||
|
requestContext.User,
|
||||||
|
tokenRequest,
|
||||||
|
requestContext);
|
||||||
|
|
||||||
|
// Verify legacy path was NOT used
|
||||||
|
await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync(
|
||||||
|
Arg.Any<Guid>(), Arg.Any<PolicyType>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is ENABLED and SSO is NOT required,
|
||||||
|
/// authentication continues successfully through the new validation path.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(true)]
|
||||||
|
[BitAutoData(false)]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_SsoNotRequired_SuccessfulLogin(
|
||||||
|
bool recoveryCodeFeatureEnabled,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
tokenRequest.ClientId = "web";
|
||||||
|
|
||||||
|
// SsoRequestValidator returns true (SSO not required)
|
||||||
|
_ssoRequestValidator.ValidateAsync(
|
||||||
|
Arg.Any<User>(),
|
||||||
|
Arg.Any<ValidatedTokenRequest>(),
|
||||||
|
Arg.Any<CustomValidatorRequestContext>())
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// No 2FA required
|
||||||
|
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||||
|
|
||||||
|
// Device validation passes
|
||||||
|
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// User is not legacy
|
||||||
|
_userService.IsLegacyUser(Arg.Any<string>()).Returns(false);
|
||||||
|
|
||||||
|
_userAccountKeysQuery.Run(Arg.Any<User>()).Returns(new UserAccountKeysData
|
||||||
|
{
|
||||||
|
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
|
||||||
|
"test-private-key",
|
||||||
|
"test-public-key"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(context.GrantResult.IsError);
|
||||||
|
await _eventService.Received(1).LogUserEventAsync(requestContext.User.Id, EventType.User_LoggedIn);
|
||||||
|
|
||||||
|
// Verify new validator was used
|
||||||
|
await _ssoRequestValidator.Received(1).ValidateAsync(
|
||||||
|
requestContext.User,
|
||||||
|
tokenRequest,
|
||||||
|
requestContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is ENABLED and SSO validation returns a custom response
|
||||||
|
/// (e.g., with organization identifier), that custom response is properly propagated to the result.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(true)]
|
||||||
|
[BitAutoData(false)]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_PropagatesCustomResponse(
|
||||||
|
bool recoveryCodeFeatureEnabled,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(recoveryCodeFeatureEnabled);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
tokenRequest.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
// SsoRequestValidator sets custom response with organization identifier
|
||||||
|
requestContext.ValidationErrorResult = new ValidationResult
|
||||||
|
{
|
||||||
|
IsError = true,
|
||||||
|
Error = "sso_required",
|
||||||
|
ErrorDescription = "SSO authentication is required."
|
||||||
|
};
|
||||||
|
requestContext.CustomResponse = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "ErrorModel", new ErrorResponseModel("SSO authentication is required.") },
|
||||||
|
{ "SsoOrganizationIdentifier", "test-org-identifier" }
|
||||||
|
};
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
|
||||||
|
_ssoRequestValidator.ValidateAsync(
|
||||||
|
Arg.Any<User>(),
|
||||||
|
Arg.Any<ValidatedTokenRequest>(),
|
||||||
|
Arg.Any<CustomValidatorRequestContext>())
|
||||||
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
Assert.NotNull(context.GrantResult.CustomResponse);
|
||||||
|
Assert.Contains("SsoOrganizationIdentifier", context.CustomValidatorRequestContext.CustomResponse);
|
||||||
|
Assert.Equal("test-org-identifier", context.CustomValidatorRequestContext.CustomResponse["SsoOrganizationIdentifier"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is DISABLED and a user with 2FA recovery completes recovery,
|
||||||
|
/// but SSO is required, the legacy error message is returned (without the recovery-specific message).
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Disabled_RecoveryWithSso_LegacyMessage(
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(true);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(false);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
// Recovery code scenario
|
||||||
|
tokenRequest.Raw["TwoFactorProvider"] = ((int)TwoFactorProviderType.RecoveryCode).ToString();
|
||||||
|
tokenRequest.Raw["TwoFactorToken"] = "valid-recovery-code";
|
||||||
|
|
||||||
|
// 2FA with recovery
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||||
|
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.VerifyTwoFactorAsync(requestContext.User, null, TwoFactorProviderType.RecoveryCode, "valid-recovery-code")
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// SSO is required (legacy check)
|
||||||
|
_policyService.AnyPoliciesApplicableToUserAsync(
|
||||||
|
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||||
|
|
||||||
|
// Legacy behavior: recovery-specific message IS shown even without RedirectOnSsoRequired
|
||||||
|
Assert.Equal("Two-factor recovery has been performed. SSO authentication is required.", errorResponse.Message);
|
||||||
|
|
||||||
|
// But legacy validation path was used
|
||||||
|
await _policyService.Received(1).AnyPoliciesApplicableToUserAsync(
|
||||||
|
requestContext.User.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that when RedirectOnSsoRequired is ENABLED and recovery code is used for SSO-required user,
|
||||||
|
/// the SsoRequestValidator provides the recovery-specific error message.
|
||||||
|
/// </summary>
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task ValidateAsync_RedirectOnSsoRequired_Enabled_RecoveryWithSso_NewValidatorMessage(
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext,
|
||||||
|
GrantValidationResult grantResult)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
SetupRecoveryCodeSupportForSsoRequiredUsersFeatureFlag(true);
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.RedirectOnSsoRequired).Returns(true);
|
||||||
|
|
||||||
|
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||||
|
_sut.isValid = true;
|
||||||
|
|
||||||
|
// Recovery code scenario
|
||||||
|
tokenRequest.Raw["TwoFactorProvider"] = ((int)TwoFactorProviderType.RecoveryCode).ToString();
|
||||||
|
tokenRequest.Raw["TwoFactorToken"] = "valid-recovery-code";
|
||||||
|
|
||||||
|
// 2FA with recovery
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||||
|
.Returns(Task.FromResult(new Tuple<bool, Organization>(true, null)));
|
||||||
|
|
||||||
|
_twoFactorAuthenticationValidator
|
||||||
|
.VerifyTwoFactorAsync(requestContext.User, null, TwoFactorProviderType.RecoveryCode, "valid-recovery-code")
|
||||||
|
.Returns(Task.FromResult(true));
|
||||||
|
|
||||||
|
// SsoRequestValidator handles the recovery + SSO scenario
|
||||||
|
requestContext.TwoFactorRecoveryRequested = true;
|
||||||
|
requestContext.ValidationErrorResult = new ValidationResult
|
||||||
|
{
|
||||||
|
IsError = true,
|
||||||
|
Error = "sso_required",
|
||||||
|
ErrorDescription = "Two-factor recovery has been performed. SSO authentication is required."
|
||||||
|
};
|
||||||
|
requestContext.CustomResponse = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "ErrorModel", new ErrorResponseModel("Two-factor recovery has been performed. SSO authentication is required.") }
|
||||||
|
};
|
||||||
|
|
||||||
|
_ssoRequestValidator.ValidateAsync(
|
||||||
|
Arg.Any<User>(),
|
||||||
|
Arg.Any<ValidatedTokenRequest>(),
|
||||||
|
Arg.Any<CustomValidatorRequestContext>())
|
||||||
|
.Returns(Task.FromResult(false));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sut.ValidateAsync(context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(context.GrantResult.IsError);
|
||||||
|
var errorResponse = (ErrorResponseModel)context.CustomValidatorRequestContext.CustomResponse["ErrorModel"];
|
||||||
|
Assert.Equal("Two-factor recovery has been performed. SSO authentication is required.", errorResponse.Message);
|
||||||
|
|
||||||
|
// Verify new validator was used
|
||||||
|
await _ssoRequestValidator.Received(1).ValidateAsync(
|
||||||
|
requestContext.User,
|
||||||
|
tokenRequest,
|
||||||
|
Arg.Is<CustomValidatorRequestContext>(ctx => ctx.TwoFactorRecoveryRequested));
|
||||||
|
|
||||||
|
// Verify legacy path was NOT used
|
||||||
|
await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync(
|
||||||
|
Arg.Any<Guid>(), Arg.Any<PolicyType>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
}
|
||||||
|
|
||||||
private BaseRequestValidationContextFake CreateContext(
|
private BaseRequestValidationContextFake CreateContext(
|
||||||
ValidatedTokenRequest tokenRequest,
|
ValidatedTokenRequest tokenRequest,
|
||||||
CustomValidatorRequestContext requestContext,
|
CustomValidatorRequestContext requestContext,
|
||||||
|
|||||||
469
test/Identity.Test/IdentityServer/SsoRequestValidatorTests.cs
Normal file
469
test/Identity.Test/IdentityServer/SsoRequestValidatorTests.cs
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
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.Services;
|
||||||
|
using Bit.Identity.IdentityServer;
|
||||||
|
using Bit.Identity.IdentityServer.Enums;
|
||||||
|
using Bit.Identity.IdentityServer.RequestValidationConstants;
|
||||||
|
using Bit.Identity.IdentityServer.RequestValidators;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Duende.IdentityModel;
|
||||||
|
using Duende.IdentityServer.Validation;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
using AuthFixtures = Bit.Identity.Test.AutoFixture;
|
||||||
|
|
||||||
|
namespace Bit.Identity.Test.IdentityServer;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class SsoRequestValidatorTests
|
||||||
|
{
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OidcConstants.GrantTypes.AuthorizationCode)]
|
||||||
|
[BitAutoData(OidcConstants.GrantTypes.ClientCredentials)]
|
||||||
|
public async void ValidateAsync_GrantTypeIgnoresSsoRequirement_ReturnsTrue(
|
||||||
|
string grantType,
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = grantType;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired);
|
||||||
|
Assert.Null(context.ValidationErrorResult);
|
||||||
|
Assert.Null(context.CustomResponse);
|
||||||
|
|
||||||
|
// Should not check policies since grant type allows bypass
|
||||||
|
await sutProvider.GetDependency<IPolicyService>().DidNotReceive()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IPolicyRequirementQuery>().DidNotReceive()
|
||||||
|
.GetAsync<RequireSsoPolicyRequirement>(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoNotRequired_RequirementPolicyFeatureFlagEnabled_ReturnsTrue(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = false };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired);
|
||||||
|
Assert.Null(context.ValidationErrorResult);
|
||||||
|
Assert.Null(context.CustomResponse);
|
||||||
|
|
||||||
|
// Should use the new policy requirement query when feature flag is enabled
|
||||||
|
await sutProvider.GetDependency<IPolicyRequirementQuery>().Received(1).GetAsync<RequireSsoPolicyRequirement>(user.Id);
|
||||||
|
await sutProvider.GetDependency<IPolicyService>().DidNotReceive()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(Arg.Any<Guid>(), Arg.Any<PolicyType>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoNotRequired_RequirementPolicyFeatureFlagDisabled_ReturnsTrue(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(
|
||||||
|
user.Id,
|
||||||
|
PolicyType.RequireSso,
|
||||||
|
OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired);
|
||||||
|
Assert.Null(context.ValidationErrorResult);
|
||||||
|
Assert.Null(context.CustomResponse);
|
||||||
|
|
||||||
|
// Should use the legacy policy service when feature flag is disabled
|
||||||
|
await sutProvider.GetDependency<IPolicyService>().Received(1).AnyPoliciesApplicableToUserAsync(
|
||||||
|
user.Id,
|
||||||
|
PolicyType.RequireSso,
|
||||||
|
OrganizationUserStatusType.Confirmed);
|
||||||
|
await sutProvider.GetDependency<IPolicyRequirementQuery>().DidNotReceive()
|
||||||
|
.GetAsync<RequireSsoPolicyRequirement>(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_RequirementPolicyFeatureFlagEnabled_ReturnsFalse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal(SsoConstants.RequestErrors.SsoRequiredDescription, context.ValidationErrorResult.ErrorDescription);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.ErrorModel));
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_RequirementPolicyFeatureFlagDisabled_ReturnsFalse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(
|
||||||
|
user.Id,
|
||||||
|
PolicyType.RequireSso,
|
||||||
|
OrganizationUserStatusType.Confirmed)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal(SsoConstants.RequestErrors.SsoRequiredDescription, context.ValidationErrorResult.ErrorDescription);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey("SsoOrganizationIdentifier"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_TwoFactorRecoveryRequested_ReturnsFalse_WithSpecialMessage(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.TwoFactorRecoveryRequested = true;
|
||||||
|
context.TwoFactorRequired = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal("Two-factor recovery has been performed. SSO authentication is required.",
|
||||||
|
context.ValidationErrorResult.ErrorDescription);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey("SsoOrganizationIdentifier"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_TwoFactorRequiredButNotRecovery_ReturnsFalse_WithStandardMessage(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal(SsoConstants.RequestErrors.SsoRequiredDescription, context.ValidationErrorResult.ErrorDescription);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey("ErrorModel"));
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey("SsoOrganizationIdentifier"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OidcConstants.GrantTypes.Password)]
|
||||||
|
[BitAutoData(OidcConstants.GrantTypes.RefreshToken)]
|
||||||
|
[BitAutoData(CustomGrantTypes.WebAuthn)]
|
||||||
|
public async void ValidateAsync_VariousGrantTypes_SsoRequired_ReturnsFalse(
|
||||||
|
string grantType,
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = grantType;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.ValidationErrorResult);
|
||||||
|
Assert.True(context.ValidationErrorResult.IsError);
|
||||||
|
Assert.Equal(OidcConstants.TokenErrors.InvalidGrant, context.ValidationErrorResult.Error);
|
||||||
|
Assert.Equal(SsoConstants.RequestErrors.SsoRequiredDescription, context.ValidationErrorResult.ErrorDescription);
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_ContextSsoRequiredUpdated_RegardlessOfInitialValue(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.SsoRequired = true; // Start with true to ensure it gets updated
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = false };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired); // Should be updated to false
|
||||||
|
Assert.Null(context.ValidationErrorResult);
|
||||||
|
Assert.Null(context.CustomResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_WithOrganizationIdentifier_IncludesIdentifierInResponse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string orgIdentifier = "test-organization";
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.User = user;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns(orgIdentifier);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.True(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier));
|
||||||
|
Assert.Equal(orgIdentifier, context.CustomResponse[CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier]);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.Received(1)
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_NoOrganizationIdentifier_DoesNotIncludeIdentifierInResponse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.User = user;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns((string)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.Received(1)
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoRequired_EmptyOrganizationIdentifier_DoesNotIncludeIdentifierInResponse(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
context.User = user;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id)
|
||||||
|
.Returns(string.Empty);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.True(context.SsoRequired);
|
||||||
|
Assert.NotNull(context.CustomResponse);
|
||||||
|
Assert.False(context.CustomResponse.ContainsKey(CustomResponseConstants.ResponseKeys.SsoOrganizationIdentifier));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.Received(1)
|
||||||
|
.GetSsoOrganizationIdentifierAsync(user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async void ValidateAsync_SsoNotRequired_DoesNotCallOrganizationIdentifierQuery(
|
||||||
|
User user,
|
||||||
|
[AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext context,
|
||||||
|
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
|
||||||
|
SutProvider<SsoRequestValidator> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
request.GrantType = OidcConstants.GrantTypes.Password;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||||
|
|
||||||
|
var requirement = new RequireSsoPolicyRequirement { SsoRequired = false };
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>().GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||||
|
.Returns(requirement);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.ValidateAsync(user, request, context);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.False(context.SsoRequired);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IUserSsoOrganizationIdentifierQuery>()
|
||||||
|
.DidNotReceive()
|
||||||
|
.GetSsoOrganizationIdentifierAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ public class TwoFactorAuthenticationValidatorTests
|
|||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokenable;
|
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmail2faSessionTokenable;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorenabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorEnabledQuery;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly TwoFactorAuthenticationValidator _sut;
|
private readonly TwoFactorAuthenticationValidator _sut;
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ public class TwoFactorAuthenticationValidatorTests
|
|||||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
_ssoEmail2faSessionTokenable = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
|
_ssoEmail2faSessionTokenable = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
|
||||||
_twoFactorenabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
_twoFactorEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||||
_currentContext = Substitute.For<ICurrentContext>();
|
_currentContext = Substitute.For<ICurrentContext>();
|
||||||
|
|
||||||
_sut = new TwoFactorAuthenticationValidator(
|
_sut = new TwoFactorAuthenticationValidator(
|
||||||
@@ -56,7 +56,7 @@ public class TwoFactorAuthenticationValidatorTests
|
|||||||
_organizationUserRepository,
|
_organizationUserRepository,
|
||||||
_organizationRepository,
|
_organizationRepository,
|
||||||
_ssoEmail2faSessionTokenable,
|
_ssoEmail2faSessionTokenable,
|
||||||
_twoFactorenabledQuery,
|
_twoFactorEnabledQuery,
|
||||||
_currentContext);
|
_currentContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ IBaseRequestValidatorTestWrapper
|
|||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDeviceValidator deviceValidator,
|
IDeviceValidator deviceValidator,
|
||||||
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator,
|
||||||
|
ISsoRequestValidator ssoRequestValidator,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@@ -73,6 +74,7 @@ IBaseRequestValidatorTestWrapper
|
|||||||
eventService,
|
eventService,
|
||||||
deviceValidator,
|
deviceValidator,
|
||||||
twoFactorAuthenticationValidator,
|
twoFactorAuthenticationValidator,
|
||||||
|
ssoRequestValidator,
|
||||||
organizationUserRepository,
|
organizationUserRepository,
|
||||||
logger,
|
logger,
|
||||||
currentContext,
|
currentContext,
|
||||||
@@ -132,12 +134,17 @@ IBaseRequestValidatorTestWrapper
|
|||||||
protected override void SetTwoFactorResult(
|
protected override void SetTwoFactorResult(
|
||||||
BaseRequestValidationContextFake context,
|
BaseRequestValidationContextFake context,
|
||||||
Dictionary<string, object> customResponse)
|
Dictionary<string, object> customResponse)
|
||||||
{ }
|
{
|
||||||
|
context.GrantResult = new GrantValidationResult(
|
||||||
|
TokenRequestErrors.InvalidGrant, "Two-factor authentication required.", customResponse);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void SetValidationErrorResult(
|
protected override void SetValidationErrorResult(
|
||||||
BaseRequestValidationContextFake context,
|
BaseRequestValidationContextFake context,
|
||||||
CustomValidatorRequestContext requestContext)
|
CustomValidatorRequestContext requestContext)
|
||||||
{ }
|
{
|
||||||
|
context.GrantResult.IsError = true;
|
||||||
|
}
|
||||||
|
|
||||||
protected override Task<bool> ValidateContextAsync(
|
protected override Task<bool> ValidateContextAsync(
|
||||||
BaseRequestValidationContextFake context,
|
BaseRequestValidationContextFake context,
|
||||||
|
|||||||
Reference in New Issue
Block a user