diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index a66055b97a..bb17607954 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -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 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(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) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireSsoPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireSsoPolicyRequirement.cs new file mode 100644 index 0000000000..f01ab1983a --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireSsoPolicyRequirement.cs @@ -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; + +/// +/// Policy requirements for the Require SSO policy. +/// +public class RequireSsoPolicyRequirement : IPolicyRequirement +{ + /// + /// Indicates whether the user can use passkey login. + /// + /// + /// The user can use passkey login if they are not a member (Accepted/Confirmed) of an organization + /// that has the Require SSO policy enabled. + /// + public bool CanUsePasskeyLogin { get; init; } + + /// + /// Indicates whether SSO requirement is enforced for the user. + /// + /// + /// The user is required to login with SSO if they are a confirmed member of an organization + /// that has the Require SSO policy enabled. + /// + public bool SsoRequired { get; init; } +} + + +public class RequireSsoPolicyRequirementFactory : BasePolicyRequirementFactory +{ + private readonly GlobalSettings _globalSettings; + + public RequireSsoPolicyRequirementFactory(GlobalSettings globalSettings) + { + _globalSettings = globalSettings; + } + + public override PolicyType PolicyType => PolicyType.RequireSso; + + protected override IEnumerable ExemptRoles => + _globalSettings.Sso.EnforceSsoPolicyForAllUsers + ? Array.Empty() + : [OrganizationUserType.Owner, OrganizationUserType.Admin]; + + public override RequireSsoPolicyRequirement Create(IEnumerable 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; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index d330c57291..1be0e61af7 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -35,5 +35,6 @@ public static class PolicyServiceCollectionExtensions services.AddScoped, SendOptionsPolicyRequirementFactory>(); services.AddScoped, ResetPasswordPolicyRequirementFactory>(); services.AddScoped, PersonalOwnershipPolicyRequirementFactory>(); + services.AddScoped, RequireSsoPolicyRequirementFactory>(); } } diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 88691fa8f7..8b7034c9d7 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -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 where T : class protected ISsoConfigRepository SsoConfigRepository { get; } protected IUserService _userService { get; } protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; } + protected IPolicyRequirementQuery PolicyRequirementQuery { get; } public BaseRequestValidator( UserManager userManager, @@ -55,7 +57,8 @@ public abstract class BaseRequestValidator 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 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 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(user.Id)) + .SsoRequired + : await PolicyService.AnyPoliciesApplicableToUserAsync( + user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed); + if (ssoRequired) { return true; } diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 597d5257e2..841cd14137 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -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 assertionOptionsDataProtector, IFeatureService featureService, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, - IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand) + IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand, + IPolicyRequirementQuery policyRequirementQuery) : base( userManager, userService, @@ -60,7 +62,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator sutProvider) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + sutProvider.GetDependency().VerifySecretAsync(user, default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(false); + sutProvider.GetDependency>() + .Protect(Arg.Any()).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 sutProvider) + { + // Arrange + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + sutProvider.GetDependency().VerifySecretAsync(user, default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true); + sutProvider.GetDependency() + .GetAsync(user.Id) + .ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = false }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => 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 sutProvider) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + sutProvider.GetDependency().VerifySecretAsync(user, default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true); + sutProvider.GetDependency() + .GetAsync(user.Id) + .ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = true }); + sutProvider.GetDependency>() + .Protect(Arg.Any()).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 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 sutProvider) + { + // Arrange + var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions); + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + sutProvider.GetDependency() + .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey) + .Returns(true); + sutProvider.GetDependency>() + .Unprotect(requestModel.Token) + .Returns(token); + sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(false); + + // Act + await sutProvider.Sut.Post(requestModel); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetUserByPrincipalAsync(default); + await sutProvider.GetDependency() + .Received(1) + .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey); + } + + [Theory, BitAutoData] + public async Task Post_WithPolicyRequirementsEnabled_CanUsePasskeyLoginFalse_ThrowsBadRequestException( + WebAuthnLoginCredentialCreateRequestModel requestModel, + CredentialCreateOptions createOptions, + User user, + SutProvider sutProvider) + { + // Arrange + var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions); + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + sutProvider.GetDependency() + .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any(), false) + .Returns(true); + sutProvider.GetDependency>() + .Unprotect(requestModel.Token) + .Returns(token); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true); + sutProvider.GetDependency() + .GetAsync(user.Id) + .ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = false }); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => 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 sutProvider) + { + // Arrange + var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions); + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + sutProvider.GetDependency() + .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey) + .Returns(true); + sutProvider.GetDependency>() + .Unprotect(requestModel.Token) + .Returns(token); + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true); + sutProvider.GetDependency() + .GetAsync(user.Id) + .ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = true }); + + // Act + await sutProvider.Sut.Post(requestModel); + + // Assert + await sutProvider.GetDependency() + .Received(1) + .GetUserByPrincipalAsync(default); + await sutProvider.GetDependency() + .Received(1) + .CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey); + } + [Theory, BitAutoData] public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider sutProvider) { diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireSsoPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireSsoPolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..06253f20ed --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireSsoPolicyRequirementFactoryTests.cs @@ -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 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 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 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 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 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 sutProvider) + { + var actual = sutProvider.Sut.Create( + [ + new PolicyDetails + { + PolicyType = PolicyType.RequireSso, + OrganizationUserStatus = OrganizationUserStatusType.Confirmed + } + ]); + + Assert.True(actual.SsoRequired); + } +} diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 589aac2842..1d58b62b02 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -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(); _ssoConfigRepository = Substitute.For(); _userDecryptionOptionsBuilder = Substitute.For(); + _policyRequirementQuery = Substitute.For(); _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(Arg.Any()).Returns(requirement); + + // Act + await _sut.ValidateAsync(context); + + // Assert + await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync( + Arg.Any(), 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(Arg.Any()).Returns(requirement); + + _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest) + .Returns(Task.FromResult(new Tuple(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()); + } + // Test grantTypes where SSO would be required but the user is not in an // organization that requires it [Theory] diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index cbe091a44c..ed28f00ce7 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -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) { }