diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 17926e892487..42a60421f70a 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -592,15 +592,6 @@ await _mailService.SendFailedTwoFactorAttemptEmailAsync(user.Email, failedAttemp private async Task GetMasterPasswordPolicyAsync(User user) { - // Check current context/cache to see if user is in any organizations, avoids extra DB call if not - var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)) - .ToList(); - - if (orgs.Count == 0) - { - return null; - } - var masterPasswordPolicy = await PolicyRequirementQuery.GetAsyncVNext(user.Id); return new MasterPasswordPolicyResponseModel(masterPasswordPolicy.EnforcedOptions); } diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index aae0361349e2..08eb627aeecd 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -1,6 +1,8 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Response; @@ -13,6 +15,7 @@ using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Api; +using Bit.Core.Models.Api.Response; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -110,6 +113,11 @@ public BaseRequestValidatorTests() _clientVersionValidator .Validate(Arg.Any(), Arg.Any()) .Returns(true); + + // Default: no master password policy enforced. + _policyRequirementQuery + .GetAsyncVNext(Arg.Any()) + .Returns(new MasterPasswordPolicyRequirement { EnforcedOptions = null }); } /* Logic path @@ -1661,6 +1669,92 @@ public async Task ValidateAsync_Success_DoesNotOverwriteCurrentContext_WhenAlrea Assert.Equal(middlewareDeviceId, _currentContext.DeviceIdentifier); } + /* Logic path + * ValidateAsync -> BuildSuccessResultAsync -> GetMasterPasswordPolicyAsync + * GetMasterPasswordPolicyAsync should always call PolicyRequirementQuery, even when user has no confirmed org memberships. + * This ensures accepted members are subject to MP policy enforcement. + */ + [Theory] + [BitAutoData] + public async Task ValidateAsync_GetMasterPasswordPolicyAsync_NoPolicyApplies_ReturnsResponseWithNullOptions( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + [AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange + var context = CreateContext(tokenRequest, requestContext, grantResult); + _sut.isValid = true; + + _policyRequirementQuery + .GetAsyncVNext(Arg.Any()) + .Returns(new MasterPasswordPolicyRequirement { EnforcedOptions = null }); + + _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) + .Returns(Task.FromResult(true)); + _ssoRequestValidator.ValidateAsync(requestContext.User, tokenRequest, requestContext) + .Returns(Task.FromResult(true)); + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("test-private-key", "test-public-key") + }); + + // Act + await _sut.ValidateAsync(context); + + // Assert + Assert.False(context.GrantResult.IsError); + await _policyRequirementQuery.Received(1).GetAsyncVNext(Arg.Any()); + var policy = (MasterPasswordPolicyResponseModel)context.GrantResult.CustomResponse["MasterPasswordPolicy"]; + Assert.Null(policy.EnforceOnLogin); + Assert.Null(policy.MinLength); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_GetMasterPasswordPolicyAsync_AcceptedMemberWithPolicy_ReturnsPolicyInResponse( + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, + [AuthFixtures.CustomValidatorRequestContext] CustomValidatorRequestContext requestContext, + GrantValidationResult grantResult) + { + // Arrange: accepted member with an MP policy enforced (EnforceOnLogin=true, MinLength=12) + var context = CreateContext(tokenRequest, requestContext, grantResult); + _sut.isValid = true; + + _policyRequirementQuery + .GetAsyncVNext(Arg.Any()) + .Returns(new MasterPasswordPolicyRequirement + { + EnforcedOptions = new MasterPasswordPolicyData + { + EnforceOnLogin = true, + MinLength = 12 + } + }); + + _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest) + .Returns(Task.FromResult(new Tuple(false, null))); + _deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext) + .Returns(Task.FromResult(true)); + _ssoRequestValidator.ValidateAsync(requestContext.User, tokenRequest, requestContext) + .Returns(Task.FromResult(true)); + _userAccountKeysQuery.Run(Arg.Any()).Returns(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("test-private-key", "test-public-key") + }); + + // Act + await _sut.ValidateAsync(context); + + // Assert + Assert.False(context.GrantResult.IsError); + await _policyRequirementQuery.Received(1).GetAsyncVNext(Arg.Any()); + var policy = (MasterPasswordPolicyResponseModel)context.GrantResult.CustomResponse["MasterPasswordPolicy"]; + Assert.True(policy.EnforceOnLogin); + Assert.Equal(12, policy.MinLength); + } + private BaseRequestValidationContextFake CreateContext( ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext,