diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs index 5b9460c5e..cb58e8dfe 100644 --- a/src/Api/Auth/Controllers/EmergencyAccessController.cs +++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs @@ -6,6 +6,7 @@ using Bit.Api.Vault.Models.Response; using Bit.Core.Auth.Services; using Bit.Core.Entities; using Bit.Core.Exceptions; +using Bit.Core.Models.Api.Response; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 2210d79ef..270d09ff2 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -12,6 +12,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Enums.Provider; using Bit.Core.Exceptions; +using Bit.Core.Models.Api.Response; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using Bit.Core.Repositories; @@ -41,6 +42,7 @@ public class AccountsController : Controller private readonly ISendRepository _sendRepository; private readonly ISendService _sendService; private readonly ICaptchaValidationService _captchaValidationService; + private readonly IPolicyService _policyService; public AccountsController( GlobalSettings globalSettings, @@ -54,7 +56,8 @@ public class AccountsController : Controller IUserService userService, ISendRepository sendRepository, ISendService sendService, - ICaptchaValidationService captchaValidationService) + ICaptchaValidationService captchaValidationService, + IPolicyService policyService) { _cipherRepository = cipherRepository; _folderRepository = folderRepository; @@ -68,6 +71,7 @@ public class AccountsController : Controller _sendRepository = sendRepository; _sendService = sendService; _captchaValidationService = captchaValidationService; + _policyService = policyService; } #region DEPRECATED (Moved to Identity Service) @@ -261,7 +265,7 @@ public class AccountsController : Controller } [HttpPost("verify-password")] - public async Task PostVerifyPassword([FromBody] SecretVerificationRequestModel model) + public async Task PostVerifyPassword([FromBody] SecretVerificationRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -271,7 +275,9 @@ public class AccountsController : Controller if (await _userService.CheckPasswordAsync(user, model.MasterPasswordHash)) { - return; + var policyData = await _policyService.GetMasterPasswordPolicyForUserAsync(user); + + return new MasterPasswordPolicyResponseModel(policyData); } ModelState.AddModelError(nameof(model.MasterPasswordHash), "Invalid password."); diff --git a/src/Api/Controllers/PoliciesController.cs b/src/Api/Controllers/PoliciesController.cs index 175e1d6a8..e352e1090 100644 --- a/src/Api/Controllers/PoliciesController.cs +++ b/src/Api/Controllers/PoliciesController.cs @@ -3,6 +3,7 @@ using Bit.Api.Models.Response; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Api.Response; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index 5aeb0e46a..096722d61 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -1,6 +1,7 @@ using Bit.Api.Models.Response; using Bit.Core.Entities; using Bit.Core.Models.Api; +using Bit.Core.Models.Api.Response; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; diff --git a/src/Core/Models/Api/Response/MasterPasswordPolicyResponseModel.cs b/src/Core/Models/Api/Response/MasterPasswordPolicyResponseModel.cs new file mode 100644 index 000000000..6a2753e76 --- /dev/null +++ b/src/Core/Models/Api/Response/MasterPasswordPolicyResponseModel.cs @@ -0,0 +1,36 @@ +using Bit.Core.Models.Data.Organizations.Policies; + +namespace Bit.Core.Models.Api.Response; + +public class MasterPasswordPolicyResponseModel : ResponseModel +{ + public MasterPasswordPolicyResponseModel(MasterPasswordPolicyData data) : base("masterPasswordPolicy") + { + if (data == null) + { + return; + } + + MinComplexity = data.MinComplexity; + MinLength = data.MinLength; + RequireLower = data.RequireLower; + RequireUpper = data.RequireUpper; + RequireNumbers = data.RequireNumbers; + RequireSpecial = data.RequireSpecial; + EnforceOnLogin = data.EnforceOnLogin; + } + + public int? MinComplexity { get; set; } + + public int? MinLength { get; set; } + + public bool? RequireLower { get; set; } + + public bool? RequireUpper { get; set; } + + public bool? RequireNumbers { get; set; } + + public bool? RequireSpecial { get; set; } + + public bool? EnforceOnLogin { get; set; } +} diff --git a/src/Api/Models/Response/PolicyResponseModel.cs b/src/Core/Models/Api/Response/PolicyResponseModel.cs similarity index 93% rename from src/Api/Models/Response/PolicyResponseModel.cs rename to src/Core/Models/Api/Response/PolicyResponseModel.cs index a812a911d..3349d2e62 100644 --- a/src/Api/Models/Response/PolicyResponseModel.cs +++ b/src/Core/Models/Api/Response/PolicyResponseModel.cs @@ -1,9 +1,8 @@ using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Api; -namespace Bit.Api.Models.Response; +namespace Bit.Core.Models.Api.Response; public class PolicyResponseModel : ResponseModel { diff --git a/src/Core/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs b/src/Core/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs new file mode 100644 index 000000000..30294620b --- /dev/null +++ b/src/Core/Models/Data/Organizations/Policies/MasterPasswordPolicyData.cs @@ -0,0 +1,40 @@ +namespace Bit.Core.Models.Data.Organizations.Policies; + +public class MasterPasswordPolicyData : IPolicyDataModel +{ + public int? MinComplexity { get; set; } + public int? MinLength { get; set; } + public bool? RequireLower { get; set; } + public bool? RequireUpper { get; set; } + public bool? RequireNumbers { get; set; } + public bool? RequireSpecial { get; set; } + public bool? EnforceOnLogin { get; set; } + + /// + /// Combine the other policy data with this instance, taking the most secure options + /// + /// The other policy instance to combine with this + public void CombineWith(MasterPasswordPolicyData other) + { + if (other == null) + { + return; + } + + if (other.MinComplexity.HasValue && (!MinComplexity.HasValue || other.MinComplexity > MinComplexity)) + { + MinComplexity = other.MinComplexity; + } + + if (other.MinLength.HasValue && (!MinLength.HasValue || other.MinLength > MinLength)) + { + MinLength = other.MinLength; + } + + RequireLower = (other.RequireLower ?? false) || (RequireLower ?? false); + RequireUpper = (other.RequireUpper ?? false) || (RequireUpper ?? false); + RequireNumbers = (other.RequireNumbers ?? false) || (RequireNumbers ?? false); + RequireSpecial = (other.RequireSpecial ?? false) || (RequireSpecial ?? false); + EnforceOnLogin = (other.EnforceOnLogin ?? false) || (EnforceOnLogin ?? false); + } +} diff --git a/src/Core/Services/IPolicyService.cs b/src/Core/Services/IPolicyService.cs index 5f1b4d366..5be4b6de0 100644 --- a/src/Core/Services/IPolicyService.cs +++ b/src/Core/Services/IPolicyService.cs @@ -1,4 +1,5 @@ using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations.Policies; namespace Bit.Core.Services; @@ -6,4 +7,9 @@ public interface IPolicyService { Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService, Guid? savingUserId); + + /// + /// Get the combined master password policy options for the specified user. + /// + Task GetMasterPasswordPolicyForUserAsync(User user); } diff --git a/src/Core/Services/Implementations/PolicyService.cs b/src/Core/Services/Implementations/PolicyService.cs index b46d75d47..7f1ec3ee0 100644 --- a/src/Core/Services/Implementations/PolicyService.cs +++ b/src/Core/Services/Implementations/PolicyService.cs @@ -2,6 +2,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.Repositories; namespace Bit.Core.Services; @@ -141,6 +142,27 @@ public class PolicyService : IPolicyService await _eventService.LogPolicyEventAsync(policy, Enums.EventType.Policy_Updated); } + public async Task GetMasterPasswordPolicyForUserAsync(User user) + { + var policies = (await _policyRepository.GetManyByUserIdAsync(user.Id)) + .Where(p => p.Type == PolicyType.MasterPassword && p.Enabled) + .ToList(); + + if (!policies.Any()) + { + return null; + } + + var enforcedOptions = new MasterPasswordPolicyData(); + + foreach (var policy in policies) + { + enforcedOptions.CombineWith(policy.GetDataModel()); + } + + return enforcedOptions; + } + private async Task DependsOnSingleOrgAsync(Organization org) { var singleOrg = await _policyRepository.GetByOrganizationIdTypeAsync(org.Id, PolicyType.SingleOrg); diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 0683ac3f6..54d971563 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -11,6 +11,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Identity; using Bit.Core.Models.Api; +using Bit.Core.Models.Api.Response; using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Services; @@ -38,6 +39,7 @@ public abstract class BaseRequestValidator where T : class private readonly GlobalSettings _globalSettings; private readonly IPolicyRepository _policyRepository; private readonly IUserRepository _userRepository; + private readonly IPolicyService _policyService; public BaseRequestValidator( UserManager userManager, @@ -54,7 +56,8 @@ public abstract class BaseRequestValidator where T : class ICurrentContext currentContext, GlobalSettings globalSettings, IPolicyRepository policyRepository, - IUserRepository userRepository) + IUserRepository userRepository, + IPolicyService policyService) { _userManager = userManager; _deviceRepository = deviceRepository; @@ -71,6 +74,7 @@ public abstract class BaseRequestValidator where T : class _globalSettings = globalSettings; _policyRepository = policyRepository; _userRepository = userRepository; + _policyService = policyService; } protected async Task ValidateAsync(T context, ValidatedTokenRequest request, @@ -181,6 +185,7 @@ public abstract class BaseRequestValidator where T : class customResponse.Add("Key", user.Key); } + customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); customResponse.Add("Kdf", (byte)user.Kdf); @@ -239,7 +244,8 @@ public abstract class BaseRequestValidator where T : class new Dictionary { { "TwoFactorProviders", providers.Keys }, - { "TwoFactorProviders2", providers } + { "TwoFactorProviders2", providers }, + { "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) } }); if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) @@ -568,4 +574,18 @@ public abstract class BaseRequestValidator where T : class var failedLoginCount = user?.FailedLoginCount ?? 0; return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling; } + + private async Task GetMasterPasswordPolicy(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.Any()) + { + return null; + } + + return new MasterPasswordPolicyResponseModel(await _policyService.GetMasterPasswordPolicyForUserAsync(user)); + } } diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index 160edec83..02c48cb67 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -36,11 +36,12 @@ public class CustomTokenRequestValidator : BaseRequestValidator(); _sendService = Substitute.For(); _captchaValidationService = Substitute.For(); + _policyService = Substitute.For(); _sut = new AccountsController( _globalSettings, _cipherRepository, @@ -60,7 +62,8 @@ public class AccountsControllerTests : IDisposable _userService, _sendRepository, _sendService, - _captchaValidationService + _captchaValidationService, + _policyService ); }