1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -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:
Rui Tomé
2025-04-23 15:43:36 +01:00
committed by GitHub
parent 9667ecaf9e
commit 722fae81b3
11 changed files with 447 additions and 18 deletions

View File

@ -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)
{

View File

@ -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);
}
}

View File

@ -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]

View File

@ -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)
{
}