mirror of
https://github.com/bitwarden/server.git
synced 2025-04-24 06:25:09 -05:00
[PM-18237] Add RequireSsoPolicyRequirement (#5655)
* Add RequireSsoPolicyRequirement and its factory to enforce SSO policies * Enhance WebAuthnController to support RequireSsoPolicyRequirement with feature flag integration. Update tests to validate behavior when SSO policies are applicable. * Integrate IPolicyRequirementQuery into request validators to support RequireSsoPolicyRequirement. Update validation logic to check SSO policies based on feature flag. * Refactor RequireSsoPolicyRequirementFactoryTests to improve test coverage for SSO policies. Add tests for handling both valid and invalid policies in CanUsePasskeyLogin and SsoRequired methods. * Remove ExemptStatuses property from RequireSsoPolicyRequirementFactory to use default values from BasePolicyRequirementFactory * Restore ValidateRequireSsoPolicyDisabledOrNotApplicable * Refactor RequireSsoPolicyRequirement to update CanUsePasskeyLogin and SsoRequired properties to use init-only setters * Refactor RequireSsoPolicyRequirementFactoryTests to enhance test clarity * Refactor BaseRequestValidatorTests to improve test clarity * Refactor WebAuthnController to replace SSO policy validation with PolicyRequirement check * Refactor BaseRequestValidator to replace SSO policy validation with PolicyRequirement check * Refactor WebAuthnControllerTests to update test method names and adjust policy requirement checks * Add tests for AttestationOptions and Post methods in WebAuthnControllerTests to validate scenario where SSO is not required * Refactor RequireSsoPolicyRequirement initialization * Refactor SSO requirement check for improved readability * Rename test methods in RequireSsoPolicyRequirementFactoryTests for clarity on exempt status conditions * Update RequireSsoPolicyRequirement to refine user status checks for SSO policy requirements
This commit is contained in:
parent
9667ecaf9e
commit
722fae81b3
@ -4,6 +4,7 @@ using Bit.Api.Auth.Models.Response.WebAuthn;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
@ -31,6 +32,8 @@ public class WebAuthnController : Controller
|
||||
private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand;
|
||||
private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;
|
||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public WebAuthnController(
|
||||
IUserService userService,
|
||||
@ -41,7 +44,9 @@ public class WebAuthnController : Controller
|
||||
IGetWebAuthnLoginCredentialCreateOptionsCommand getWebAuthnLoginCredentialCreateOptionsCommand,
|
||||
ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand)
|
||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_userService = userService;
|
||||
_policyService = policyService;
|
||||
@ -52,7 +57,8 @@ public class WebAuthnController : Controller
|
||||
_createWebAuthnLoginCredentialCommand = createWebAuthnLoginCredentialCommand;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -68,7 +74,7 @@ public class WebAuthnController : Controller
|
||||
public async Task<WebAuthnCredentialCreateOptionsResponseModel> AttestationOptions([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
var user = await VerifyUserAsync(model);
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||
await ValidateIfUserCanUsePasskeyLogin(user.Id);
|
||||
var options = await _getWebAuthnLoginCredentialCreateOptionsCommand.GetWebAuthnLoginCredentialCreateOptionsAsync(user);
|
||||
|
||||
var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options);
|
||||
@ -101,7 +107,7 @@ public class WebAuthnController : Controller
|
||||
public async Task Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model)
|
||||
{
|
||||
var user = await GetUserAsync();
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||
await ValidateIfUserCanUsePasskeyLogin(user.Id);
|
||||
var tokenable = _createOptionsDataProtector.Unprotect(model.Token);
|
||||
|
||||
if (!tokenable.TokenIsValid(user))
|
||||
@ -126,6 +132,22 @@ public class WebAuthnController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateIfUserCanUsePasskeyLogin(Guid userId)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||
{
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(userId);
|
||||
return;
|
||||
}
|
||||
|
||||
var requireSsoPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(userId);
|
||||
|
||||
if (!requireSsoPolicyRequirement.CanUsePasskeyLogin)
|
||||
{
|
||||
throw new BadRequestException("Passkeys cannot be created for your account. SSO login is required.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut()]
|
||||
public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)
|
||||
{
|
||||
|
@ -0,0 +1,62 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Policy requirements for the Require SSO policy.
|
||||
/// </summary>
|
||||
public class RequireSsoPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the user can use passkey login.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The user can use passkey login if they are not a member (Accepted/Confirmed) of an organization
|
||||
/// that has the Require SSO policy enabled.
|
||||
/// </remarks>
|
||||
public bool CanUsePasskeyLogin { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether SSO requirement is enforced for the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The user is required to login with SSO if they are a confirmed member of an organization
|
||||
/// that has the Require SSO policy enabled.
|
||||
/// </remarks>
|
||||
public bool SsoRequired { get; init; }
|
||||
}
|
||||
|
||||
|
||||
public class RequireSsoPolicyRequirementFactory : BasePolicyRequirementFactory<RequireSsoPolicyRequirement>
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public RequireSsoPolicyRequirementFactory(GlobalSettings globalSettings)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public override PolicyType PolicyType => PolicyType.RequireSso;
|
||||
|
||||
protected override IEnumerable<OrganizationUserType> ExemptRoles =>
|
||||
_globalSettings.Sso.EnforceSsoPolicyForAllUsers
|
||||
? Array.Empty<OrganizationUserType>()
|
||||
: [OrganizationUserType.Owner, OrganizationUserType.Admin];
|
||||
|
||||
public override RequireSsoPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
{
|
||||
var result = new RequireSsoPolicyRequirement
|
||||
{
|
||||
CanUsePasskeyLogin = policyDetails.All(p =>
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Revoked ||
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Invited),
|
||||
|
||||
SsoRequired = policyDetails.Any(p =>
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Confirmed)
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -35,5 +35,6 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@ -39,6 +40,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected ISsoConfigRepository SsoConfigRepository { get; }
|
||||
protected IUserService _userService { get; }
|
||||
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
||||
protected IPolicyRequirementQuery PolicyRequirementQuery { get; }
|
||||
|
||||
public BaseRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
@ -55,7 +57,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userService = userService;
|
||||
@ -72,6 +75,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
FeatureService = featureService;
|
||||
SsoConfigRepository = ssoConfigRepository;
|
||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||
PolicyRequirementQuery = policyRequirementQuery;
|
||||
}
|
||||
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
@ -348,9 +352,12 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
|
||||
// Check if user belongs to any organization with an active SSO policy
|
||||
var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(
|
||||
user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
if (anySsoPoliciesApplicableToUser)
|
||||
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;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@ -43,8 +44,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IUpdateInstallationCommand updateInstallationCommand
|
||||
)
|
||||
IUpdateInstallationCommand updateInstallationCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@ -60,7 +61,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_updateInstallationCommand = updateInstallationCommand;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
@ -40,7 +41,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@ -56,7 +58,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@ -44,7 +45,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||
IFeatureService featureService,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand)
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@ -60,7 +62,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery)
|
||||
{
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
|
@ -1,7 +1,9 @@
|
||||
using Bit.Api.Auth.Controllers;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
@ -80,6 +82,57 @@ public class WebAuthnControllerTests
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AttestationOptions_RequireSsoPolicyNotApplicable_Succeeds(
|
||||
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(false);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Protect(Arg.Any<WebAuthnCredentialCreateOptionsTokenable>()).Returns("token");
|
||||
|
||||
var result = await sutProvider.Sut.AttestationOptions(requestModel);
|
||||
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AttestationOptions_WithPolicyRequirementsEnabled_CanUsePasskeyLoginFalse_ThrowsBadRequestException(
|
||||
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||
.ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = false });
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AttestationOptions(requestModel));
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AttestationOptions_WithPolicyRequirementsEnabled_CanUsePasskeyLoginTrue_Succeeds(
|
||||
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||
.ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = true });
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Protect(Arg.Any<WebAuthnCredentialCreateOptionsTokenable>()).Returns("token");
|
||||
|
||||
var result = await sutProvider.Sut.AttestationOptions(requestModel);
|
||||
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
#region Assertion Options
|
||||
[Theory, BitAutoData]
|
||||
public async Task AssertionOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||
@ -211,6 +264,102 @@ public class WebAuthnControllerTests
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_RequireSsoPolicyNotApplicable_Succeeds(
|
||||
WebAuthnLoginCredentialCreateRequestModel requestModel,
|
||||
CredentialCreateOptions createOptions,
|
||||
User user,
|
||||
SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Unprotect(requestModel.Token)
|
||||
.Returns(token);
|
||||
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.Post(requestModel);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
.GetUserByPrincipalAsync(default);
|
||||
await sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.Received(1)
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_WithPolicyRequirementsEnabled_CanUsePasskeyLoginFalse_ThrowsBadRequestException(
|
||||
WebAuthnLoginCredentialCreateRequestModel requestModel,
|
||||
CredentialCreateOptions createOptions,
|
||||
User user,
|
||||
SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), false)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Unprotect(requestModel.Token)
|
||||
.Returns(token);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||
.ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = false });
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.Post(requestModel));
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_WithPolicyRequirementsEnabled_CanUsePasskeyLoginTrue_Succeeds(
|
||||
WebAuthnLoginCredentialCreateRequestModel requestModel,
|
||||
CredentialCreateOptions createOptions,
|
||||
User user,
|
||||
SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Unprotect(requestModel.Token)
|
||||
.Returns(token);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||
.ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = true });
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.Post(requestModel);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
.GetUserByPrincipalAsync(default);
|
||||
await sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.Received(1)
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
|
@ -0,0 +1,104 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RequireSsoPolicyRequirementFactoryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void CanUsePasskeyLogin_WithNoPolicies_ReturnsTrue(
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.True(actual.CanUsePasskeyLogin);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
||||
public void CanUsePasskeyLogin_WithoutExemptStatus_ReturnsFalse(
|
||||
OrganizationUserStatusType userStatus,
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
PolicyType = PolicyType.RequireSso,
|
||||
OrganizationUserStatus = userStatus
|
||||
}
|
||||
]);
|
||||
|
||||
Assert.False(actual.CanUsePasskeyLogin);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||
public void CanUsePasskeyLogin_WithExemptStatus_ReturnsTrue(
|
||||
OrganizationUserStatusType userStatus,
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
PolicyType = PolicyType.RequireSso,
|
||||
OrganizationUserStatus = userStatus
|
||||
}
|
||||
]);
|
||||
|
||||
Assert.True(actual.CanUsePasskeyLogin);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void SsoRequired_WithNoPolicies_ReturnsFalse(
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.False(actual.SsoRequired);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||
public void SsoRequired_WithoutExemptStatus_ReturnsFalse(
|
||||
OrganizationUserStatusType userStatus,
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
PolicyType = PolicyType.RequireSso,
|
||||
OrganizationUserStatus = userStatus
|
||||
}
|
||||
]);
|
||||
|
||||
Assert.False(actual.SsoRequired);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void SsoRequired_WithExemptStatus_ReturnsTrue(
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
PolicyType = PolicyType.RequireSso,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed
|
||||
}
|
||||
]);
|
||||
|
||||
Assert.True(actual.SsoRequired);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
@ -41,6 +42,7 @@ public class BaseRequestValidatorTests
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
|
||||
private readonly BaseRequestValidatorTestWrapper _sut;
|
||||
|
||||
@ -61,6 +63,7 @@ public class BaseRequestValidatorTests
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||
_userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>();
|
||||
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||
|
||||
_sut = new BaseRequestValidatorTestWrapper(
|
||||
_userManager,
|
||||
@ -77,7 +80,8 @@ public class BaseRequestValidatorTests
|
||||
_policyService,
|
||||
_featureService,
|
||||
_ssoConfigRepository,
|
||||
_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder,
|
||||
_policyRequirementQuery);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
@ -276,6 +280,75 @@ public class BaseRequestValidatorTests
|
||||
Assert.Equal("SSO authentication is required.", errorResponse.Message);
|
||||
}
|
||||
|
||||
// Test grantTypes with RequireSsoPolicyRequirement when feature flag is enabled
|
||||
[Theory]
|
||||
[BitAutoData("password")]
|
||||
[BitAutoData("webauthn")]
|
||||
[BitAutoData("refresh_token")]
|
||||
public async Task ValidateAsync_GrantTypes_WithPolicyRequirementsEnabled_OrgSsoRequiredTrue_ShouldSetSsoResult(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = grantType;
|
||||
// Configure requirement to require SSO
|
||||
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||
_policyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(Arg.Any<Guid>()).Returns(requirement);
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
Assert.Equal("SSO authentication is required.", errorResponse.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("password")]
|
||||
[BitAutoData("webauthn")]
|
||||
[BitAutoData("refresh_token")]
|
||||
public async Task ValidateAsync_GrantTypes_WithPolicyRequirementsEnabled_OrgSsoRequiredFalse_ShouldSucceed(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = grantType;
|
||||
context.ValidatedTokenRequest.ClientId = "web";
|
||||
|
||||
// Configure requirement to not require SSO
|
||||
var requirement = new RequireSsoPolicyRequirement { SsoRequired = false };
|
||||
_policyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(Arg.Any<Guid>()).Returns(requirement);
|
||||
|
||||
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
await _eventService.Received(1).LogUserEventAsync(
|
||||
context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn);
|
||||
await _userRepository.Received(1).ReplaceAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
// Test grantTypes where SSO would be required but the user is not in an
|
||||
// organization that requires it
|
||||
[Theory]
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
@ -61,7 +62,8 @@ IBaseRequestValidatorTestWrapper
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) :
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery) :
|
||||
base(
|
||||
userManager,
|
||||
userService,
|
||||
@ -77,7 +79,8 @@ IBaseRequestValidatorTestWrapper
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery)
|
||||
{
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user