From cd3ae7301c3af2c18de12f5c9e39cea31c74b0ca Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Wed, 25 Sep 2024 12:56:02 -0700 Subject: [PATCH 01/25] replace duo token provider --- .../Models/Request/TwoFactorRequestModels.cs | 52 ++------ .../TwoFactor/TwoFactorDuoResponseModel.cs | 65 +--------- src/Core/Auth/Identity/DuoTokenProvider.cs | 119 ++++++++++++++++++ .../Identity/DuoUniversalTokenProvider.cs | 79 ++++++++++++ src/Core/Auth/Identity/DuoWebTokenProvider.cs | 86 ------------- .../OrganizationDuoUniversalTokenProvider.cs | 79 ++++++++++++ .../OrganizationDuoWebTokenProvider.cs | 76 ----------- .../IdentityServer/BaseRequestValidator.cs | 40 +----- .../CustomTokenRequestValidator.cs | 5 +- .../ResourceOwnerPasswordValidator.cs | 5 +- .../IdentityServer/WebAuthnGrantValidator.cs | 5 +- .../Utilities/ServiceCollectionExtensions.cs | 6 +- 12 files changed, 301 insertions(+), 316 deletions(-) create mode 100644 src/Core/Auth/Identity/DuoTokenProvider.cs create mode 100644 src/Core/Auth/Identity/DuoUniversalTokenProvider.cs delete mode 100644 src/Core/Auth/Identity/DuoWebTokenProvider.cs create mode 100644 src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs delete mode 100644 src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs diff --git a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs index f2f01a2378e5..f62d2ae4ecdc 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -42,20 +42,12 @@ public User ToUser(User existingUser) public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject { - /* - To support both v2 and v4 we need to remove the required annotation from the properties. - todo - the required annotation will be added back in PM-8107. - */ + [Required] [StringLength(50)] public string ClientId { get; set; } + [Required] [StringLength(50)] public string ClientSecret { get; set; } - //todo - will remove SKey and IKey with PM-8107 - [StringLength(50)] - public string IntegrationKey { get; set; } - //todo - will remove SKey and IKey with PM-8107 - [StringLength(50)] - public string SecretKey { get; set; } [Required] [StringLength(50)] public string Host { get; set; } @@ -65,22 +57,17 @@ public User ToUser(User existingUser) var providers = existingUser.GetTwoFactorProviders(); if (providers == null) { - providers = new Dictionary(); + providers = []; } else if (providers.ContainsKey(TwoFactorProviderType.Duo)) { providers.Remove(TwoFactorProviderType.Duo); } - Temporary_SyncDuoParams(); - providers.Add(TwoFactorProviderType.Duo, new TwoFactorProvider { MetaData = new Dictionary { - //todo - will remove SKey and IKey with PM-8107 - ["SKey"] = SecretKey, - ["IKey"] = IntegrationKey, ["ClientSecret"] = ClientSecret, ["ClientId"] = ClientId, ["Host"] = Host @@ -96,22 +83,17 @@ public Organization ToOrganization(Organization existingOrg) var providers = existingOrg.GetTwoFactorProviders(); if (providers == null) { - providers = new Dictionary(); + providers = []; } else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo)) { providers.Remove(TwoFactorProviderType.OrganizationDuo); } - Temporary_SyncDuoParams(); - providers.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider { MetaData = new Dictionary { - //todo - will remove SKey and IKey with PM-8107 - ["SKey"] = SecretKey, - ["IKey"] = IntegrationKey, ["ClientSecret"] = ClientSecret, ["ClientId"] = ClientId, ["Host"] = Host @@ -124,33 +106,13 @@ public Organization ToOrganization(Organization existingOrg) public override IEnumerable Validate(ValidationContext validationContext) { - if (!DuoApi.ValidHost(Host)) + if (!DuoApi.ValidHost(Host)) // TODO replace with DuoUniversal { yield return new ValidationResult("Host is invalid.", [nameof(Host)]); } - if (string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId) && - string.IsNullOrWhiteSpace(SecretKey) && string.IsNullOrWhiteSpace(IntegrationKey)) - { - yield return new ValidationResult("Neither v2 or v4 values are valid.", [nameof(IntegrationKey), nameof(SecretKey), nameof(ClientSecret), nameof(ClientId)]); - } - } - - /* - use this method to ensure that both v2 params and v4 params are in sync - todo will be removed in pm-8107 - */ - private void Temporary_SyncDuoParams() - { - // Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret - if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId)) - { - SecretKey = ClientSecret; - IntegrationKey = ClientId; - } - else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey)) + if (string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId)) { - ClientSecret = SecretKey; - ClientId = IntegrationKey; + yield return new ValidationResult("ClientSecret or ClientId are invalid", [nameof(ClientSecret), nameof(ClientId)]); } } } diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs index 8b8c36d2e8b4..d17ee7c8e03d 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs @@ -13,10 +13,7 @@ public class TwoFactorDuoResponseModel : ResponseModel public TwoFactorDuoResponseModel(User user) : base(ResponseObj) { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } + ArgumentNullException.ThrowIfNull(user); var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); Build(provider); @@ -25,10 +22,7 @@ public TwoFactorDuoResponseModel(User user) public TwoFactorDuoResponseModel(Organization org) : base(ResponseObj) { - if (org == null) - { - throw new ArgumentNullException(nameof(org)); - } + ArgumentNullException.ThrowIfNull(org); var provider = org.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); Build(provider); @@ -36,14 +30,9 @@ public TwoFactorDuoResponseModel(Organization org) public bool Enabled { get; set; } public string Host { get; set; } - //TODO - will remove SecretKey with PM-8107 - public string SecretKey { get; set; } - //TODO - will remove IntegrationKey with PM-8107 - public string IntegrationKey { get; set; } public string ClientSecret { get; set; } public string ClientId { get; set; } - // updated build to assist in the EDD migration for the Duo 2FA provider private void Build(TwoFactorProvider provider) { if (provider?.MetaData != null && provider.MetaData.Count > 0) @@ -54,36 +43,13 @@ private void Build(TwoFactorProvider provider) { Host = (string)host; } - - //todo - will remove SKey and IKey with PM-8107 - // check Skey and IKey first if they exist - if (provider.MetaData.TryGetValue("SKey", out var sKey)) - { - ClientSecret = MaskKey((string)sKey); - SecretKey = MaskKey((string)sKey); - } - if (provider.MetaData.TryGetValue("IKey", out var iKey)) - { - IntegrationKey = (string)iKey; - ClientId = (string)iKey; - } - - // Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret if (provider.MetaData.TryGetValue("ClientSecret", out var clientSecret)) { - if (!string.IsNullOrWhiteSpace((string)clientSecret)) - { - ClientSecret = MaskKey((string)clientSecret); - SecretKey = MaskKey((string)clientSecret); - } + ClientSecret = MaskKey((string)clientSecret); } if (provider.MetaData.TryGetValue("ClientId", out var clientId)) { - if (!string.IsNullOrWhiteSpace((string)clientId)) - { - ClientId = (string)clientId; - IntegrationKey = (string)clientId; - } + ClientId = (string)clientId; } } else @@ -92,29 +58,6 @@ private void Build(TwoFactorProvider provider) } } - /* - use this method to ensure that both v2 params and v4 params are in sync - todo will be removed in pm-8107 - */ - private void Temporary_SyncDuoParams() - { - // Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret - if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId)) - { - SecretKey = ClientSecret; - IntegrationKey = ClientId; - } - else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey)) - { - ClientSecret = SecretKey; - ClientId = IntegrationKey; - } - else - { - throw new InvalidDataException("Invalid Duo parameters."); - } - } - private static string MaskKey(string key) { if (string.IsNullOrWhiteSpace(key) || key.Length <= 6) diff --git a/src/Core/Auth/Identity/DuoTokenProvider.cs b/src/Core/Auth/Identity/DuoTokenProvider.cs new file mode 100644 index 000000000000..df1621a01124 --- /dev/null +++ b/src/Core/Auth/Identity/DuoTokenProvider.cs @@ -0,0 +1,119 @@ +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Duo = DuoUniversal; + +namespace Bit.Core.Auth.Identity; + +/// +/// In the TwoFactorController before we write a configuration to the database we check the configuration +/// this interface creates a simple way to inject the process into those endpoints. +/// +public interface IDuoTokenProvider +{ + Task BuildDuoClientAsync(TwoFactorProvider provider); +} + +/// +/// OrganizationDuo and Duo types both use the same flows so both of those Token Providers will +/// inherit from this class +/// +public class DuoTokenProvider : IDuoTokenProvider +{ + private readonly ICurrentContext _currentContext; + private readonly GlobalSettings _globalSettings; + + public DuoTokenProvider(ICurrentContext currentContext, GlobalSettings globalSettings) + { + _currentContext = currentContext; + _globalSettings = globalSettings; + } + + protected bool HasProperMetaData(TwoFactorProvider provider) + { + return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") && + provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host"); + } + + protected async Task GenerateAuthUrlAsync( + TwoFactorProvider provider, + IDataProtectorTokenFactory tokenDataFactory, + User user) + { + var duoClient = await BuildDuoClientAsync(provider); + if (duoClient == null) + { + return null; + } + + var state = tokenDataFactory.Protect(new DuoUserStateTokenable(user)); + var authUrl = duoClient.GenerateAuthUri(user.Email, state); + + return authUrl; + } + + /// + /// Makes the request to Duo to validate the authCode and state token + /// + /// Duo or OrganizationDuo + /// Factory for decrypting the state + /// self + /// token received from the client + /// boolean based on result from Duo + protected async Task RequestDuoValidationAsync( + TwoFactorProvider provider, + IDataProtectorTokenFactory tokenDataFactory, + User user, + string token) + { + var duoClient = await BuildDuoClientAsync(provider); + if (duoClient == null) + { + return false; + } + + var parts = token.Split("|"); + var authCode = parts[0]; + var state = parts[1]; + tokenDataFactory.TryUnprotect(state, out var tokenable); + if (!tokenable.Valid || !tokenable.TokenIsValid(user)) + { + return false; + } + + // duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used + // their authCode with a victims credentials + var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email); + // If the result of the exchange doesn't throw an exception and it's not null, then it's valid + return res.AuthResult.Result == "allow"; + } + + /// + /// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation + /// + /// TwoFactorProvider Duo or OrganizationDuo + /// Duo.Client object or null + public async Task BuildDuoClientAsync(TwoFactorProvider provider) + { + // Fetch Client name from header value since duo auth can be initiated from multiple clients and we want + // to redirect back to the initiating client + _currentContext.HttpContext.Request.Headers.TryGetValue("Bitwarden-Client-Name", out var bitwardenClientName); + var redirectUri = string.Format("{0}/duo-redirect-connector.html?client={1}", + _globalSettings.BaseServiceUri.Vault, bitwardenClientName.FirstOrDefault() ?? "web"); + + var client = new Duo.ClientBuilder( + (string)provider.MetaData["ClientId"], + (string)provider.MetaData["ClientSecret"], + (string)provider.MetaData["Host"], + redirectUri).Build(); + + if (!await client.DoHealthCheck(false)) + { + return null; + } + return client; + } +} \ No newline at end of file diff --git a/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs new file mode 100644 index 000000000000..6f1344f63b71 --- /dev/null +++ b/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs @@ -0,0 +1,79 @@ +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Auth.Identity; + +public class DuoUniversalTokenProvider : DuoTokenProvider, IUserTwoFactorTokenProvider +{ + private readonly IServiceProvider _serviceProvider; + + public DuoUniversalTokenProvider( + IServiceProvider serviceProvider, + GlobalSettings globalSettings, + ICurrentContext currentContext) + : base(currentContext, globalSettings) + { + _serviceProvider = serviceProvider; + } + + public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + { + var provider = await GetTwoFactorProvideAsync(user); + if (provider == null) + { + return false; + } + + var userService = _serviceProvider.GetRequiredService(); + return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user); + } + + public async Task GenerateAsync(string purpose, UserManager manager, User user) + { + var provider = await GetTwoFactorProvideAsync(user); + if (provider == null) + { + return null; + } + + var tokenDataFactory = _serviceProvider.GetRequiredService>(); + return await GenerateAuthUrlAsync(provider, tokenDataFactory, user); + } + + public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) + { + var provider = await GetTwoFactorProvideAsync(user); + if (provider == null) + { + return false; + } + + var tokenDataFactory = _serviceProvider.GetRequiredService>(); + return await RequestDuoValidationAsync(provider, tokenDataFactory, user, token); + } + + private async Task GetTwoFactorProvideAsync(User user) + { + var userService = _serviceProvider.GetRequiredService(); + if (!await userService.CanAccessPremium(user)) + { + return null; + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); + if (!HasProperMetaData(provider)) + { + return null; + } + + return provider; + } +} diff --git a/src/Core/Auth/Identity/DuoWebTokenProvider.cs b/src/Core/Auth/Identity/DuoWebTokenProvider.cs deleted file mode 100644 index 6ab020326284..000000000000 --- a/src/Core/Auth/Identity/DuoWebTokenProvider.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; -using Bit.Core.Auth.Utilities.Duo; -using Bit.Core.Entities; -using Bit.Core.Services; -using Bit.Core.Settings; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; - -namespace Bit.Core.Auth.Identity; - -public class DuoWebTokenProvider : IUserTwoFactorTokenProvider -{ - private readonly IServiceProvider _serviceProvider; - private readonly GlobalSettings _globalSettings; - - public DuoWebTokenProvider( - IServiceProvider serviceProvider, - GlobalSettings globalSettings) - { - _serviceProvider = serviceProvider; - _globalSettings = globalSettings; - } - - public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) - { - var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) - { - return false; - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) - { - return false; - } - - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user); - } - - public async Task GenerateAsync(string purpose, UserManager manager, User user) - { - var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) - { - return null; - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) - { - return null; - } - - var signatureRequest = DuoWeb.SignRequest((string)provider.MetaData["IKey"], - (string)provider.MetaData["SKey"], _globalSettings.Duo.AKey, user.Email); - return signatureRequest; - } - - public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) - { - var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) - { - return false; - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) - { - return false; - } - - var response = DuoWeb.VerifyResponse((string)provider.MetaData["IKey"], (string)provider.MetaData["SKey"], - _globalSettings.Duo.AKey, token); - - return response == user.Email; - } - - private bool HasProperMetaData(TwoFactorProvider provider) - { - return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") && - provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host"); - } -} diff --git a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs new file mode 100644 index 000000000000..df52946d0786 --- /dev/null +++ b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs @@ -0,0 +1,79 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Auth.Identity; + +public interface IOrganizationDuoUniversalTokenProvider : IOrganizationTwoFactorTokenProvider { } + +public class OrganizationDuoUniversalTokenProvider : DuoTokenProvider, IOrganizationDuoUniversalTokenProvider +{ + private readonly IServiceProvider _serviceProvider; + public OrganizationDuoUniversalTokenProvider( + GlobalSettings globalSettings, + IServiceProvider serviceProvider, + ICurrentContext currentContext + ) : base(currentContext, globalSettings) + { + _serviceProvider = serviceProvider; + } + + public Task CanGenerateTwoFactorTokenAsync(Organization organization) + { + if (organization == null || !organization.Enabled || !organization.Use2fa) + { + return Task.FromResult(false); + } + + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo) + && HasProperMetaData(provider); + return Task.FromResult(canGenerate); + } + + public async Task GenerateAsync(Organization organization, User user) + { + var provider = GetTwoFactorProvider(organization); + if (provider == null) + { + return null; + } + + var tokenDataFactory = _serviceProvider.GetRequiredService>(); + return await GenerateAuthUrlAsync(provider, tokenDataFactory, user); + } + + public async Task ValidateAsync(string token, Organization organization, User user) + { + var provider = GetTwoFactorProvider(organization); + if (provider == null) + { + return false; + } + + var tokenDataFactory = _serviceProvider.GetRequiredService>(); + return await RequestDuoValidationAsync(provider, tokenDataFactory, user, token); + } + + private TwoFactorProvider GetTwoFactorProvider(Organization organization) + { + if (organization == null || !organization.Enabled || !organization.Use2fa) + { + return null; + } + + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + if (!HasProperMetaData(provider)) + { + return null; + } + + return provider; + } +} diff --git a/src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs b/src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs deleted file mode 100644 index 58bcf5efd8db..000000000000 --- a/src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; -using Bit.Core.Auth.Utilities.Duo; -using Bit.Core.Entities; -using Bit.Core.Settings; - -namespace Bit.Core.Auth.Identity; - -public interface IOrganizationDuoWebTokenProvider : IOrganizationTwoFactorTokenProvider { } - -public class OrganizationDuoWebTokenProvider : IOrganizationDuoWebTokenProvider -{ - private readonly GlobalSettings _globalSettings; - - public OrganizationDuoWebTokenProvider(GlobalSettings globalSettings) - { - _globalSettings = globalSettings; - } - - public Task CanGenerateTwoFactorTokenAsync(Organization organization) - { - if (organization == null || !organization.Enabled || !organization.Use2fa) - { - return Task.FromResult(false); - } - - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo) - && HasProperMetaData(provider); - return Task.FromResult(canGenerate); - } - - public Task GenerateAsync(Organization organization, User user) - { - if (organization == null || !organization.Enabled || !organization.Use2fa) - { - return Task.FromResult(null); - } - - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - if (!HasProperMetaData(provider)) - { - return Task.FromResult(null); - } - - var signatureRequest = DuoWeb.SignRequest(provider.MetaData["IKey"].ToString(), - provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, user.Email); - return Task.FromResult(signatureRequest); - } - - public Task ValidateAsync(string token, Organization organization, User user) - { - if (organization == null || !organization.Enabled || !organization.Use2fa) - { - return Task.FromResult(false); - } - - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - if (!HasProperMetaData(provider)) - { - return Task.FromResult(false); - } - - var response = DuoWeb.VerifyResponse(provider.MetaData["IKey"].ToString(), - provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, token); - - return Task.FromResult(response == user.Email); - } - - private bool HasProperMetaData(TwoFactorProvider provider) - { - return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") && - provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host"); - } -} diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 881ae4d49b16..cc2028103243 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -36,8 +36,7 @@ public abstract class BaseRequestValidator where T : class private readonly IDeviceRepository _deviceRepository; private readonly IDeviceService _deviceService; private readonly IEventService _eventService; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; + private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoWebTokenProvider; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IApplicationCacheService _applicationCacheService; @@ -60,8 +59,7 @@ public BaseRequestValidator( IDeviceService deviceService, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IApplicationCacheService applicationCacheService, @@ -82,7 +80,6 @@ public BaseRequestValidator( _userService = userService; _eventService = eventService; _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; - _duoWebV4SDKService = duoWebV4SDKService; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _applicationCacheService = applicationCacheService; @@ -431,20 +428,6 @@ private async Task VerifyTwoFactor(User user, Organization organization, T { return false; } - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (type == TwoFactorProviderType.Duo) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - } - return await _userManager.VerifyTwoFactorTokenAsync(user, CoreHelpers.CustomProviderName(type), token); case TwoFactorProviderType.OrganizationDuo: @@ -452,21 +435,6 @@ private async Task VerifyTwoFactor(User user, Organization organization, T { return false; } - - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (type == TwoFactorProviderType.OrganizationDuo) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - } - return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user); default: return false; @@ -494,7 +462,7 @@ private async Task> BuildTwoFactorParams(Organization var duoResponse = new Dictionary { ["Host"] = provider.MetaData["Host"], - ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), + ["AuthUrl"] = token }; return duoResponse; @@ -526,7 +494,7 @@ private async Task> BuildTwoFactorParams(Organization var duoResponse = new Dictionary { ["Host"] = provider.MetaData["Host"], - ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), + ["AuthUrl"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user), }; return duoResponse; diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index 3af1337ee2ba..afdc8b8a162a 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -33,8 +33,7 @@ public CustomTokenRequestValidator( IDeviceService deviceService, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IApplicationCacheService applicationCacheService, @@ -49,7 +48,7 @@ public CustomTokenRequestValidator( IFeatureService featureService, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : base(userManager, deviceRepository, deviceService, userService, eventService, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, + organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs index cb63bd94edc0..161ced7910fc 100644 --- a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -31,8 +31,7 @@ public ResourceOwnerPasswordValidator( IDeviceService deviceService, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IApplicationCacheService applicationCacheService, @@ -49,7 +48,7 @@ public ResourceOwnerPasswordValidator( ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : base(userManager, deviceRepository, deviceService, userService, eventService, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, + organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) { diff --git a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs index 855226565268..6758f62e9d1b 100644 --- a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs @@ -34,8 +34,7 @@ public WebAuthnGrantValidator( IDeviceService deviceService, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IApplicationCacheService applicationCacheService, @@ -53,7 +52,7 @@ public WebAuthnGrantValidator( IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand ) : base(userManager, deviceRepository, deviceService, userService, eventService, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, + organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) { diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index be451ea3181d..e9b31721e48b 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -371,8 +371,8 @@ public static void AddNoopServices(this IServiceCollection services) public static IdentityBuilder AddCustomIdentityServices( this IServiceCollection services, GlobalSettings globalSettings) { - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.Configure(options => options.IterationCount = 100000); services.Configure(options => { @@ -413,7 +413,7 @@ public static IdentityBuilder AddCustomIdentityServices( CoreHelpers.CustomProviderName(TwoFactorProviderType.Email)) .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey)) - .AddTokenProvider( + .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.Duo)) .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)) From a9170970bba9f4939c0422c58f53f5e273ce1b76 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Thu, 26 Sep 2024 11:55:21 -0700 Subject: [PATCH 02/25] removed last bits and replaced validation --- .../Auth/Controllers/TwoFactorController.cs | 39 +-- .../Models/Request/TwoFactorRequestModels.cs | 13 - src/Core/Auth/Identity/DuoTokenProvider.cs | 31 +- src/Core/Auth/Utilities/DuoApi.cs | 277 ------------------ src/Core/Auth/Utilities/DuoUtilities.cs | 16 + src/Core/Auth/Utilities/DuoWeb.cs | 240 --------------- 6 files changed, 47 insertions(+), 569 deletions(-) delete mode 100644 src/Core/Auth/Utilities/DuoApi.cs create mode 100644 src/Core/Auth/Utilities/DuoUtilities.cs delete mode 100644 src/Core/Auth/Utilities/DuoWeb.cs diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 0a50f9bc2fca..33079c250657 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -5,9 +5,9 @@ using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -30,11 +30,11 @@ public class TwoFactorController : Controller private readonly IUserService _userService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationService _organizationService; - private readonly GlobalSettings _globalSettings; private readonly UserManager _userManager; private readonly ICurrentContext _currentContext; private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand; private readonly IFeatureService _featureService; + private readonly IDuoTokenProvider _duoTokenProvider; private readonly IDataProtectorTokenFactory _twoFactorAuthenticatorDataProtector; private readonly IDataProtectorTokenFactory _ssoEmailTwoFactorSessionDataProtector; private readonly bool _TwoFactorAuthenticatorTokenFeatureFlagEnabled; @@ -48,17 +48,18 @@ public TwoFactorController( ICurrentContext currentContext, IVerifyAuthRequestCommand verifyAuthRequestCommand, IFeatureService featureService, + IDuoTokenProvider duoTokenProvider, IDataProtectorTokenFactory twoFactorAuthenticatorDataProtector, IDataProtectorTokenFactory ssoEmailTwoFactorSessionDataProtector) { _userService = userService; _organizationRepository = organizationRepository; _organizationService = organizationService; - _globalSettings = globalSettings; _userManager = userManager; _currentContext = currentContext; _verifyAuthRequestCommand = verifyAuthRequestCommand; _featureService = featureService; + _duoTokenProvider = duoTokenProvider; _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector; _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector; _TwoFactorAuthenticatorTokenFeatureFlagEnabled = _featureService.IsEnabled(FeatureFlagKeys.AuthenticatorTwoFactorToken); @@ -200,21 +201,7 @@ public async Task GetDuo([FromBody] SecretVerificatio public async Task PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model) { var user = await CheckAsync(model, true); - try - { - // for backwards compatibility - will be removed with PM-8107 - DuoApi duoApi = null; - if (model.ClientId != null && model.ClientSecret != null) - { - duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host); - } - else - { - duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host); - } - await duoApi.JSONApiCall("GET", "/auth/v2/check"); - } - catch (DuoException) + if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); @@ -257,21 +244,7 @@ public async Task PutOrganizationDuo(string id, } var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException(); - try - { - // for backwards compatibility - will be removed with PM-8107 - DuoApi duoApi = null; - if (model.ClientId != null && model.ClientSecret != null) - { - duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host); - } - else - { - duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host); - } - await duoApi.JSONApiCall("GET", "/auth/v2/check"); - } - catch (DuoException) + if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); diff --git a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs index f62d2ae4ecdc..7a280d526bc0 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; -using Bit.Core.Auth.Utilities; using Bit.Core.Entities; using Fido2NetLib; @@ -103,18 +102,6 @@ public Organization ToOrganization(Organization existingOrg) existingOrg.SetTwoFactorProviders(providers); return existingOrg; } - - public override IEnumerable Validate(ValidationContext validationContext) - { - if (!DuoApi.ValidHost(Host)) // TODO replace with DuoUniversal - { - yield return new ValidationResult("Host is invalid.", [nameof(Host)]); - } - if (string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId)) - { - yield return new ValidationResult("ClientSecret or ClientId are invalid", [nameof(ClientSecret), nameof(ClientId)]); - } - } } public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestModel, IValidatableObject diff --git a/src/Core/Auth/Identity/DuoTokenProvider.cs b/src/Core/Auth/Identity/DuoTokenProvider.cs index df1621a01124..69fa78b0fc1b 100644 --- a/src/Core/Auth/Identity/DuoTokenProvider.cs +++ b/src/Core/Auth/Identity/DuoTokenProvider.cs @@ -1,5 +1,6 @@ using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Settings; @@ -14,7 +15,7 @@ namespace Bit.Core.Auth.Identity; /// public interface IDuoTokenProvider { - Task BuildDuoClientAsync(TwoFactorProvider provider); + Task ValidateDuoConfiguration(string clientId, string clientSecret, string host); } /// @@ -34,8 +35,11 @@ public DuoTokenProvider(ICurrentContext currentContext, GlobalSettings globalSet protected bool HasProperMetaData(TwoFactorProvider provider) { - return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") && - provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host"); + return provider?.MetaData != null && + provider.MetaData.ContainsKey("ClientId") && + provider.MetaData.ContainsKey("ClientSecret") && + provider.MetaData.ContainsKey("Host") && + DuoUtilities.ValidHost((string)provider.MetaData["Host"]); } protected async Task GenerateAuthUrlAsync( @@ -43,7 +47,7 @@ protected async Task GenerateAuthUrlAsync( IDataProtectorTokenFactory tokenDataFactory, User user) { - var duoClient = await BuildDuoClientAsync(provider); + var duoClient = await BuildDuoTwoFactorClientAsync(provider); if (duoClient == null) { return null; @@ -69,7 +73,7 @@ protected async Task RequestDuoValidationAsync( User user, string token) { - var duoClient = await BuildDuoClientAsync(provider); + var duoClient = await BuildDuoTwoFactorClientAsync(provider); if (duoClient == null) { return false; @@ -96,7 +100,7 @@ protected async Task RequestDuoValidationAsync( /// /// TwoFactorProvider Duo or OrganizationDuo /// Duo.Client object or null - public async Task BuildDuoClientAsync(TwoFactorProvider provider) + protected async Task BuildDuoTwoFactorClientAsync(TwoFactorProvider provider) { // Fetch Client name from header value since duo auth can be initiated from multiple clients and we want // to redirect back to the initiating client @@ -116,4 +120,19 @@ protected async Task RequestDuoValidationAsync( } return client; } + + /// + /// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation + /// + /// + /// + /// + /// + public async Task ValidateDuoConfiguration(string clientId, string clientSecret, string host) + { + // The AuthURI isn't important for this health check so we pass in a non-empty string + var client = new Duo.ClientBuilder(clientId, clientSecret, host, "non-empty").Build(); + + return await client.DoHealthCheck(false); + } } \ No newline at end of file diff --git a/src/Core/Auth/Utilities/DuoApi.cs b/src/Core/Auth/Utilities/DuoApi.cs deleted file mode 100644 index 8bf5f16a91b4..000000000000 --- a/src/Core/Auth/Utilities/DuoApi.cs +++ /dev/null @@ -1,277 +0,0 @@ -/* -Original source modified from https://github.com/duosecurity/duo_api_csharp - -============================================================================= -============================================================================= - -Copyright (c) 2018 Duo Security -All rights reserved -*/ - -using System.Globalization; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Web; -using Bit.Core.Models.Api.Response.Duo; - -namespace Bit.Core.Auth.Utilities; - -public class DuoApi -{ - private const string UrlScheme = "https"; - private const string UserAgent = "Bitwarden_DuoAPICSharp/1.0 (.NET Core)"; - - private readonly string _host; - private readonly string _ikey; - private readonly string _skey; - - private readonly HttpClient _httpClient = new(); - - public DuoApi(string ikey, string skey, string host) - { - _ikey = ikey; - _skey = skey; - _host = host; - - if (!ValidHost(host)) - { - throw new DuoException("Invalid Duo host configured.", new ArgumentException(nameof(host))); - } - } - - public static bool ValidHost(string host) - { - if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri)) - { - return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") && - uri.Host.StartsWith("api-") && - (uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com")); - } - return false; - } - - public static string CanonicalizeParams(Dictionary parameters) - { - var ret = new List(); - foreach (var pair in parameters) - { - var p = string.Format("{0}={1}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(pair.Value)); - // Signatures require upper-case hex digits. - p = Regex.Replace(p, "(%[0-9A-Fa-f][0-9A-Fa-f])", c => c.Value.ToUpperInvariant()); - // Escape only the expected characters. - p = Regex.Replace(p, "([!'()*])", c => "%" + Convert.ToByte(c.Value[0]).ToString("X")); - p = p.Replace("%7E", "~"); - // UrlEncode converts space (" ") to "+". The - // signature algorithm requires "%20" instead. Actual - // + has already been replaced with %2B. - p = p.Replace("+", "%20"); - ret.Add(p); - } - - ret.Sort(StringComparer.Ordinal); - return string.Join("&", ret.ToArray()); - } - - protected string CanonicalizeRequest(string method, string path, string canonParams, string date) - { - string[] lines = { - date, - method.ToUpperInvariant(), - _host.ToLower(), - path, - canonParams, - }; - return string.Join("\n", lines); - } - - public string Sign(string method, string path, string canonParams, string date) - { - var canon = CanonicalizeRequest(method, path, canonParams, date); - var sig = HmacSign(canon); - var auth = string.Concat(_ikey, ':', sig); - return string.Concat("Basic ", Encode64(auth)); - } - - /// The request timeout, in milliseconds. - /// Specify 0 to use the system-default timeout. Use caution if - /// you choose to specify a custom timeout - some API - /// calls (particularly in the Auth APIs) will not - /// return a response until an out-of-band authentication process - /// has completed. In some cases, this may take as much as a - /// small number of minutes. - private async Task<(string result, HttpStatusCode statusCode)> ApiCall(string method, string path, Dictionary parameters, int timeout) - { - if (parameters == null) - { - parameters = new Dictionary(); - } - - var canonParams = CanonicalizeParams(parameters); - var query = string.Empty; - if (!method.Equals("POST") && !method.Equals("PUT")) - { - if (parameters.Count > 0) - { - query = "?" + canonParams; - } - } - var url = $"{UrlScheme}://{_host}{path}{query}"; - - var dateString = RFC822UtcNow(); - var auth = Sign(method, path, canonParams, dateString); - - var request = new HttpRequestMessage - { - Method = new HttpMethod(method), - RequestUri = new Uri(url), - }; - request.Headers.Add("Authorization", auth); - request.Headers.Add("X-Duo-Date", dateString); - request.Headers.UserAgent.ParseAdd(UserAgent); - - if (timeout > 0) - { - _httpClient.Timeout = TimeSpan.FromMilliseconds(timeout); - } - - if (method.Equals("POST") || method.Equals("PUT")) - { - request.Content = new StringContent(canonParams, Encoding.UTF8, "application/x-www-form-urlencoded"); - } - - var response = await _httpClient.SendAsync(request); - var result = await response.Content.ReadAsStringAsync(); - var statusCode = response.StatusCode; - return (result, statusCode); - } - - public async Task JSONApiCall(string method, string path, Dictionary parameters = null) - { - return await JSONApiCall(method, path, parameters, 0); - } - - /// The request timeout, in milliseconds. - /// Specify 0 to use the system-default timeout. Use caution if - /// you choose to specify a custom timeout - some API - /// calls (particularly in the Auth APIs) will not - /// return a response until an out-of-band authentication process - /// has completed. In some cases, this may take as much as a - /// small number of minutes. - private async Task JSONApiCall(string method, string path, Dictionary parameters, int timeout) - { - var (res, statusCode) = await ApiCall(method, path, parameters, timeout); - try - { - var obj = JsonSerializer.Deserialize(res); - if (obj.Stat == "OK") - { - return obj.Response; - } - - throw new ApiException(obj.Code ?? 0, (int)statusCode, obj.Message, obj.MessageDetail); - } - catch (ApiException) - { - throw; - } - catch (Exception e) - { - throw new BadResponseException((int)statusCode, e); - } - } - - private int? ToNullableInt(string s) - { - int i; - if (int.TryParse(s, out i)) - { - return i; - } - return null; - } - - private string HmacSign(string data) - { - var keyBytes = Encoding.ASCII.GetBytes(_skey); - var dataBytes = Encoding.ASCII.GetBytes(data); - - using (var hmac = new HMACSHA1(keyBytes)) - { - var hash = hmac.ComputeHash(dataBytes); - var hex = BitConverter.ToString(hash); - return hex.Replace("-", string.Empty).ToLower(); - } - } - - private static string Encode64(string plaintext) - { - var plaintextBytes = Encoding.ASCII.GetBytes(plaintext); - return Convert.ToBase64String(plaintextBytes); - } - - private static string RFC822UtcNow() - { - // Can't use the "zzzz" format because it adds a ":" - // between the offset's hours and minutes. - var dateString = DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture); - var offset = 0; - var zone = "+" + offset.ToString(CultureInfo.InvariantCulture).PadLeft(2, '0'); - dateString += " " + zone.PadRight(5, '0'); - return dateString; - } -} - -public class DuoException : Exception -{ - public int HttpStatus { get; private set; } - - public DuoException(string message, Exception inner) - : base(message, inner) - { } - - public DuoException(int httpStatus, string message, Exception inner) - : base(message, inner) - { - HttpStatus = httpStatus; - } -} - -public class ApiException : DuoException -{ - public int Code { get; private set; } - public string ApiMessage { get; private set; } - public string ApiMessageDetail { get; private set; } - - public ApiException(int code, int httpStatus, string apiMessage, string apiMessageDetail) - : base(httpStatus, FormatMessage(code, apiMessage, apiMessageDetail), null) - { - Code = code; - ApiMessage = apiMessage; - ApiMessageDetail = apiMessageDetail; - } - - private static string FormatMessage(int code, string apiMessage, string apiMessageDetail) - { - return string.Format("Duo API Error {0}: '{1}' ('{2}')", code, apiMessage, apiMessageDetail); - } -} - -public class BadResponseException : DuoException -{ - public BadResponseException(int httpStatus, Exception inner) - : base(httpStatus, FormatMessage(httpStatus, inner), inner) - { } - - private static string FormatMessage(int httpStatus, Exception inner) - { - var innerMessage = "(null)"; - if (inner != null) - { - innerMessage = string.Format("'{0}'", inner.Message); - } - return string.Format("Got error {0} with HTTP Status {1}", innerMessage, httpStatus); - } -} diff --git a/src/Core/Auth/Utilities/DuoUtilities.cs b/src/Core/Auth/Utilities/DuoUtilities.cs new file mode 100644 index 000000000000..b84334be2487 --- /dev/null +++ b/src/Core/Auth/Utilities/DuoUtilities.cs @@ -0,0 +1,16 @@ +namespace Bit.Core.Auth.Utilities; + +public class DuoUtilities +{ + public static bool ValidHost(string host) + { + if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri)) + { + return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") && + uri.Host.StartsWith("api-") && + (uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com")); + } + throw new ArgumentException("Invalid Duo host configured.", nameof(host)); + } +} + diff --git a/src/Core/Auth/Utilities/DuoWeb.cs b/src/Core/Auth/Utilities/DuoWeb.cs deleted file mode 100644 index 98fa974ab28a..000000000000 --- a/src/Core/Auth/Utilities/DuoWeb.cs +++ /dev/null @@ -1,240 +0,0 @@ -/* -Original source modified from https://github.com/duosecurity/duo_dotnet - -============================================================================= -============================================================================= - -ref: https://github.com/duosecurity/duo_dotnet/blob/master/LICENSE - -Copyright (c) 2011, Duo Security, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -using System.Security.Cryptography; -using System.Text; - -namespace Bit.Core.Auth.Utilities.Duo; - -public static class DuoWeb -{ - private const string DuoProfix = "TX"; - private const string AppPrefix = "APP"; - private const string AuthPrefix = "AUTH"; - private const int DuoExpire = 300; - private const int AppExpire = 3600; - private const int IKeyLength = 20; - private const int SKeyLength = 40; - private const int AKeyLength = 40; - - public static string ErrorUser = "ERR|The username passed to sign_request() is invalid."; - public static string ErrorIKey = "ERR|The Duo integration key passed to sign_request() is invalid."; - public static string ErrorSKey = "ERR|The Duo secret key passed to sign_request() is invalid."; - public static string ErrorAKey = "ERR|The application secret key passed to sign_request() must be at least " + - "40 characters."; - public static string ErrorUnknown = "ERR|An unknown error has occurred."; - - // throw on invalid bytes - private static Encoding _encoding = new UTF8Encoding(false, true); - private static DateTime _epoc = new DateTime(1970, 1, 1); - - /// - /// Generate a signed request for Duo authentication. - /// The returned value should be passed into the Duo.init() call - /// in the rendered web page used for Duo authentication. - /// - /// Duo integration key - /// Duo secret key - /// Application secret key - /// Primary-authenticated username - /// (optional) The current UTC time - /// signed request - public static string SignRequest(string ikey, string skey, string akey, string username, - DateTime? currentTime = null) - { - string duoSig; - string appSig; - - var currentTimeValue = currentTime ?? DateTime.UtcNow; - - if (username == string.Empty) - { - return ErrorUser; - } - if (username.Contains("|")) - { - return ErrorUser; - } - if (ikey.Length != IKeyLength) - { - return ErrorIKey; - } - if (skey.Length != SKeyLength) - { - return ErrorSKey; - } - if (akey.Length < AKeyLength) - { - return ErrorAKey; - } - - try - { - duoSig = SignVals(skey, username, ikey, DuoProfix, DuoExpire, currentTimeValue); - appSig = SignVals(akey, username, ikey, AppPrefix, AppExpire, currentTimeValue); - } - catch - { - return ErrorUnknown; - } - - return $"{duoSig}:{appSig}"; - } - - /// - /// Validate the signed response returned from Duo. - /// Returns the username of the authenticated user, or null. - /// - /// Duo integration key - /// Duo secret key - /// Application secret key - /// The signed response POST'ed to the server - /// (optional) The current UTC time - /// authenticated username, or null - public static string VerifyResponse(string ikey, string skey, string akey, string sigResponse, - DateTime? currentTime = null) - { - string authUser = null; - string appUser = null; - var currentTimeValue = currentTime ?? DateTime.UtcNow; - - try - { - var sigs = sigResponse.Split(':'); - var authSig = sigs[0]; - var appSig = sigs[1]; - - authUser = ParseVals(skey, authSig, AuthPrefix, ikey, currentTimeValue); - appUser = ParseVals(akey, appSig, AppPrefix, ikey, currentTimeValue); - } - catch - { - return null; - } - - if (authUser != appUser) - { - return null; - } - - return authUser; - } - - private static string SignVals(string key, string username, string ikey, string prefix, long expire, - DateTime currentTime) - { - var ts = (long)(currentTime - _epoc).TotalSeconds; - expire = ts + expire; - var val = $"{username}|{ikey}|{expire.ToString()}"; - var cookie = $"{prefix}|{Encode64(val)}"; - var sig = Sign(key, cookie); - return $"{cookie}|{sig}"; - } - - private static string ParseVals(string key, string val, string prefix, string ikey, DateTime currentTime) - { - var ts = (long)(currentTime - _epoc).TotalSeconds; - - var parts = val.Split('|'); - if (parts.Length != 3) - { - return null; - } - - var uPrefix = parts[0]; - var uB64 = parts[1]; - var uSig = parts[2]; - - var sig = Sign(key, $"{uPrefix}|{uB64}"); - if (Sign(key, sig) != Sign(key, uSig)) - { - return null; - } - - if (uPrefix != prefix) - { - return null; - } - - var cookie = Decode64(uB64); - var cookieParts = cookie.Split('|'); - if (cookieParts.Length != 3) - { - return null; - } - - var username = cookieParts[0]; - var uIKey = cookieParts[1]; - var expire = cookieParts[2]; - - if (uIKey != ikey) - { - return null; - } - - var expireTs = Convert.ToInt32(expire); - if (ts >= expireTs) - { - return null; - } - - return username; - } - - private static string Sign(string skey, string data) - { - var keyBytes = Encoding.ASCII.GetBytes(skey); - var dataBytes = Encoding.ASCII.GetBytes(data); - - using (var hmac = new HMACSHA1(keyBytes)) - { - var hash = hmac.ComputeHash(dataBytes); - var hex = BitConverter.ToString(hash); - return hex.Replace("-", "").ToLower(); - } - } - - private static string Encode64(string plaintext) - { - var plaintextBytes = _encoding.GetBytes(plaintext); - return Convert.ToBase64String(plaintextBytes); - } - - private static string Decode64(string encoded) - { - var plaintextBytes = Convert.FromBase64String(encoded); - return _encoding.GetString(plaintextBytes); - } -} From 34b4a12883eda7503e8c1c484e920498d92d85be Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Fri, 27 Sep 2024 11:55:28 -0700 Subject: [PATCH 03/25] fixed tests --- .../TwoFactor/TwoFactorDuoResponseModel.cs | 10 +-- src/Core/Auth/Identity/DuoTokenProvider.cs | 10 --- .../Identity/DuoUniversalTokenProvider.cs | 3 +- .../OrganizationDuoUniversalTokenProvider.cs | 5 +- src/Core/Auth/Utilities/DuoUtilities.cs | 16 ++++- ...ganizationTwoFactorDuoRequestModelTests.cs | 60 ----------------- ...TwoFactorDuoRequestModelValidationTests.cs | 67 ------------------- .../UserTwoFactorDuoRequestModelTests.cs | 60 ----------------- ...anizationTwoFactorDuoResponseModelTests.cs | 57 +++------------- .../UserTwoFactorDuoResponseModelTests.cs | 57 +++------------- .../Auth/Utilities/DuoUtilitiesTests.cs | 39 +++++++++++ 11 files changed, 84 insertions(+), 300 deletions(-) delete mode 100644 test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs create mode 100644 test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs index d17ee7c8e03d..dbdc66d6d8f1 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs @@ -45,7 +45,7 @@ private void Build(TwoFactorProvider provider) } if (provider.MetaData.TryGetValue("ClientSecret", out var clientSecret)) { - ClientSecret = MaskKey((string)clientSecret); + ClientSecret = MaskSecret((string)clientSecret); } if (provider.MetaData.TryGetValue("ClientId", out var clientId)) { @@ -58,14 +58,14 @@ private void Build(TwoFactorProvider provider) } } - private static string MaskKey(string key) + private static string MaskSecret(string secret) { - if (string.IsNullOrWhiteSpace(key) || key.Length <= 6) + if (string.IsNullOrWhiteSpace(secret) || secret.Length <= 6) { - return key; + return secret; } // Mask all but the first 6 characters. - return string.Concat(key.AsSpan(0, 6), new string('*', key.Length - 6)); + return string.Concat(secret.AsSpan(0, 6), new string('*', secret.Length - 6)); } } diff --git a/src/Core/Auth/Identity/DuoTokenProvider.cs b/src/Core/Auth/Identity/DuoTokenProvider.cs index 69fa78b0fc1b..384106668e84 100644 --- a/src/Core/Auth/Identity/DuoTokenProvider.cs +++ b/src/Core/Auth/Identity/DuoTokenProvider.cs @@ -1,6 +1,5 @@ using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Settings; @@ -33,15 +32,6 @@ public DuoTokenProvider(ICurrentContext currentContext, GlobalSettings globalSet _globalSettings = globalSettings; } - protected bool HasProperMetaData(TwoFactorProvider provider) - { - return provider?.MetaData != null && - provider.MetaData.ContainsKey("ClientId") && - provider.MetaData.ContainsKey("ClientSecret") && - provider.MetaData.ContainsKey("Host") && - DuoUtilities.ValidHost((string)provider.MetaData["Host"]); - } - protected async Task GenerateAuthUrlAsync( TwoFactorProvider provider, IDataProtectorTokenFactory tokenDataFactory, diff --git a/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs index 6f1344f63b71..4a0e1e861289 100644 --- a/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs @@ -1,6 +1,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Services; @@ -69,7 +70,7 @@ private async Task GetTwoFactorProvideAsync(User user) } var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) + if (!DuoUtilities.HasProperDuoMetadata(provider)) { return null; } diff --git a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs index df52946d0786..a1aa0e02d9bf 100644 --- a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Settings; @@ -33,7 +34,7 @@ public Task CanGenerateTwoFactorTokenAsync(Organization organization) var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo) - && HasProperMetaData(provider); + && DuoUtilities.HasProperDuoMetadata(provider);; return Task.FromResult(canGenerate); } @@ -69,7 +70,7 @@ private TwoFactorProvider GetTwoFactorProvider(Organization organization) } var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - if (!HasProperMetaData(provider)) + if (!DuoUtilities.HasProperDuoMetadata(provider)) { return null; } diff --git a/src/Core/Auth/Utilities/DuoUtilities.cs b/src/Core/Auth/Utilities/DuoUtilities.cs index b84334be2487..d34fd598cc7d 100644 --- a/src/Core/Auth/Utilities/DuoUtilities.cs +++ b/src/Core/Auth/Utilities/DuoUtilities.cs @@ -1,8 +1,19 @@ -namespace Bit.Core.Auth.Utilities; +using Bit.Core.Auth.Models; + +namespace Bit.Core.Auth.Utilities; public class DuoUtilities { - public static bool ValidHost(string host) + public static bool HasProperDuoMetadata(TwoFactorProvider provider) + { + return provider?.MetaData != null && + provider.MetaData.ContainsKey("ClientId") && + provider.MetaData.ContainsKey("ClientSecret") && + provider.MetaData.ContainsKey("Host") && + ValidDuoHost((string)provider.MetaData["Host"]); + } + + public static bool ValidDuoHost(string host) { if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri)) { @@ -13,4 +24,3 @@ public static bool ValidHost(string host) throw new ArgumentException("Invalid Duo host configured.", nameof(host)); } } - diff --git a/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs index 5fbaf88671c5..361adea536d8 100644 --- a/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs +++ b/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs @@ -18,8 +18,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( { ClientId = "clientId", ClientSecret = "clientSecret", - IntegrationKey = "integrationKey", - SecretKey = "secretKey", Host = "example.com" }; @@ -30,8 +28,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); } @@ -49,8 +45,6 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() { ClientId = "newClientId", ClientSecret = "newClientSecret", - IntegrationKey = "newIntegrationKey", - SecretKey = "newSecretKey", Host = "newExample.com" }; @@ -61,61 +55,7 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); Assert.Equal("newExample.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); } - - [Fact] - public void DuoV2ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingOrg = new Organization(); - var model = new UpdateTwoFactorDuoRequestModel - { - IntegrationKey = "integrationKey", - SecretKey = "secretKey", - Host = "example.com" - }; - - // Act - var result = model.ToOrganization(existingOrg); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); - } - - [Fact] - public void DuoV4ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingOrg = new Organization(); - var model = new UpdateTwoFactorDuoRequestModel - { - ClientId = "clientId", - ClientSecret = "clientSecret", - Host = "example.com" - }; - - // Act - var result = model.ToOrganization(existingOrg); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); - } } diff --git a/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs b/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs deleted file mode 100644 index ab05a94f13fd..000000000000 --- a/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Bit.Api.Auth.Models.Request; -using Xunit; - -namespace Bit.Api.Test.Auth.Models.Request; - -public class TwoFactorDuoRequestModelValidationTests -{ - [Fact] - public void ShouldReturnValidationError_WhenHostIsInvalid() - { - // Arrange - var model = new UpdateTwoFactorDuoRequestModel - { - Host = "invalidHost", - ClientId = "clientId", - ClientSecret = "clientSecret", - }; - - // Act - var result = model.Validate(new ValidationContext(model)); - - // Assert - Assert.Single(result); - Assert.Equal("Host is invalid.", result.First().ErrorMessage); - Assert.Equal("Host", result.First().MemberNames.First()); - } - - [Fact] - public void ShouldReturnValidationError_WhenValuesAreInvalid() - { - // Arrange - var model = new UpdateTwoFactorDuoRequestModel - { - Host = "api-12345abc.duosecurity.com" - }; - - // Act - var result = model.Validate(new ValidationContext(model)); - - // Assert - Assert.Single(result); - Assert.Equal("Neither v2 or v4 values are valid.", result.First().ErrorMessage); - Assert.Contains("ClientId", result.First().MemberNames); - Assert.Contains("ClientSecret", result.First().MemberNames); - Assert.Contains("IntegrationKey", result.First().MemberNames); - Assert.Contains("SecretKey", result.First().MemberNames); - } - - [Fact] - public void ShouldReturnSuccess_WhenValuesAreValid() - { - // Arrange - var model = new UpdateTwoFactorDuoRequestModel - { - Host = "api-12345abc.duosecurity.com", - ClientId = "clientId", - ClientSecret = "clientSecret", - }; - - // Act - var result = model.Validate(new ValidationContext(model)); - - // Assert - Assert.Empty(result); - } -} diff --git a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs index 28dfc83a2de7..b35bc846f6a2 100644 --- a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs +++ b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs @@ -17,8 +17,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( { ClientId = "clientId", ClientSecret = "clientSecret", - IntegrationKey = "integrationKey", - SecretKey = "secretKey", Host = "example.com" }; @@ -30,8 +28,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); } @@ -49,8 +45,6 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() { ClientId = "newClientId", ClientSecret = "newClientSecret", - IntegrationKey = "newIntegrationKey", - SecretKey = "newSecretKey", Host = "newExample.com" }; @@ -62,61 +56,7 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); Assert.Equal("newExample.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); } - - [Fact] - public void DuoV2ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingUser = new User(); - var model = new UpdateTwoFactorDuoRequestModel - { - IntegrationKey = "integrationKey", - SecretKey = "secretKey", - Host = "example.com" - }; - - // Act - var result = model.ToUser(existingUser); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); - } - - [Fact] - public void DuoV4ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingUser = new User(); - var model = new UpdateTwoFactorDuoRequestModel - { - ClientId = "clientId", - ClientSecret = "clientSecret", - Host = "example.com" - }; - - // Act - var result = model.ToUser(existingUser); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); - } } diff --git a/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs index dea76b2cdbc8..3e81e3e5f545 100644 --- a/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs @@ -10,64 +10,40 @@ public class OrganizationTwoFactorDuoResponseModelTests { [Theory] [BitAutoData] - public void Organization_WithDuoV4_ShouldBuildModel(Organization organization) + public void Organization_WithDuo_ShouldBuildModel(Organization organization) { // Arrange - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoV4ProvidersJson(); + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProvidersJson(); // Act var model = new TwoFactorDuoResponseModel(organization); - // Assert if v4 data Ikey and Skey are set to clientId and clientSecret + /// Assert Even if both versions are present priority is given to v4 data Assert.NotNull(model); Assert.Equal("clientId", model.ClientId); Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); - } - - [Theory] - [BitAutoData] - public void Organization_WithDuoV2_ShouldBuildModel(Organization organization) - { - // Arrange - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoV2ProvidersJson(); - - // Act - var model = new TwoFactorDuoResponseModel(organization); - - // Assert if only v2 data clientId and clientSecret are set to Ikey and Sk - Assert.NotNull(model); - Assert.Equal("IKey", model.ClientId); - Assert.Equal("SKey", model.ClientSecret); - Assert.Equal("IKey", model.IntegrationKey); - Assert.Equal("SKey", model.SecretKey); } [Theory] [BitAutoData] - public void Organization_WithDuo_ShouldBuildModel(Organization organization) + public void Organization_WithDuoEmpty_ShouldFail(Organization organization) { // Arrange - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProvidersJson(); + organization.TwoFactorProviders = "{\"6\" : {}}"; // Act var model = new TwoFactorDuoResponseModel(organization); - /// Assert Even if both versions are present priority is given to v4 data - Assert.NotNull(model); - Assert.Equal("clientId", model.ClientId); - Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); + /// Assert + Assert.False(model.Enabled); } [Theory] [BitAutoData] - public void Organization_WithDuoEmpty_ShouldFail(Organization organization) + public void Organization_WithTwoFactorProvidersNull_ShouldFail(Organization organization) { // Arrange - organization.TwoFactorProviders = "{\"6\" : {}}"; + organization.TwoFactorProviders = null; // Act var model = new TwoFactorDuoResponseModel(organization); @@ -78,10 +54,10 @@ public void Organization_WithDuoEmpty_ShouldFail(Organization organization) [Theory] [BitAutoData] - public void Organization_WithTwoFactorProvidersNull_ShouldFail(Organization organization) + public void Organization_WithTwoFactorProvidersEmpty_ShouldFail(Organization organization) { // Arrange - organization.TwoFactorProviders = "{\"6\" : {}}"; + organization.TwoFactorProviders = "{}"; // Act var model = new TwoFactorDuoResponseModel(organization); @@ -91,19 +67,8 @@ public void Organization_WithTwoFactorProvidersNull_ShouldFail(Organization orga } private string GetTwoFactorOrganizationDuoProvidersJson() - { - return - "{\"6\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; - } - - private string GetTwoFactorOrganizationDuoV4ProvidersJson() { return "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - - private string GetTwoFactorOrganizationDuoV2ProvidersJson() - { - return "{\"6\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"Host\":\"example.com\"}}}"; - } } diff --git a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs index cb46273a60d2..32fd434c4c68 100644 --- a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs @@ -10,64 +10,40 @@ public class UserTwoFactorDuoResponseModelTests { [Theory] [BitAutoData] - public void User_WithDuoV4_ShouldBuildModel(User user) + public void User_WithDuo_ShouldBuildModel(User user) { // Arrange - user.TwoFactorProviders = GetTwoFactorDuoV4ProvidersJson(); + user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); // Act var model = new TwoFactorDuoResponseModel(user); - // Assert if v4 data Ikey and Skey are set to clientId and clientSecret + // Assert Even if both versions are present priority is given to v4 data Assert.NotNull(model); Assert.Equal("clientId", model.ClientId); Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); } [Theory] [BitAutoData] - public void User_WithDuov2_ShouldBuildModel(User user) - { - // Arrange - user.TwoFactorProviders = GetTwoFactorDuoV2ProvidersJson(); - - // Act - var model = new TwoFactorDuoResponseModel(user); - - // Assert if only v2 data clientId and clientSecret are set to Ikey and Skey - Assert.NotNull(model); - Assert.Equal("IKey", model.ClientId); - Assert.Equal("SKey", model.ClientSecret); - Assert.Equal("IKey", model.IntegrationKey); - Assert.Equal("SKey", model.SecretKey); - } - - [Theory] - [BitAutoData] - public void User_WithDuo_ShouldBuildModel(User user) + public void User_WithDuoEmpty_ShouldFail(User user) { // Arrange - user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); + user.TwoFactorProviders = "{\"2\" : {}}"; // Act var model = new TwoFactorDuoResponseModel(user); - // Assert Even if both versions are present priority is given to v4 data - Assert.NotNull(model); - Assert.Equal("clientId", model.ClientId); - Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); + /// Assert + Assert.False(model.Enabled); } [Theory] [BitAutoData] - public void User_WithDuoEmpty_ShouldFail(User user) + public void User_WithTwoFactorProvidersNull_ShouldFail(User user) { // Arrange - user.TwoFactorProviders = "{\"2\" : {}}"; + user.TwoFactorProviders = null; // Act var model = new TwoFactorDuoResponseModel(user); @@ -78,10 +54,10 @@ public void User_WithDuoEmpty_ShouldFail(User user) [Theory] [BitAutoData] - public void User_WithTwoFactorProvidersNull_ShouldFail(User user) + public void User_WithTwoFactorProvidersEmpty_ShouldFail(User user) { // Arrange - user.TwoFactorProviders = null; + user.TwoFactorProviders = "{}"; // Act var model = new TwoFactorDuoResponseModel(user); @@ -91,19 +67,8 @@ public void User_WithTwoFactorProvidersNull_ShouldFail(User user) } private string GetTwoFactorDuoProvidersJson() - { - return - "{\"2\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; - } - - private string GetTwoFactorDuoV4ProvidersJson() { return "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - - private string GetTwoFactorDuoV2ProvidersJson() - { - return "{\"2\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"Host\":\"example.com\"}}}"; - } } diff --git a/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs new file mode 100644 index 000000000000..bd45fd4bc5e1 --- /dev/null +++ b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs @@ -0,0 +1,39 @@ +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Utilities; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Auth.Utilities; + +public class DuoUtilitiesTests +{ + [Theory] + [BitAutoData] + public void HasProperMetaData_ReturnsTrue(TwoFactorProvider twoFactorProvider) + { + twoFactorProvider.MetaData = DuoMetaData(); + var result = DuoUtilities.HasProperDuoMetadata(twoFactorProvider); + + Assert.True(result); + } + + [Theory] + [BitAutoData] + public void HasProperMetaData_ReturnsFalse(TwoFactorProvider twoFactorProvider) + { + twoFactorProvider.MetaData = null; + var result = DuoUtilities.HasProperDuoMetadata(twoFactorProvider); + + Assert.False(result); + } + + private Dictionary DuoMetaData() + { + return new() + { + { "ClientId", "clientId" }, + { "ClientSecret", "clientSecret" }, + { "Host", "api-abcd1234.duosecurity.com" } + }; + } +} From 966fe5f36cc645537cd0468aff8ac16068eae08b Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 30 Sep 2024 12:27:24 -0700 Subject: [PATCH 04/25] Testing Duo two factor implementation --- .../Auth/Controllers/TwoFactorController.cs | 41 +-- .../OrganizationDuoUniversalTokenProvider.cs | 2 +- src/Core/Auth/Utilities/DuoUtilities.cs | 2 +- .../Controllers/TwoFactorControllerTests.cs | 296 ++++++++++++++++++ .../UserTwoFactorDuoRequestModelTests.cs | 2 - .../UserTwoFactorDuoResponseModelTests.cs | 27 +- .../Attributes/BitCustomizeAttribute.cs | 4 +- .../Auth/Identity/BaseTokenProviderTests.cs | 3 + .../DuoTwoFactorTokenProviderTests.cs | 61 ++++ .../BaseRequestValidatorTests.cs | 9 +- .../BaseRequestValidatorTestWrapper.cs | 4 +- 11 files changed, 405 insertions(+), 46 deletions(-) create mode 100644 test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs create mode 100644 test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 33079c250657..84bea1ad7189 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -4,6 +4,7 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Response; using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; @@ -218,14 +219,8 @@ public async Task GetOrganizationDuo(string id, [FromBody] SecretVerificationRequestModel model) { await CheckAsync(model, false, true); + var organization = await CheckOrganizationAsync(new Guid(id)); - var orgIdGuid = new Guid(id); - if (!await _currentContext.ManagePolicies(orgIdGuid)) - { - throw new NotFoundException(); - } - - var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException(); var response = new TwoFactorDuoResponseModel(organization); return response; } @@ -236,14 +231,8 @@ public async Task PutOrganizationDuo(string id, [FromBody] UpdateTwoFactorDuoRequestModel model) { await CheckAsync(model, false); + var organization = await CheckOrganizationAsync(new Guid(id)); - var orgIdGuid = new Guid(id); - if (!await _currentContext.ManagePolicies(orgIdGuid)) - { - throw new NotFoundException(); - } - - var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException(); if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( @@ -395,19 +384,8 @@ public async Task PutDisable([FromBody] TwoFacto public async Task PutOrganizationDisable(string id, [FromBody] TwoFactorProviderRequestModel model) { - var user = await CheckAsync(model, false); - - var orgIdGuid = new Guid(id); - if (!await _currentContext.ManagePolicies(orgIdGuid)) - { - throw new NotFoundException(); - } - - var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); - if (organization == null) - { - throw new NotFoundException(); - } + await CheckAsync(model, false); + var organization = await CheckOrganizationAsync(new Guid(id)); await _organizationService.DisableTwoFactorProviderAsync(organization, model.Type.Value); var response = new TwoFactorProviderResponseModel(model.Type.Value, organization); @@ -449,6 +427,15 @@ public Task PutDeviceVerificationSettings( return Task.FromResult(new DeviceVerificationResponseModel(false, false)); } + private async Task CheckOrganizationAsync(Guid organizationId){ + if (!await _currentContext.ManagePolicies(organizationId)) + { + throw new NotFoundException(); + } + var organization = await _organizationRepository.GetByIdAsync(organizationId) ?? throw new NotFoundException(); + return organization; + } + private async Task CheckAsync(SecretVerificationRequestModel model, bool premium, bool skipVerification = false) { diff --git a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs index a1aa0e02d9bf..a43b59a1db93 100644 --- a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs @@ -34,7 +34,7 @@ public Task CanGenerateTwoFactorTokenAsync(Organization organization) var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo) - && DuoUtilities.HasProperDuoMetadata(provider);; + && DuoUtilities.HasProperDuoMetadata(provider); return Task.FromResult(canGenerate); } diff --git a/src/Core/Auth/Utilities/DuoUtilities.cs b/src/Core/Auth/Utilities/DuoUtilities.cs index d34fd598cc7d..ddeeb923f971 100644 --- a/src/Core/Auth/Utilities/DuoUtilities.cs +++ b/src/Core/Auth/Utilities/DuoUtilities.cs @@ -21,6 +21,6 @@ public static bool ValidDuoHost(string host) uri.Host.StartsWith("api-") && (uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com")); } - throw new ArgumentException("Invalid Duo host configured.", nameof(host)); + return false; } } diff --git a/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs new file mode 100644 index 000000000000..576ee629af5a --- /dev/null +++ b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs @@ -0,0 +1,296 @@ +using Xunit; +using NSubstitute; +using Bit.Api.Auth.Controllers; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.AutoFixture; +using Bit.Core.Services; +using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Core.Entities; +using Bit.Api.Auth.Models.Response.TwoFactor; +using Bit.Core.Exceptions; +using Bit.Api.Auth.Models.Request; +using Bit.Core.Auth.Identity; +using Bit.Core.Context; +using Bit.Core.Repositories; +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Api.Test.Auth.Controllers; + +[ControllerCustomize(typeof(TwoFactorController))] +[SutProviderCustomize] +public class TwoFactorControllerTests +{ + [Theory, BitAutoData] + public async Task CheckAsync_UserNull_ThrowsUnauthorizedException(SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(null as User); + + // Act + var result = () => sutProvider.Sut.GetDuo(request); + + // Assert + await Assert.ThrowsAsync(result); + + } + + [Theory, BitAutoData] + public async Task CheckAsync_BadSecret_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + + sutProvider.GetDependency() + .VerifySecretAsync(default, default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.GetDuo(request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("The model state is invalid.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task CheckAsync_CannotAccessPremium_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + + sutProvider.GetDependency() + .VerifySecretAsync(default, default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .CanAccessPremium(default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.GetDuo(request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("Premium status is required.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task GetDuo_Success(User user, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + // Act + var result = await sutProvider.Sut.GetDuo(request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PutDuo_InvalidConfiguration_ThrowsBadRequestException(User user, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.PutDuo(request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("Duo configuration settings are not valid. Please re-check the Duo Admin panel.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task PutDuo_Success(User user, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(true); + + // Act + var result = await sutProvider.Sut.PutDuo(request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(user.TwoFactorProviders, request.ToUser(user).TwoFactorProviders); + } + + [Theory, BitAutoData] + public async Task CheckOrganizationAsync_ManagePolicies_ThrowsNotFoundException( + User user, Organization organization, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ManagePolicies(default) + .ReturnsForAnyArgs(false); + + // Act + var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + public async Task CheckOrganizationAsync_GetByIdAsync_ThrowsNotFoundException( + User user, Organization organization, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ManagePolicies(default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .GetByIdAsync(default) + .ReturnsForAnyArgs(null as Organization); + + // Act + var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + public async Task GetOrganizationDuo_Success( + User user, Organization organization, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + SetupCheckOrganizationAsyncToPass(sutProvider, organization); + + // Act + var result = await sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PutOrganizationDuo_InvalidConfiguration_ThrowsBadRequestException( + User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + SetupCheckAsyncToPass(sutProvider, user); + SetupCheckOrganizationAsyncToPass(sutProvider, organization); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("Duo configuration settings are not valid. Please re-check the Duo Admin panel.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task PutOrganizationDuo_Success( + User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + SetupCheckAsyncToPass(sutProvider, user); + SetupCheckOrganizationAsyncToPass(sutProvider, organization); + organization.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson(); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(true); + + // Act + var result = + await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(organization.TwoFactorProviders, request.ToOrganization(organization).TwoFactorProviders); + } + + + private string GetUserTwoFactorDuoProvidersJson() + { + return + "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + private string GetOrganizationTwoFactorDuoProvidersJson() + { + return + "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + /// + /// Sets up the CheckAsync method to pass. + /// + /// uses bit auto data + /// uses bit auto data + private void SetupCheckAsyncToPass(SutProvider sutProvider, User user) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + + sutProvider.GetDependency() + .VerifySecretAsync(default, default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .CanAccessPremium(default) + .ReturnsForAnyArgs(true); + } + + private void SetupCheckOrganizationAsyncToPass(SutProvider sutProvider, Organization organization){ + sutProvider.GetDependency() + .ManagePolicies(default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .GetByIdAsync(default) + .ReturnsForAnyArgs(organization); + } +} diff --git a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs index b35bc846f6a2..56c9af1e0d7e 100644 --- a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs +++ b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs @@ -24,7 +24,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( var result = model.ToUser(existingUser); // Assert - // IKey and SKey should be the same as ClientId and ClientSecret Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); @@ -52,7 +51,6 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() var result = model.ToUser(existingUser); // Assert - // IKey and SKey should be the same as ClientId and ClientSecret Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); diff --git a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs index 32fd434c4c68..699bbeb33d2b 100644 --- a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs @@ -8,6 +8,25 @@ namespace Bit.Api.Test.Auth.Models.Response; public class UserTwoFactorDuoResponseModelTests { + [Theory] + [BitAutoData] + public void User_WithDuo_UserNull_ThrowsArgumentException(User user) + { + // Arrange + user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); + + // Act + try + { + var model = new TwoFactorDuoResponseModel(null as User); + } + catch (ArgumentNullException e) + { + // Assert + Assert.Equal("Value cannot be null. (Parameter 'user')", e.Message); + } + } + [Theory] [BitAutoData] public void User_WithDuo_ShouldBuildModel(User user) @@ -18,7 +37,7 @@ public void User_WithDuo_ShouldBuildModel(User user) // Act var model = new TwoFactorDuoResponseModel(user); - // Assert Even if both versions are present priority is given to v4 data + // Assert Assert.NotNull(model); Assert.Equal("clientId", model.ClientId); Assert.Equal("secret************", model.ClientSecret); @@ -34,7 +53,7 @@ public void User_WithDuoEmpty_ShouldFail(User user) // Act var model = new TwoFactorDuoResponseModel(user); - /// Assert + // Assert Assert.False(model.Enabled); } @@ -48,7 +67,7 @@ public void User_WithTwoFactorProvidersNull_ShouldFail(User user) // Act var model = new TwoFactorDuoResponseModel(user); - /// Assert + // Assert Assert.False(model.Enabled); } @@ -62,7 +81,7 @@ public void User_WithTwoFactorProvidersEmpty_ShouldFail(User user) // Act var model = new TwoFactorDuoResponseModel(user); - /// Assert + // Assert Assert.False(model.Enabled); } diff --git a/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs b/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs index 105a6632d893..e8a88c684838 100644 --- a/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs +++ b/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs @@ -13,8 +13,8 @@ namespace Bit.Test.Common.AutoFixture.Attributes; public abstract class BitCustomizeAttribute : Attribute { /// - /// /// Gets a customization for the method's parameters. + /// Gets a customization for the method's parameters. /// - /// A customization for the method's paramters. + /// A customization for the method's parameters. public abstract ICustomization GetCustomization(); } diff --git a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs index b90f71ae71bc..12620ed05509 100644 --- a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs @@ -48,6 +48,9 @@ protected virtual void SetupUserService(IUserService userService, User user) userService .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType, user) .Returns(true); + userService + .CanAccessPremium(user) + .Returns(true); } protected static UserManager SubstituteUserManager() diff --git a/test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs new file mode 100644 index 000000000000..0ae9af91635a --- /dev/null +++ b/test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs @@ -0,0 +1,61 @@ +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; +using Bit.Core.Entities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Auth.Identity; + +public class DuoTwoFactorTokenProviderTests : BaseTokenProviderTests +{ + public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Duo; + + public static IEnumerable CanGenerateTwoFactorTokenAsyncData + => SetupCanGenerateData( + ( + new Dictionary + { + ["ClientId"] = "ClientId", + ["ClientSecret"] = "ClientSecret", + ["Host"] = "api-abcd1234.duosecurity.com", + }, + true + ), + ( + new Dictionary + { + ["ClientId"] = "ClientId", + ["ClientSecret"] = "ClientSecret", + ["Host"] = "api-abcd1234.duofederal.com", + }, + true + ), + ( + new Dictionary + { + ["ClientId"] = "ClientId", + ["ClientSecret"] = "ClientSecret", + ["Host"] = "", + }, + false + ), + ( + new Dictionary + { + ["ClientSecret"] = "ClientSecret", + ["Host"] = "api-abcd1234.duofederal.com", + }, + false + ) + ); + + [Theory, BitMemberAutoData(nameof(CanGenerateTwoFactorTokenAsyncData))] + public override async Task RunCanGenerateTwoFactorTokenAsync(Dictionary metaData, bool expectedResponse, + User user, SutProvider sutProvider) + { + user.Premium = true; + user.PremiumExpirationDate = DateTime.UtcNow.AddDays(1); + await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider); + } +} diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index c1d34e1b047e..1086524a0327 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -33,8 +33,7 @@ public class BaseRequestValidatorTests private readonly IDeviceService _deviceService; private readonly IUserService _userService; private readonly IEventService _eventService; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; + private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IApplicationCacheService _applicationCacheService; @@ -57,8 +56,7 @@ public BaseRequestValidatorTests() _deviceService = Substitute.For(); _userService = Substitute.For(); _eventService = Substitute.For(); - _organizationDuoWebTokenProvider = Substitute.For(); - _duoWebV4SDKService = Substitute.For(); + _organizationDuoUniversalTokenProvider = Substitute.For(); _organizationRepository = Substitute.For(); _organizationUserRepository = Substitute.For(); _applicationCacheService = Substitute.For(); @@ -80,8 +78,7 @@ public BaseRequestValidatorTests() _deviceService, _userService, _eventService, - _organizationDuoWebTokenProvider, - _duoWebV4SDKService, + _organizationDuoUniversalTokenProvider, _organizationRepository, _organizationUserRepository, _applicationCacheService, diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index e525d0de764c..c7b0b9716018 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -55,8 +55,7 @@ public BaseRequestValidatorTestWrapper( IDeviceService deviceService, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IApplicationCacheService applicationCacheService, @@ -77,7 +76,6 @@ public BaseRequestValidatorTestWrapper( userService, eventService, organizationDuoWebTokenProvider, - duoWebV4SDKService, organizationRepository, organizationUserRepository, applicationCacheService, From 55105fed49953460c83bd84cf8a39e73bc7c5c2b Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 30 Sep 2024 13:33:33 -0700 Subject: [PATCH 05/25] Formatting --- .../Auth/Controllers/TwoFactorController.cs | 7 +++-- src/Core/Auth/Utilities/DuoUtilities.cs | 4 +-- .../Controllers/TwoFactorControllerTests.cs | 28 +++++++++---------- .../UserTwoFactorDuoResponseModelTests.cs | 2 +- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 84bea1ad7189..2bc34f4abe36 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -202,7 +202,7 @@ public async Task GetDuo([FromBody] SecretVerificatio public async Task PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model) { var user = await CheckAsync(model, true); - if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) + if (!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); @@ -233,7 +233,7 @@ public async Task PutOrganizationDuo(string id, await CheckAsync(model, false); var organization = await CheckOrganizationAsync(new Guid(id)); - if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) + if (!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); @@ -427,7 +427,8 @@ public Task PutDeviceVerificationSettings( return Task.FromResult(new DeviceVerificationResponseModel(false, false)); } - private async Task CheckOrganizationAsync(Guid organizationId){ + private async Task CheckOrganizationAsync(Guid organizationId) + { if (!await _currentContext.ManagePolicies(organizationId)) { throw new NotFoundException(); diff --git a/src/Core/Auth/Utilities/DuoUtilities.cs b/src/Core/Auth/Utilities/DuoUtilities.cs index ddeeb923f971..8db23fa15f71 100644 --- a/src/Core/Auth/Utilities/DuoUtilities.cs +++ b/src/Core/Auth/Utilities/DuoUtilities.cs @@ -6,9 +6,9 @@ public class DuoUtilities { public static bool HasProperDuoMetadata(TwoFactorProvider provider) { - return provider?.MetaData != null && + return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") && - provider.MetaData.ContainsKey("ClientSecret") && + provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host") && ValidDuoHost((string)provider.MetaData["Host"]); } diff --git a/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs index 576ee629af5a..8b3faebd74c0 100644 --- a/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs @@ -1,18 +1,18 @@ -using Xunit; -using NSubstitute; -using Bit.Api.Auth.Controllers; -using Bit.Test.Common.AutoFixture.Attributes; -using Bit.Test.Common.AutoFixture; -using Bit.Core.Services; +using Bit.Api.Auth.Controllers; +using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Core.Entities; using Bit.Api.Auth.Models.Response.TwoFactor; -using Bit.Core.Exceptions; -using Bit.Api.Auth.Models.Request; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Identity; using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; namespace Bit.Api.Test.Auth.Controllers; @@ -33,7 +33,6 @@ public async Task CheckAsync_UserNull_ThrowsUnauthorizedException(SecretVerifica // Assert await Assert.ThrowsAsync(result); - } [Theory, BitAutoData] @@ -175,7 +174,7 @@ public async Task CheckOrganizationAsync_GetByIdAsync_ThrowsNotFoundException( sutProvider.GetDependency() .ManagePolicies(default) .ReturnsForAnyArgs(true); - + sutProvider.GetDependency() .GetByIdAsync(default) .ReturnsForAnyArgs(null as Organization); @@ -242,7 +241,7 @@ public async Task PutOrganizationDuo_Success( .ReturnsForAnyArgs(true); // Act - var result = + var result = await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request); // Assert @@ -284,7 +283,8 @@ private void SetupCheckAsyncToPass(SutProvider sutProvider, .ReturnsForAnyArgs(true); } - private void SetupCheckOrganizationAsyncToPass(SutProvider sutProvider, Organization organization){ + private void SetupCheckOrganizationAsyncToPass(SutProvider sutProvider, Organization organization) + { sutProvider.GetDependency() .ManagePolicies(default) .ReturnsForAnyArgs(true); diff --git a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs index 699bbeb33d2b..9d4e961da466 100644 --- a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs @@ -22,7 +22,7 @@ public void User_WithDuo_UserNull_ThrowsArgumentException(User user) } catch (ArgumentNullException e) { - // Assert + // Assert Assert.Equal("Value cannot be null. (Parameter 'user')", e.Message); } } From 8ed411e78ba218fa651cf25009947ef3044d7ebd Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 30 Sep 2024 14:50:25 -0700 Subject: [PATCH 06/25] formatting --- src/Core/Auth/Identity/DuoTokenProvider.cs | 4 ++-- test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core/Auth/Identity/DuoTokenProvider.cs b/src/Core/Auth/Identity/DuoTokenProvider.cs index 384106668e84..2bceb889b3d9 100644 --- a/src/Core/Auth/Identity/DuoTokenProvider.cs +++ b/src/Core/Auth/Identity/DuoTokenProvider.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Entities; @@ -125,4 +125,4 @@ public async Task ValidateDuoConfiguration(string clientId, string clientS return await client.DoHealthCheck(false); } -} \ No newline at end of file +} diff --git a/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs index bd45fd4bc5e1..4ee9f44ae3f4 100644 --- a/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs +++ b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models; using Bit.Core.Auth.Utilities; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; From ed66e44336d6489504ec714eea3b8eb71139254a Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Wed, 2 Oct 2024 15:30:44 -0700 Subject: [PATCH 07/25] initial device removal --- src/Identity/IdentityServer/BaseRequestValidator.cs | 2 +- src/Identity/IdentityServer/DeviceValidator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 8129a1a10ec6..8adee11dc0fb 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -573,7 +573,7 @@ private async Task GetMasterPasswordPolicy(Us return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user)); } -#nullable enable + #nullable enable /// /// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents /// diff --git a/src/Identity/IdentityServer/DeviceValidator.cs b/src/Identity/IdentityServer/DeviceValidator.cs index bf3e6d7dd81f..b8b6356a4ec8 100644 --- a/src/Identity/IdentityServer/DeviceValidator.cs +++ b/src/Identity/IdentityServer/DeviceValidator.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core.Context; using Bit.Core.Entities; From 5b027ed1731131cfdc9ebfd05902542eb0c64391 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Wed, 2 Oct 2024 17:37:18 -0700 Subject: [PATCH 08/25] Unit Testing --- src/Identity/IdentityServer/DeviceValidator.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Identity/IdentityServer/DeviceValidator.cs b/src/Identity/IdentityServer/DeviceValidator.cs index b8b6356a4ec8..a1d3733c4e53 100644 --- a/src/Identity/IdentityServer/DeviceValidator.cs +++ b/src/Identity/IdentityServer/DeviceValidator.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core.Context; using Bit.Core.Entities; @@ -41,6 +41,12 @@ public class DeviceValidator( private readonly IMailService _mailService = mailService; private readonly ICurrentContext _currentContext = currentContext; + /// + /// Save a device to the database. If the device is already known, it will be returned. + /// + /// The user is assumed NOT null, still going to check though + /// Duende Validated Request that contains the data to create the device object + /// Returns null if user or device is malformed; The existing device if already in DB; a new device login public async Task SaveDeviceAsync(User user, ValidatedTokenRequest request) { var device = GetDeviceFromRequest(request); From 07264af0d06aaa6c0b19a544e42730cc22220d0c Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Thu, 3 Oct 2024 20:11:56 -0700 Subject: [PATCH 09/25] Finalized tests --- src/Identity/IdentityServer/BaseRequestValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 8adee11dc0fb..8129a1a10ec6 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -573,7 +573,7 @@ private async Task GetMasterPasswordPolicy(Us return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user)); } - #nullable enable +#nullable enable /// /// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents /// From 74308025ad10e78fd6889bf58b5d48723190383d Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Thu, 10 Oct 2024 21:55:18 -0700 Subject: [PATCH 10/25] initial commit refactoring two factor --- .../IdentityServer/BaseRequestValidator.cs | 296 +++--------------- .../CustomTokenRequestValidator.cs | 34 +- .../ResourceOwnerPasswordValidator.cs | 29 +- .../IdentityServer/TwoFactorValidator.cs | 270 ++++++++++++++++ .../IdentityServer/WebAuthnGrantValidator.cs | 34 +- .../Utilities/ServiceCollectionExtensions.cs | 1 + .../ResourceOwnerPasswordValidatorTests.cs | 5 + .../BaseRequestValidatorTests.cs | 65 ++-- .../BaseRequestValidatorTestWrapper.cs | 25 +- 9 files changed, 394 insertions(+), 365 deletions(-) create mode 100644 src/Identity/IdentityServer/TwoFactorValidator.cs diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 8129a1a10ec6..b5a4cd8fd776 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -1,15 +1,10 @@ using System.Security.Claims; -using System.Text.Json; using Bit.Core; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Api.Response; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -17,11 +12,9 @@ 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; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Core.Utilities; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; @@ -33,16 +26,12 @@ public abstract class BaseRequestValidator where T : class private UserManager _userManager; private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; - private readonly IOrganizationRepository _organizationRepository; + private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IApplicationCacheService _applicationCacheService; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; - private readonly IDataProtectorTokenFactory _tokenDataFactory; protected ICurrentContext CurrentContext { get; } protected IPolicyService PolicyService { get; } @@ -56,18 +45,14 @@ public BaseRequestValidator( IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) @@ -76,18 +61,14 @@ public BaseRequestValidator( _userService = userService; _eventService = eventService; _deviceValidator = deviceValidator; - _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; - _duoWebV4SDKService = duoWebV4SDKService; - _organizationRepository = organizationRepository; + _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator; _organizationUserRepository = organizationUserRepository; - _applicationCacheService = applicationCacheService; _mailService = mailService; _logger = logger; CurrentContext = currentContext; _globalSettings = globalSettings; PolicyService = policyService; _userRepository = userRepository; - _tokenDataFactory = tokenDataFactory; FeatureService = featureService; SsoConfigRepository = ssoConfigRepository; UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder; @@ -104,12 +85,6 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, request.UserName, validatorContext.CaptchaResponse.Score); } - var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString(); - var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); - var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; - var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && - !string.IsNullOrWhiteSpace(twoFactorProvider); - var valid = await ValidateContextAsync(context, validatorContext); var user = validatorContext.User; if (!valid) @@ -123,17 +98,32 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, return; } - var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request); + var (isTwoFactorRequired, twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request); + var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString(); + var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); + var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; + var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && + !string.IsNullOrWhiteSpace(twoFactorProvider); + if (isTwoFactorRequired) { - if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) + // 2FA required and not provided response + if (!validTwoFactorRequest || + !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) { - await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); + var resultDict = await _twoFactorAuthenticationValidator + .BuildTwoFactorResultAsync(user, twoFactorOrganization); + + // Include Master Password Policy in 2FA response + resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + SetTwoFactorResult(context, resultDict); return; } - var verified = await VerifyTwoFactor(user, twoFactorOrganization, - twoFactorProviderType, twoFactorToken); + var verified = await _twoFactorAuthenticationValidator + .VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); + + // 2FA required but request not valid or remember token expired response if (!verified || isBot) { if (twoFactorProviderType != TwoFactorProviderType.Remember) @@ -143,16 +133,20 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, } else if (twoFactorProviderType == TwoFactorProviderType.Remember) { - await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); + var resultDict = await _twoFactorAuthenticationValidator + .BuildTwoFactorResultAsync(user, twoFactorOrganization); + + // Include Master Password Policy in 2FA response + resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + SetTwoFactorResult(context, resultDict); } return; } } else { - twoFactorRequest = false; + validTwoFactorRequest = false; twoFactorRemember = false; - twoFactorToken = null; } // Force legacy users to the web for migration @@ -165,7 +159,6 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, } } - // Returns true if can finish validation process if (await IsValidAuthTypeAsync(user, request.GrantType)) { var device = await _deviceValidator.SaveDeviceAsync(user, request); @@ -174,8 +167,7 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, await BuildErrorResultAsync("No device information provided.", false, context, user); return; } - - await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember); + await BuildSuccessResultAsync(user, context, device, validTwoFactorRequest && twoFactorRemember); } else { @@ -238,67 +230,6 @@ protected async Task BuildSuccessResultAsync(User user, T context, Device device await SetSuccessResult(context, user, claims, customResponse); } - protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context) - { - var providerKeys = new List(); - var providers = new Dictionary>(); - - var enabledProviders = new List>(); - if (organization?.GetTwoFactorProviders() != null) - { - enabledProviders.AddRange(organization.GetTwoFactorProviders().Where( - p => organization.TwoFactorProviderIsEnabled(p.Key))); - } - - if (user.GetTwoFactorProviders() != null) - { - foreach (var p in user.GetTwoFactorProviders()) - { - if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user)) - { - enabledProviders.Add(p); - } - } - } - - if (!enabledProviders.Any()) - { - await BuildErrorResultAsync("No two-step providers enabled.", false, context, user); - return; - } - - foreach (var provider in enabledProviders) - { - providerKeys.Add((byte)provider.Key); - var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); - providers.Add(((byte)provider.Key).ToString(), infoDict); - } - - var twoFactorResultDict = new Dictionary - { - { "TwoFactorProviders", providers.Keys }, - { "TwoFactorProviders2", providers }, - { "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) }, - }; - - // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token - if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) - { - twoFactorResultDict.Add("SsoEmail2faSessionToken", - _tokenDataFactory.Protect(new SsoEmail2faSessionTokenable(user))); - - twoFactorResultDict.Add("Email", user.Email); - } - - SetTwoFactorResult(context, twoFactorResultDict); - - if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) - { - // Send email now if this is their only 2FA method - await _userService.SendTwoFactorEmailAsync(user); - } - } - protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user) { if (user != null) @@ -329,35 +260,13 @@ protected abstract Task SetSuccessResult(T context, User user, List claim protected abstract void SetErrorResult(T context, Dictionary customResponse); protected abstract ClaimsPrincipal GetSubject(T context); - protected virtual async Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) - { - if (request.GrantType == "client_credentials") - { - // Do not require MFA for api key logins - return new Tuple(false, null); - } - - var individualRequired = _userManager.SupportsUserTwoFactor && - await _userManager.GetTwoFactorEnabledAsync(user) && - (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; - - Organization firstEnabledOrg = null; - var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList(); - if (orgs.Count > 0) - { - var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); - var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id)); - if (twoFactorOrgs.Any()) - { - var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id); - firstEnabledOrg = userOrgs.FirstOrDefault( - o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled()); - } - } - - return new Tuple(individualRequired || firstEnabledOrg != null, firstEnabledOrg); - } - + /// + /// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are + /// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement. + /// + /// user trying to login + /// magic string identifying the grant type requested + /// private async Task IsValidAuthTypeAsync(User user, string grantType) { if (grantType == "authorization_code" || grantType == "client_credentials") @@ -367,7 +276,6 @@ private async Task IsValidAuthTypeAsync(User user, string grantType) return true; } - // 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) @@ -379,134 +287,6 @@ private async Task IsValidAuthTypeAsync(User user, string grantType) return true; } - private bool OrgUsing2fa(IDictionary orgAbilities, Guid orgId) - { - return orgAbilities != null && orgAbilities.ContainsKey(orgId) && - orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; - } - - private async Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, - string token) - { - switch (type) - { - case TwoFactorProviderType.Authenticator: - case TwoFactorProviderType.Email: - case TwoFactorProviderType.Duo: - case TwoFactorProviderType.YubiKey: - case TwoFactorProviderType.WebAuthn: - case TwoFactorProviderType.Remember: - if (type != TwoFactorProviderType.Remember && - !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) - { - return false; - } - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (type == TwoFactorProviderType.Duo) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - } - - return await _userManager.VerifyTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(type), token); - case TwoFactorProviderType.OrganizationDuo: - if (!organization?.TwoFactorProviderIsEnabled(type) ?? true) - { - return false; - } - - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (type == TwoFactorProviderType.OrganizationDuo) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - } - - return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user); - default: - return false; - } - } - - private async Task> BuildTwoFactorParams(Organization organization, User user, - TwoFactorProviderType type, TwoFactorProvider provider) - { - switch (type) - { - case TwoFactorProviderType.Duo: - case TwoFactorProviderType.WebAuthn: - case TwoFactorProviderType.Email: - case TwoFactorProviderType.YubiKey: - if (!await _userService.TwoFactorProviderIsEnabledAsync(type, user)) - { - return null; - } - - var token = await _userManager.GenerateTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(type)); - if (type == TwoFactorProviderType.Duo) - { - var duoResponse = new Dictionary - { - ["Host"] = provider.MetaData["Host"], - ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), - }; - - return duoResponse; - } - else if (type == TwoFactorProviderType.WebAuthn) - { - if (token == null) - { - return null; - } - - return JsonSerializer.Deserialize>(token); - } - else if (type == TwoFactorProviderType.Email) - { - var twoFactorEmail = (string)provider.MetaData["Email"]; - var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail); - return new Dictionary { ["Email"] = redactedEmail }; - } - else if (type == TwoFactorProviderType.YubiKey) - { - return new Dictionary { ["Nfc"] = (bool)provider.MetaData["Nfc"] }; - } - - return null; - case TwoFactorProviderType.OrganizationDuo: - if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) - { - var duoResponse = new Dictionary - { - ["Host"] = provider.MetaData["Host"], - ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), - }; - - return duoResponse; - } - return null; - default: - return null; - } - } - private async Task ResetFailedAuthDetailsAsync(User user) { // Early escape if db hit not necessary diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index 0d7a92c8af2f..c506ec8c84fc 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -29,28 +29,36 @@ public class CustomTokenRequestValidator : BaseRequestValidator userManager, - IDeviceValidator deviceValidator, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + IDeviceValidator deviceValidator, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, - ISsoConfigRepository ssoConfigRepository, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, - IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) - : base(userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, - userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, + ISsoConfigRepository ssoConfigRepository, + IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder + ) + : base( + userManager, + userService, + eventService, + deviceValidator, + twoFactorAuthenticationValidator, + organizationUserRepository, + mailService, + logger, + currentContext, + globalSettings, + userRepository, + policyService, + featureService, + ssoConfigRepository, userDecryptionOptionsBuilder) { _userManager = userManager; @@ -70,7 +78,7 @@ public async Task ValidateAsync(CustomTokenRequestValidationContext context) } } - string[] allowedGrantTypes = { "authorization_code", "client_credentials" }; + string[] allowedGrantTypes = ["authorization_code", "client_credentials"]; if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType) || context.Result.ValidatedRequest.ClientId.StartsWith("organization") || context.Result.ValidatedRequest.ClientId.StartsWith("installation") diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs index 08560e240d84..6e488bb48255 100644 --- a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -1,8 +1,6 @@ using System.Security.Claims; using Bit.Core; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Context; @@ -10,7 +8,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Core.Utilities; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; @@ -31,11 +28,8 @@ public ResourceOwnerPasswordValidator( IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, @@ -44,14 +38,25 @@ public ResourceOwnerPasswordValidator( IAuthRequestRepository authRequestRepository, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) - : base(userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, - tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) + : base( + userManager, + userService, + eventService, + deviceValidator, + twoFactorAuthenticationValidator, + organizationUserRepository, + mailService, + logger, + currentContext, + globalSettings, + userRepository, + policyService, + featureService, + ssoConfigRepository, + userDecryptionOptionsBuilder) { _userManager = userManager; _currentContext = currentContext; diff --git a/src/Identity/IdentityServer/TwoFactorValidator.cs b/src/Identity/IdentityServer/TwoFactorValidator.cs new file mode 100644 index 000000000000..361567290e94 --- /dev/null +++ b/src/Identity/IdentityServer/TwoFactorValidator.cs @@ -0,0 +1,270 @@ + +using System.Text.Json; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tokens; +using Bit.Core.Utilities; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Identity.IdentityServer; + +public interface ITwoFactorAuthenticationValidator +{ + Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request); + Task> BuildTwoFactorResultAsync(User user, Organization organization); + Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, string token); +} + +public class TwoFactorAuthenticationValidator( + IUserService userService, + UserManager userManager, + IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, + ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IFeatureService featureService, + IApplicationCacheService applicationCacheService, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IDataProtectorTokenFactory ssoEmail2faSessionTokeFactory, + ICurrentContext currentContext) : ITwoFactorAuthenticationValidator +{ + private readonly IUserService _userService = userService; + private readonly UserManager _userManager = userManager; + private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; + private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService = duoWebV4SDKService; + private readonly IFeatureService _featureService = featureService; + private readonly IApplicationCacheService _applicationCacheService = applicationCacheService; + private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository; + private readonly IOrganizationRepository _organizationRepository = organizationRepository; + private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory; + private readonly ICurrentContext _currentContext = currentContext; + + public async Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) + { + if (request.GrantType == "client_credentials" || request.GrantType == "webauthn") + { + /* + Do not require MFA for api key logins. + We consider Fido2 userVerification a second factor, so we don't require a second factor here. + */ + return new Tuple(false, null); + } + + var individualRequired = _userManager.SupportsUserTwoFactor && + await _userManager.GetTwoFactorEnabledAsync(user) && + (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; + + Organization firstEnabledOrg = null; + var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList(); + if (orgs.Count > 0) + { + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id)); + if (twoFactorOrgs.Any()) + { + var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id); + firstEnabledOrg = userOrgs.FirstOrDefault( + o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled()); + } + } + + return new Tuple(individualRequired || firstEnabledOrg != null, firstEnabledOrg); + } + + public async Task> BuildTwoFactorResultAsync(User user, Organization organization) + { + var providerKeys = new List(); + var providers = new Dictionary>(); + + var enabledProviders = new List>(); + if (organization?.GetTwoFactorProviders() != null) + { + enabledProviders.AddRange(organization.GetTwoFactorProviders().Where( + p => organization.TwoFactorProviderIsEnabled(p.Key))); + } + + if (user.GetTwoFactorProviders() != null) + { + foreach (var p in user.GetTwoFactorProviders()) + { + if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user)) + { + enabledProviders.Add(p); + } + } + } + + if (!enabledProviders.Any()) + { + return null; + // await BuildErrorResultAsync("No two-step providers enabled.", false, context, user); + } + + foreach (var provider in enabledProviders) + { + providerKeys.Add((byte)provider.Key); + var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); + providers.Add(((byte)provider.Key).ToString(), infoDict); + } + + var twoFactorResultDict = new Dictionary + { + { "TwoFactorProviders", providers.Keys }, + { "TwoFactorProviders2", providers }, + }; + + // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token + if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) + { + twoFactorResultDict.Add("SsoEmail2faSessionToken", + _ssoEmail2faSessionTokeFactory.Protect(new SsoEmail2faSessionTokenable(user))); + + twoFactorResultDict.Add("Email", user.Email); + } + + if (enabledProviders.Count == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) + { + // Send email now if this is their only 2FA method + await _userService.SendTwoFactorEmailAsync(user); + } + + return twoFactorResultDict; + } + + public async Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, + string token) + { + switch (type) + { + case TwoFactorProviderType.Authenticator: + case TwoFactorProviderType.Email: + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.YubiKey: + case TwoFactorProviderType.WebAuthn: + case TwoFactorProviderType.Remember: + if (type != TwoFactorProviderType.Remember && + !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) + { + return false; + } + // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt + if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + if (type == TwoFactorProviderType.Duo) + { + if (!token.Contains(':')) + { + // We have to send the provider to the DuoWebV4SDKService to create the DuoClient + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); + return await _duoWebV4SDKService.ValidateAsync(token, provider, user); + } + } + } + + return await _userManager.VerifyTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(type), token); + case TwoFactorProviderType.OrganizationDuo: + if (!organization?.TwoFactorProviderIsEnabled(type) ?? true) + { + return false; + } + + // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt + if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + if (type == TwoFactorProviderType.OrganizationDuo) + { + if (!token.Contains(':')) + { + // We have to send the provider to the DuoWebV4SDKService to create the DuoClient + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + return await _duoWebV4SDKService.ValidateAsync(token, provider, user); + } + } + } + + return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user); + default: + return false; + } + } + + private async Task> BuildTwoFactorParams(Organization organization, User user, + TwoFactorProviderType type, TwoFactorProvider provider) + { + switch (type) + { + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.WebAuthn: + case TwoFactorProviderType.Email: + case TwoFactorProviderType.YubiKey: + if (!await _userService.TwoFactorProviderIsEnabledAsync(type, user)) + { + return null; + } + + var token = await _userManager.GenerateTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(type)); + if (type == TwoFactorProviderType.Duo) + { + var duoResponse = new Dictionary + { + ["Host"] = provider.MetaData["Host"], + ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), + }; + + return duoResponse; + } + else if (type == TwoFactorProviderType.WebAuthn) + { + if (token == null) + { + return null; + } + + return JsonSerializer.Deserialize>(token); + } + else if (type == TwoFactorProviderType.Email) + { + var twoFactorEmail = (string)provider.MetaData["Email"]; + var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail); + return new Dictionary { ["Email"] = redactedEmail }; + } + else if (type == TwoFactorProviderType.YubiKey) + { + return new Dictionary { ["Nfc"] = (bool)provider.MetaData["Nfc"] }; + } + + return null; + case TwoFactorProviderType.OrganizationDuo: + if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) + { + var duoResponse = new Dictionary + { + ["Host"] = provider.MetaData["Host"], + ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), + }; + + return duoResponse; + } + return null; + default: + return null; + } + } + + private bool OrgUsing2fa(IDictionary orgAbilities, Guid orgId) + { + return orgAbilities != null && orgAbilities.ContainsKey(orgId) && + orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; + } +} \ No newline at end of file diff --git a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs index 7bf90c756327..1b684aa9be9c 100644 --- a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs @@ -1,10 +1,8 @@ using System.Security.Claims; using System.Text.Json; using Bit.Core; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; @@ -34,11 +32,8 @@ public WebAuthnGrantValidator( IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, @@ -46,16 +41,27 @@ public WebAuthnGrantValidator( ISsoConfigRepository ssoConfigRepository, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IDataProtectorTokenFactory assertionOptionsDataProtector, IFeatureService featureService, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand ) - : base(userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, - userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) + : base( + userManager, + userService, + eventService, + deviceValidator, + twoFactorAuthenticationValidator, + organizationUserRepository, + mailService, + logger, + currentContext, + globalSettings, + userRepository, + policyService, + featureService, + ssoConfigRepository, + userDecryptionOptionsBuilder) { _assertionOptionsDataProtector = assertionOptionsDataProtector; _assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand; @@ -122,12 +128,6 @@ protected override ClaimsPrincipal GetSubject(ExtensionGrantValidationContext co return context.Result.Subject; } - protected override Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) - { - // We consider Fido2 userVerification a second factor, so we don't require a second factor here. - return Task.FromResult(new Tuple(false, null)); - } - protected override void SetTwoFactorResult(ExtensionGrantValidationContext context, Dictionary customResponse) { diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index 43532cb3f5e0..0774cef72961 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -21,6 +21,7 @@ public static IIdentityServerBuilder AddCustomIdentityServerServices(this IServi services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index 91d0ee01f700..5aa74e74ab81 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -237,6 +237,11 @@ await factory.RegisterAsync(new RegisterRequestModel MasterPasswordHash = DefaultPassword }); var userManager = factory.GetService>(); + await factory.RegisterAsync(new RegisterRequestModel + { + Email = DefaultUsername, + MasterPasswordHash = DefaultPassword + }); var user = await userManager.FindByEmailAsync(DefaultUsername); Assert.NotNull(user); diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 39b7edf8dd9c..43e53a6c7871 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -1,8 +1,7 @@ using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -11,7 +10,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Identity.IdentityServer; using Bit.Identity.Test.Wrappers; using Bit.Test.Common.AutoFixture.Attributes; @@ -32,19 +30,15 @@ public class BaseRequestValidatorTests private readonly IUserService _userService; private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; - private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IApplicationCacheService _applicationCacheService; + private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; + private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IPolicyService _policyService; - private readonly IDataProtectorTokenFactory _tokenDataFactory; - private readonly IFeatureService _featureService; + private readonly IFeatureService _featureService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder; @@ -52,43 +46,35 @@ public class BaseRequestValidatorTests public BaseRequestValidatorTests() { + _userManager = SubstituteUserManager(); _userService = Substitute.For(); _eventService = Substitute.For(); _deviceValidator = Substitute.For(); - _organizationDuoWebTokenProvider = Substitute.For(); - _duoWebV4SDKService = Substitute.For(); - _organizationRepository = Substitute.For(); + _twoFactorAuthenticationValidator = Substitute.For(); _organizationUserRepository = Substitute.For(); - _applicationCacheService = Substitute.For(); _mailService = Substitute.For(); _logger = Substitute.For>(); _currentContext = Substitute.For(); _globalSettings = Substitute.For(); _userRepository = Substitute.For(); _policyService = Substitute.For(); - _tokenDataFactory = Substitute.For>(); _featureService = Substitute.For(); _ssoConfigRepository = Substitute.For(); _userDecryptionOptionsBuilder = Substitute.For(); - _userManager = SubstituteUserManager(); _sut = new BaseRequestValidatorTestWrapper( _userManager, _userService, _eventService, _deviceValidator, - _organizationDuoWebTokenProvider, - _duoWebV4SDKService, - _organizationRepository, + _twoFactorAuthenticationValidator, _organizationUserRepository, - _applicationCacheService, _mailService, _logger, _currentContext, _globalSettings, _userRepository, _policyService, - _tokenDataFactory, _featureService, _ssoConfigRepository, _userDecryptionOptionsBuilder); @@ -202,6 +188,9 @@ public async Task ValidateAsync_AuthCodeGrantType_DeviceNull_ShouldError( { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, default))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -230,6 +219,9 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed( { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -271,6 +263,9 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_Should _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) .Returns(device); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -306,6 +301,9 @@ public async Task ValidateAsync_InvalidAuthType_ShouldSetSsoResult( _policyService.AnyPoliciesApplicableToUserAsync( Arg.Any(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed) .Returns(Task.FromResult(true)); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -330,6 +328,9 @@ public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync( context.ValidatedTokenRequest.ClientId = "Not Web"; _sut.isValid = true; _featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -341,28 +342,6 @@ public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync( , errorResponse.Message); } - [Theory, BitAutoData] - public async Task RequiresTwoFactorAsync_ClientCredentialsGrantType_ShouldReturnFalse( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - context.ValidatedTokenRequest.GrantType = "client_credentials"; - - // Act - var result = await _sut.TestRequiresTwoFactorAsync( - context.CustomValidatorRequestContext.User, - context.ValidatedTokenRequest); - - // Assert - Assert.False(result.Item1); - Assert.Null(result.Item2); - } - private BaseRequestValidationContextFake CreateContext( ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index 26043fd59217..47c8facd5c60 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -1,15 +1,11 @@ using System.Security.Claims; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Identity.IdentityServer; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; @@ -54,38 +50,30 @@ public BaseRequestValidatorTestWrapper( IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : - base( + base( userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, - duoWebV4SDKService, - organizationRepository, + twoFactorAuthenticationValidator, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, - tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) @@ -98,13 +86,6 @@ public async Task ValidateAsync( await ValidateAsync(context, context.ValidatedTokenRequest, context.CustomValidatorRequestContext); } - public async Task> TestRequiresTwoFactorAsync( - User user, - ValidatedTokenRequest context) - { - return await RequiresTwoFactorAsync(user, context); - } - protected override ClaimsPrincipal GetSubject( BaseRequestValidationContextFake context) { From b994eea0fadc16ab934002e9b8c2c266a2733199 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Thu, 10 Oct 2024 21:58:58 -0700 Subject: [PATCH 11/25] initial tests --- .../TwoFactorAuthenticationValidatorTests.cs | 248 ++++++++++++++++++ .../Wrappers/UserManagerTestWrapper.cs | 96 +++++++ 2 files changed, 344 insertions(+) create mode 100644 test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs create mode 100644 test/Identity.Test/Wrappers/UserManagerTestWrapper.cs diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs new file mode 100644 index 000000000000..a59a2018e1c4 --- /dev/null +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -0,0 +1,248 @@ +using System.Data; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Interfaces; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Bit.Identity.IdentityServer; +using Bit.Identity.Test.Wrappers; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using Xunit; +using AuthFixtures = Bit.Identity.Test.AutoFixture; + +namespace Bit.Identity.Test.IdentityServer; + +public class TwoFactorAuthenticationValidatorTests +{ + private readonly IUserService _userService; + private readonly UserManagerTestWrapper _userManager; + private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; + private readonly ITemporaryDuoWebV4SDKService _temporaryDuoWebV4SDKService; + private readonly IFeatureService _featureService; + private readonly IApplicationCacheService _applicationCacheService; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokenable; + private readonly ICurrentContext _currentContext; + private readonly TwoFactorAuthenticationValidator _sut; + + public TwoFactorAuthenticationValidatorTests() + { + _userService = Substitute.For(); + _userManager = SubstituteUserManager(); + _organizationDuoWebTokenProvider = Substitute.For(); + _temporaryDuoWebV4SDKService = Substitute.For(); + _featureService = Substitute.For(); + _applicationCacheService = Substitute.For(); + _organizationUserRepository = Substitute.For(); + _organizationRepository = Substitute.For(); + _ssoEmail2faSessionTokenable = Substitute.For>(); + _currentContext = Substitute.For(); + + _sut = new TwoFactorAuthenticationValidator( + _userService, + _userManager, + _organizationDuoWebTokenProvider, + _temporaryDuoWebV4SDKService, + _featureService, + _applicationCacheService, + _organizationUserRepository, + _organizationRepository, + _ssoEmail2faSessionTokenable, + _currentContext); + } + + [Theory] + [BitAutoData("password")] + [BitAutoData("authorization_code")] + public async void RequiresTwoFactorAsync_IndividualOnly_Required_ReturnTrue( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request.GrantType = grantType; + // All three of these must be true for the two factor authentication to be required + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.SUPPORTS_TWO_FACTOR = true; + // In order for the two factor authentication to be required, the user must have at least one two factor provider + _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + + + // Act + var result = await _sut.RequiresTwoFactorAsync(user, request); + + // Assert + Assert.True(result.Item1); + Assert.Null(result.Item2); + } + + [Theory] + [BitAutoData("client_credentials")] + [BitAutoData("webauthn")] + public async void RequiresTwoFactorAsync_NotRequired_ReturnFalse( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request.GrantType = grantType; + + // Act + var result = await _sut.RequiresTwoFactorAsync(user, request); + + // Assert + Assert.False(result.Item1); + Assert.Null(result.Item2); + } + + [Theory] + [BitAutoData("password")] + [BitAutoData("authorization_code")] + public async void RequiresTwoFactorAsync_IndividualFalse_OrganizationRequired_ReturnTrue( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user, + OrganizationUserOrganizationDetails orgUser, + Organization organization, + ICollection organizationCollection) + { + // Arrange + request.GrantType = grantType; + // Link the orgUser to the User making the request + orgUser.UserId = user.Id; + // Link organization to the organization user + organization.Id = orgUser.OrganizationId; + + // Set Organization 2FA to required + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + // Make sure organization list is not empty + organizationCollection.Clear(); + // Fix OrganizationUser Permissions field + orgUser.Permissions = "{}"; + organizationCollection.Add(new CurrentContextOrganization(orgUser)); + + _currentContext.OrganizationMembershipAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(organizationCollection)); + + _applicationCacheService.GetOrganizationAbilitiesAsync() + .Returns(new Dictionary() + { + { orgUser.OrganizationId, new OrganizationAbility(organization)} + }); + + _organizationRepository.GetManyByUserIdAsync(Arg.Any()).Returns([organization]); + + // Act + var result = await _sut.RequiresTwoFactorAsync(user, request); + + // Assert + Assert.True(result.Item1); + Assert.NotNull(result.Item2); + Assert.IsType(result.Item2); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_NoProviders_ReturnsNull( + User user, + Organization organization) + { + // Arrange + organization.Use2fa = true; + organization.TwoFactorProviders = "{}"; + organization.Enabled = true; + + user.TwoFactorProviders = ""; + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, organization); + + // Assert + Assert.Null(result); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_OrganizationProviders_ReturnsNotNull( + User user, + Organization organization) + { + // Arrange + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + user.TwoFactorProviders = ""; + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, organization); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders")); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_IndividualProviders_ReturnsNotNull( + User user, + Organization organization) + { + // Arrange + user.TwoFactorProviders = GetTwoFactorIndividualDuoProviderJson(); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, organization); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders")); + } + + private UserManagerTestWrapper SubstituteUserManager() + { + return new UserManagerTestWrapper( + Substitute.For>(), + Substitute.For>(), + Substitute.For>(), + Enumerable.Empty>(), + Enumerable.Empty>(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For>>()); + } + + private static string GetTwoFactorOrganizationDuoProviderJson() + { + return + "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + private static string GetTwoFactorIndividualDuoProviderJson() + { + return + "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } +} diff --git a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs new file mode 100644 index 000000000000..f1207a4b9a26 --- /dev/null +++ b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs @@ -0,0 +1,96 @@ + +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Bit.Identity.Test.Wrappers; + +public class UserManagerTestWrapper : UserManager where TUser : class +{ + /// + /// Modify this value to mock the responses from UserManager.GetTwoFactorEnabledAsync() + /// + public bool TWO_FACTOR_ENABLED { get; set; } = false; + /// + /// Modify this value to mock the responses from UserManager.GetValidTwoFactorProvidersAsync() + /// + public IList TWO_FACTOR_PROVIDERS { get; set; } = []; + /// + /// Modify this value to mock the responses from UserManager.GenerateTwoFactorTokenAsync() + /// + public string TWO_FACTOR_TOKEN { get; set; } = string.Empty; + /// + /// Modify this value to mock the responses from UserManager.VerifyTwoFactorTokenAsync() + /// + public bool TWO_FACTOR_TOKEN_VERIFIED { get; set; } = false; + + /// + /// Modify this value to mock the responses from UserManager.SupportsUserTwoFactor + /// + public bool SUPPORTS_TWO_FACTOR { get; set; } = false; + + public override bool SupportsUserTwoFactor + { + get + { + return SUPPORTS_TWO_FACTOR; + } + } + + public UserManagerTestWrapper( + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, + keyNormalizer, errors, services, logger) + { } + + /// + /// return class variable TWO_FACTOR_ENABLED + /// + /// + /// + public override async Task GetTwoFactorEnabledAsync(TUser user) + { + return TWO_FACTOR_ENABLED; + } + + /// + /// return class variable TWO_FACTOR_PROVIDERS + /// + /// + /// + public override async Task> GetValidTwoFactorProvidersAsync(TUser user) + { + return TWO_FACTOR_PROVIDERS; + } + + /// + /// return class variable TWO_FACTOR_TOKEN + /// + /// + /// + /// + public override async Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider) + { + return TWO_FACTOR_TOKEN; + } + + /// + /// return class variable TWO_FACTOR_TOKEN_VERIFIED + /// + /// + /// + /// + /// + public override async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) + { + return TWO_FACTOR_TOKEN_VERIFIED; + } +} From 4c0871ae5a2c0878884ccfd903d3dc5ea8d5b17d Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 14 Oct 2024 22:11:12 -0700 Subject: [PATCH 12/25] Unit Tests --- .../IdentityServer/BaseRequestValidator.cs | 2 +- .../CustomTokenRequestValidator.cs | 21 +- .../IdentityServer/TwoFactorValidator.cs | 11 +- .../IdentityServer/WebAuthnGrantValidator.cs | 20 +- .../ResourceOwnerPasswordValidatorTests.cs | 8 +- .../BaseRequestValidatorTests.cs | 14 +- .../TwoFactorAuthenticationValidatorTests.cs | 206 ++++++++++++++++-- .../Wrappers/UserManagerTestWrapper.cs | 2 +- 8 files changed, 231 insertions(+), 53 deletions(-) diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index b5a4cd8fd776..8f2776ec924d 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -108,7 +108,7 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, if (isTwoFactorRequired) { // 2FA required and not provided response - if (!validTwoFactorRequest || + if (!validTwoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) { var resultDict = await _twoFactorAuthenticationValidator diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index c506ec8c84fc..8c2654ca6af6 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -1,9 +1,7 @@ using System.Diagnostics; using System.Security.Claims; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Api.Response; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -11,7 +9,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Validation; using HandlebarsDotNet; @@ -45,19 +42,19 @@ public CustomTokenRequestValidator( IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder ) : base( - userManager, - userService, - eventService, - deviceValidator, + userManager, + userService, + eventService, + deviceValidator, twoFactorAuthenticationValidator, organizationUserRepository, - mailService, - logger, - currentContext, + mailService, + logger, + currentContext, globalSettings, - userRepository, + userRepository, policyService, - featureService, + featureService, ssoConfigRepository, userDecryptionOptionsBuilder) { diff --git a/src/Identity/IdentityServer/TwoFactorValidator.cs b/src/Identity/IdentityServer/TwoFactorValidator.cs index 361567290e94..d78d29e668f5 100644 --- a/src/Identity/IdentityServer/TwoFactorValidator.cs +++ b/src/Identity/IdentityServer/TwoFactorValidator.cs @@ -112,8 +112,8 @@ public async Task> BuildTwoFactorResultAsync(User use foreach (var provider in enabledProviders) { providerKeys.Add((byte)provider.Key); - var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); - providers.Add(((byte)provider.Key).ToString(), infoDict); + var twoFactorParams = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); + providers.Add(((byte)provider.Key).ToString(), twoFactorParams); } var twoFactorResultDict = new Dictionary @@ -140,7 +140,10 @@ public async Task> BuildTwoFactorResultAsync(User use return twoFactorResultDict; } - public async Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, + public async Task VerifyTwoFactor( + User user, + Organization organization, + TwoFactorProviderType type, string token) { switch (type) @@ -267,4 +270,4 @@ private bool OrgUsing2fa(IDictionary orgAbilities, Gu return orgAbilities != null && orgAbilities.ContainsKey(orgId) && orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; } -} \ No newline at end of file +} diff --git a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs index 1b684aa9be9c..df4b7870e716 100644 --- a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs @@ -46,20 +46,20 @@ public WebAuthnGrantValidator( IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand ) - : base( - userManager, - userService, - eventService, - deviceValidator, + : base( + userManager, + userService, + eventService, + deviceValidator, twoFactorAuthenticationValidator, organizationUserRepository, - mailService, - logger, - currentContext, + mailService, + logger, + currentContext, globalSettings, - userRepository, + userRepository, policyService, - featureService, + featureService, ssoConfigRepository, userDecryptionOptionsBuilder) { diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index 5aa74e74ab81..cd734dba0a62 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -238,10 +238,10 @@ await factory.RegisterAsync(new RegisterRequestModel }); var userManager = factory.GetService>(); await factory.RegisterAsync(new RegisterRequestModel - { - Email = DefaultUsername, - MasterPasswordHash = DefaultPassword - }); + { + Email = DefaultUsername, + MasterPasswordHash = DefaultPassword + }); var user = await userManager.FindByEmailAsync(DefaultUsername); Assert.NotNull(user); diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 43e53a6c7871..ef425dcdcafb 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -31,14 +31,14 @@ public class BaseRequestValidatorTests private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; - private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IPolicyService _policyService; - private readonly IFeatureService _featureService; + private readonly IFeatureService _featureService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder; @@ -190,7 +190,7 @@ public async Task ValidateAsync_AuthCodeGrantType_DeviceNull_ShouldError( var context = CreateContext(tokenRequest, requestContext, grantResult); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, default))); + .Returns(Task.FromResult(new Tuple(false, default))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -221,7 +221,7 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed( var context = CreateContext(tokenRequest, requestContext, grantResult); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); + .Returns(Task.FromResult(new Tuple(false, null))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -265,7 +265,7 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_Should .Returns(device); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -303,7 +303,7 @@ public async Task ValidateAsync_InvalidAuthType_ShouldSetSsoResult( .Returns(Task.FromResult(true)); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -330,7 +330,7 @@ public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync( _featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index a59a2018e1c4..a533deca7c5a 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -1,16 +1,15 @@ -using System.Data; +using Bit.Core; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Interfaces; +using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Entities; -using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Identity.IdentityServer; using Bit.Identity.Test.Wrappers; @@ -81,7 +80,6 @@ public async void RequiresTwoFactorAsync_IndividualOnly_Required_ReturnTrue( // In order for the two factor authentication to be required, the user must have at least one two factor provider _userManager.TWO_FACTOR_PROVIDERS = ["email"]; - // Act var result = await _sut.RequiresTwoFactorAsync(user, request); @@ -189,7 +187,7 @@ public async void BuildTwoFactorResultAsync_OrganizationProviders_ReturnsNotNull organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); organization.Enabled = true; - user.TwoFactorProviders = ""; + user.TwoFactorProviders = null; // Act var result = await _sut.BuildTwoFactorResultAsync(user, organization); @@ -204,14 +202,16 @@ public async void BuildTwoFactorResultAsync_OrganizationProviders_ReturnsNotNull [Theory] [BitAutoData] public async void BuildTwoFactorResultAsync_IndividualProviders_ReturnsNotNull( - User user, - Organization organization) + User user) { // Arrange - user.TwoFactorProviders = GetTwoFactorIndividualDuoProviderJson(); + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(TwoFactorProviderType.Duo); + + _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) + .Returns(true); // Act - var result = await _sut.BuildTwoFactorResultAsync(user, organization); + var result = await _sut.BuildTwoFactorResultAsync(user, null); // Assert Assert.NotNull(result); @@ -220,7 +220,178 @@ public async void BuildTwoFactorResultAsync_IndividualProviders_ReturnsNotNull( Assert.True(result.ContainsKey("TwoFactorProviders")); } - private UserManagerTestWrapper SubstituteUserManager() + [Theory] + [BitAutoData(TwoFactorProviderType.Email)] + public async void BuildTwoFactorResultAsync_IndividualEmailProvider_SendsEmail_SetsSsoToken_ReturnsNotNull( + TwoFactorProviderType providerType, + User user) + { + // Arrange + var providerTypeInt = (int)providerType; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.SUPPORTS_TWO_FACTOR = true; + _userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()]; + + _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) + .Returns(true); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, null); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders2")); + var providers = (Dictionary>)result["TwoFactorProviders2"]; + Assert.True(providers.ContainsKey(providerTypeInt.ToString())); + Assert.True(result.ContainsKey("SsoEmail2faSessionToken")); + Assert.True(result.ContainsKey("Email")); + + await _userService.Received(1).SendTwoFactorEmailAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.WebAuthn)] + [BitAutoData(TwoFactorProviderType.Email)] + [BitAutoData(TwoFactorProviderType.YubiKey)] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void BuildTwoFactorResultAsync_IndividualProvider_ReturnMatchesType( + TwoFactorProviderType providerType, + User user) + { + // Arrange + var providerTypeInt = (int)providerType; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.SUPPORTS_TWO_FACTOR = true; + _userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()]; + _userManager.TWO_FACTOR_TOKEN = "{\"Key1\":\"WebauthnToken\"}"; + + _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) + .Returns(true); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, null); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders2")); + var providers = (Dictionary>)result["TwoFactorProviders2"]; + Assert.True(providers.ContainsKey(providerTypeInt.ToString())); + } + + [Theory] + [BitAutoData] + public async void VerifyTwoFactorAsync_Individual_TypeNull_ReturnsFalse( + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + TwoFactorProviderType.Email, user).Returns(true); + + _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + + // Act + var result = await _sut.VerifyTwoFactor( + user, null, default, token); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.WebAuthn)] + [BitAutoData(TwoFactorProviderType.Email)] + [BitAutoData(TwoFactorProviderType.YubiKey)] + [BitAutoData(TwoFactorProviderType.Remember)] + public async void VerifyTwoFactorAsync_Individual_ValidToken_ReturnsTrue( + TwoFactorProviderType providerType, + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + providerType, user).Returns(true); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN = token; + + // Act + var result = await _sut.VerifyTwoFactor(user, null, providerType, token); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( + TwoFactorProviderType providerType, + User user, + Organization organization, + string token) + { + // Arrange + _organizationDuoWebTokenProvider.ValidateAsync( + token, organization, user).Returns(true); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN = token; + + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + // Act + var result = await _sut.VerifyTwoFactor( + user, organization, providerType, token); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void VerifyTwoFactorAsync_TemporaryDuoService_ReturnsFalse( + TwoFactorProviderType providerType, + User user, + Organization organization, + string token) + { + // Arrange + _featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true); + _userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true); + _temporaryDuoWebV4SDKService.ValidateAsync( + token, Arg.Any(), user).Returns(true); + + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN = token; + + // Act + var result = await _sut.VerifyTwoFactor( + user, organization, providerType, token); + + // Assert + Assert.True(result); + + } + + private static UserManagerTestWrapper SubstituteUserManager() { return new UserManagerTestWrapper( Substitute.For>(), @@ -240,9 +411,16 @@ private static string GetTwoFactorOrganizationDuoProviderJson() "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - private static string GetTwoFactorIndividualDuoProviderJson() + private static string GetTwoFactorIndividualProviderJson(TwoFactorProviderType providerType) { - return - "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + return providerType switch + { + TwoFactorProviderType.Duo => "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}", + TwoFactorProviderType.Email => "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}", + TwoFactorProviderType.WebAuthn => "{\"7\":{\"Enabled\":true,\"MetaData\":{\"Key1\":{\"Name\":\"key1\",\"Descriptor\":{\"Type\":0,\"Id\":\"keyId\",\"Transports\":null},\"PublicKey\":\"key\",\"UserHandle\":\"handle\",\"SignatureCounter\":0,\"CredType\":\"none\",\"RegDate\":\"2022-01-01T00:00:00Z\",\"AaGuid\":\"00000000-0000-0000-0000-000000000000\",\"Migrated\":false}}}}", + TwoFactorProviderType.YubiKey => "{\"3\":{\"Enabled\":true,\"MetaData\":{\"Id\":\"yubikeyId\",\"Nfc\":true}}}", + TwoFactorProviderType.OrganizationDuo => "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}", + _ => "{}", + }; } } diff --git a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs index f1207a4b9a26..aa06310a866a 100644 --- a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs +++ b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs @@ -91,6 +91,6 @@ public override async Task GenerateTwoFactorTokenAsync(TUser user, strin /// public override async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) { - return TWO_FACTOR_TOKEN_VERIFIED; + return token == TWO_FACTOR_TOKEN; } } From db2e3fda92289e8273a96a6d8f01acb3afadb683 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Wed, 2 Oct 2024 15:30:44 -0700 Subject: [PATCH 13/25] initial device removal --- src/Identity/IdentityServer/BaseRequestValidator.cs | 2 +- src/Identity/IdentityServer/DeviceValidator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 8129a1a10ec6..8adee11dc0fb 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -573,7 +573,7 @@ private async Task GetMasterPasswordPolicy(Us return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user)); } -#nullable enable + #nullable enable /// /// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents /// diff --git a/src/Identity/IdentityServer/DeviceValidator.cs b/src/Identity/IdentityServer/DeviceValidator.cs index bf3e6d7dd81f..b8b6356a4ec8 100644 --- a/src/Identity/IdentityServer/DeviceValidator.cs +++ b/src/Identity/IdentityServer/DeviceValidator.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core.Context; using Bit.Core.Entities; From d127e6f721e339d51cd2980e6e7b4af8d158b096 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Wed, 2 Oct 2024 17:37:18 -0700 Subject: [PATCH 14/25] Unit Testing --- src/Identity/IdentityServer/DeviceValidator.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Identity/IdentityServer/DeviceValidator.cs b/src/Identity/IdentityServer/DeviceValidator.cs index b8b6356a4ec8..a1d3733c4e53 100644 --- a/src/Identity/IdentityServer/DeviceValidator.cs +++ b/src/Identity/IdentityServer/DeviceValidator.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Reflection; using Bit.Core.Context; using Bit.Core.Entities; @@ -41,6 +41,12 @@ public class DeviceValidator( private readonly IMailService _mailService = mailService; private readonly ICurrentContext _currentContext = currentContext; + /// + /// Save a device to the database. If the device is already known, it will be returned. + /// + /// The user is assumed NOT null, still going to check though + /// Duende Validated Request that contains the data to create the device object + /// Returns null if user or device is malformed; The existing device if already in DB; a new device login public async Task SaveDeviceAsync(User user, ValidatedTokenRequest request) { var device = GetDeviceFromRequest(request); From 7ef6c0bf0c2ad113a4299ca349263264fac3458a Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Thu, 3 Oct 2024 20:11:56 -0700 Subject: [PATCH 15/25] Finalized tests --- src/Identity/IdentityServer/BaseRequestValidator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 8adee11dc0fb..8129a1a10ec6 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -573,7 +573,7 @@ private async Task GetMasterPasswordPolicy(Us return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user)); } - #nullable enable +#nullable enable /// /// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents /// From 3b5ec21f48d08a18090e333d0643c4e99570b5c3 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Thu, 10 Oct 2024 21:55:18 -0700 Subject: [PATCH 16/25] initial commit refactoring two factor --- .../IdentityServer/BaseRequestValidator.cs | 296 +++--------------- .../CustomTokenRequestValidator.cs | 34 +- .../ResourceOwnerPasswordValidator.cs | 29 +- .../IdentityServer/TwoFactorValidator.cs | 270 ++++++++++++++++ .../IdentityServer/WebAuthnGrantValidator.cs | 34 +- .../Utilities/ServiceCollectionExtensions.cs | 1 + .../ResourceOwnerPasswordValidatorTests.cs | 5 + .../BaseRequestValidatorTests.cs | 65 ++-- .../BaseRequestValidatorTestWrapper.cs | 25 +- 9 files changed, 394 insertions(+), 365 deletions(-) create mode 100644 src/Identity/IdentityServer/TwoFactorValidator.cs diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 8129a1a10ec6..b5a4cd8fd776 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -1,15 +1,10 @@ using System.Security.Claims; -using System.Text.Json; using Bit.Core; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Api.Response; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -17,11 +12,9 @@ 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; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Core.Utilities; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; @@ -33,16 +26,12 @@ public abstract class BaseRequestValidator where T : class private UserManager _userManager; private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; - private readonly IOrganizationRepository _organizationRepository; + private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IApplicationCacheService _applicationCacheService; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; - private readonly IDataProtectorTokenFactory _tokenDataFactory; protected ICurrentContext CurrentContext { get; } protected IPolicyService PolicyService { get; } @@ -56,18 +45,14 @@ public BaseRequestValidator( IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) @@ -76,18 +61,14 @@ public BaseRequestValidator( _userService = userService; _eventService = eventService; _deviceValidator = deviceValidator; - _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; - _duoWebV4SDKService = duoWebV4SDKService; - _organizationRepository = organizationRepository; + _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator; _organizationUserRepository = organizationUserRepository; - _applicationCacheService = applicationCacheService; _mailService = mailService; _logger = logger; CurrentContext = currentContext; _globalSettings = globalSettings; PolicyService = policyService; _userRepository = userRepository; - _tokenDataFactory = tokenDataFactory; FeatureService = featureService; SsoConfigRepository = ssoConfigRepository; UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder; @@ -104,12 +85,6 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, request.UserName, validatorContext.CaptchaResponse.Score); } - var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString(); - var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); - var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; - var twoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && - !string.IsNullOrWhiteSpace(twoFactorProvider); - var valid = await ValidateContextAsync(context, validatorContext); var user = validatorContext.User; if (!valid) @@ -123,17 +98,32 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, return; } - var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request); + var (isTwoFactorRequired, twoFactorOrganization) = await _twoFactorAuthenticationValidator.RequiresTwoFactorAsync(user, request); + var twoFactorToken = request.Raw["TwoFactorToken"]?.ToString(); + var twoFactorProvider = request.Raw["TwoFactorProvider"]?.ToString(); + var twoFactorRemember = request.Raw["TwoFactorRemember"]?.ToString() == "1"; + var validTwoFactorRequest = !string.IsNullOrWhiteSpace(twoFactorToken) && + !string.IsNullOrWhiteSpace(twoFactorProvider); + if (isTwoFactorRequired) { - if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) + // 2FA required and not provided response + if (!validTwoFactorRequest || + !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) { - await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); + var resultDict = await _twoFactorAuthenticationValidator + .BuildTwoFactorResultAsync(user, twoFactorOrganization); + + // Include Master Password Policy in 2FA response + resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + SetTwoFactorResult(context, resultDict); return; } - var verified = await VerifyTwoFactor(user, twoFactorOrganization, - twoFactorProviderType, twoFactorToken); + var verified = await _twoFactorAuthenticationValidator + .VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); + + // 2FA required but request not valid or remember token expired response if (!verified || isBot) { if (twoFactorProviderType != TwoFactorProviderType.Remember) @@ -143,16 +133,20 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, } else if (twoFactorProviderType == TwoFactorProviderType.Remember) { - await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); + var resultDict = await _twoFactorAuthenticationValidator + .BuildTwoFactorResultAsync(user, twoFactorOrganization); + + // Include Master Password Policy in 2FA response + resultDict.Add("MasterPasswordPolicy", await GetMasterPasswordPolicy(user)); + SetTwoFactorResult(context, resultDict); } return; } } else { - twoFactorRequest = false; + validTwoFactorRequest = false; twoFactorRemember = false; - twoFactorToken = null; } // Force legacy users to the web for migration @@ -165,7 +159,6 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, } } - // Returns true if can finish validation process if (await IsValidAuthTypeAsync(user, request.GrantType)) { var device = await _deviceValidator.SaveDeviceAsync(user, request); @@ -174,8 +167,7 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, await BuildErrorResultAsync("No device information provided.", false, context, user); return; } - - await BuildSuccessResultAsync(user, context, device, twoFactorRequest && twoFactorRemember); + await BuildSuccessResultAsync(user, context, device, validTwoFactorRequest && twoFactorRemember); } else { @@ -238,67 +230,6 @@ protected async Task BuildSuccessResultAsync(User user, T context, Device device await SetSuccessResult(context, user, claims, customResponse); } - protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context) - { - var providerKeys = new List(); - var providers = new Dictionary>(); - - var enabledProviders = new List>(); - if (organization?.GetTwoFactorProviders() != null) - { - enabledProviders.AddRange(organization.GetTwoFactorProviders().Where( - p => organization.TwoFactorProviderIsEnabled(p.Key))); - } - - if (user.GetTwoFactorProviders() != null) - { - foreach (var p in user.GetTwoFactorProviders()) - { - if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user)) - { - enabledProviders.Add(p); - } - } - } - - if (!enabledProviders.Any()) - { - await BuildErrorResultAsync("No two-step providers enabled.", false, context, user); - return; - } - - foreach (var provider in enabledProviders) - { - providerKeys.Add((byte)provider.Key); - var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); - providers.Add(((byte)provider.Key).ToString(), infoDict); - } - - var twoFactorResultDict = new Dictionary - { - { "TwoFactorProviders", providers.Keys }, - { "TwoFactorProviders2", providers }, - { "MasterPasswordPolicy", await GetMasterPasswordPolicy(user) }, - }; - - // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token - if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) - { - twoFactorResultDict.Add("SsoEmail2faSessionToken", - _tokenDataFactory.Protect(new SsoEmail2faSessionTokenable(user))); - - twoFactorResultDict.Add("Email", user.Email); - } - - SetTwoFactorResult(context, twoFactorResultDict); - - if (enabledProviders.Count() == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) - { - // Send email now if this is their only 2FA method - await _userService.SendTwoFactorEmailAsync(user); - } - } - protected async Task BuildErrorResultAsync(string message, bool twoFactorRequest, T context, User user) { if (user != null) @@ -329,35 +260,13 @@ protected abstract Task SetSuccessResult(T context, User user, List claim protected abstract void SetErrorResult(T context, Dictionary customResponse); protected abstract ClaimsPrincipal GetSubject(T context); - protected virtual async Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) - { - if (request.GrantType == "client_credentials") - { - // Do not require MFA for api key logins - return new Tuple(false, null); - } - - var individualRequired = _userManager.SupportsUserTwoFactor && - await _userManager.GetTwoFactorEnabledAsync(user) && - (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; - - Organization firstEnabledOrg = null; - var orgs = (await CurrentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList(); - if (orgs.Count > 0) - { - var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); - var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id)); - if (twoFactorOrgs.Any()) - { - var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id); - firstEnabledOrg = userOrgs.FirstOrDefault( - o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled()); - } - } - - return new Tuple(individualRequired || firstEnabledOrg != null, firstEnabledOrg); - } - + /// + /// Check if the user is required to authenticate via SSO. If the user requires SSO, but they are + /// logging in using an API Key (client_credentials) then they are allowed to bypass the SSO requirement. + /// + /// user trying to login + /// magic string identifying the grant type requested + /// private async Task IsValidAuthTypeAsync(User user, string grantType) { if (grantType == "authorization_code" || grantType == "client_credentials") @@ -367,7 +276,6 @@ private async Task IsValidAuthTypeAsync(User user, string grantType) return true; } - // 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) @@ -379,134 +287,6 @@ private async Task IsValidAuthTypeAsync(User user, string grantType) return true; } - private bool OrgUsing2fa(IDictionary orgAbilities, Guid orgId) - { - return orgAbilities != null && orgAbilities.ContainsKey(orgId) && - orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; - } - - private async Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, - string token) - { - switch (type) - { - case TwoFactorProviderType.Authenticator: - case TwoFactorProviderType.Email: - case TwoFactorProviderType.Duo: - case TwoFactorProviderType.YubiKey: - case TwoFactorProviderType.WebAuthn: - case TwoFactorProviderType.Remember: - if (type != TwoFactorProviderType.Remember && - !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) - { - return false; - } - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (type == TwoFactorProviderType.Duo) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - } - - return await _userManager.VerifyTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(type), token); - case TwoFactorProviderType.OrganizationDuo: - if (!organization?.TwoFactorProviderIsEnabled(type) ?? true) - { - return false; - } - - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - if (type == TwoFactorProviderType.OrganizationDuo) - { - if (!token.Contains(':')) - { - // We have to send the provider to the DuoWebV4SDKService to create the DuoClient - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - return await _duoWebV4SDKService.ValidateAsync(token, provider, user); - } - } - } - - return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user); - default: - return false; - } - } - - private async Task> BuildTwoFactorParams(Organization organization, User user, - TwoFactorProviderType type, TwoFactorProvider provider) - { - switch (type) - { - case TwoFactorProviderType.Duo: - case TwoFactorProviderType.WebAuthn: - case TwoFactorProviderType.Email: - case TwoFactorProviderType.YubiKey: - if (!await _userService.TwoFactorProviderIsEnabledAsync(type, user)) - { - return null; - } - - var token = await _userManager.GenerateTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(type)); - if (type == TwoFactorProviderType.Duo) - { - var duoResponse = new Dictionary - { - ["Host"] = provider.MetaData["Host"], - ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), - }; - - return duoResponse; - } - else if (type == TwoFactorProviderType.WebAuthn) - { - if (token == null) - { - return null; - } - - return JsonSerializer.Deserialize>(token); - } - else if (type == TwoFactorProviderType.Email) - { - var twoFactorEmail = (string)provider.MetaData["Email"]; - var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail); - return new Dictionary { ["Email"] = redactedEmail }; - } - else if (type == TwoFactorProviderType.YubiKey) - { - return new Dictionary { ["Nfc"] = (bool)provider.MetaData["Nfc"] }; - } - - return null; - case TwoFactorProviderType.OrganizationDuo: - if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) - { - var duoResponse = new Dictionary - { - ["Host"] = provider.MetaData["Host"], - ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), - }; - - return duoResponse; - } - return null; - default: - return null; - } - } - private async Task ResetFailedAuthDetailsAsync(User user) { // Early escape if db hit not necessary diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index 0d7a92c8af2f..c506ec8c84fc 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -29,28 +29,36 @@ public class CustomTokenRequestValidator : BaseRequestValidator userManager, - IDeviceValidator deviceValidator, IUserService userService, IEventService eventService, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + IDeviceValidator deviceValidator, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, - ISsoConfigRepository ssoConfigRepository, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, - IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) - : base(userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, - userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, + ISsoConfigRepository ssoConfigRepository, + IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder + ) + : base( + userManager, + userService, + eventService, + deviceValidator, + twoFactorAuthenticationValidator, + organizationUserRepository, + mailService, + logger, + currentContext, + globalSettings, + userRepository, + policyService, + featureService, + ssoConfigRepository, userDecryptionOptionsBuilder) { _userManager = userManager; @@ -70,7 +78,7 @@ public async Task ValidateAsync(CustomTokenRequestValidationContext context) } } - string[] allowedGrantTypes = { "authorization_code", "client_credentials" }; + string[] allowedGrantTypes = ["authorization_code", "client_credentials"]; if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType) || context.Result.ValidatedRequest.ClientId.StartsWith("organization") || context.Result.ValidatedRequest.ClientId.StartsWith("installation") diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs index 08560e240d84..6e488bb48255 100644 --- a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -1,8 +1,6 @@ using System.Security.Claims; using Bit.Core; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Context; @@ -10,7 +8,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Core.Utilities; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; @@ -31,11 +28,8 @@ public ResourceOwnerPasswordValidator( IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, @@ -44,14 +38,25 @@ public ResourceOwnerPasswordValidator( IAuthRequestRepository authRequestRepository, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) - : base(userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, - tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) + : base( + userManager, + userService, + eventService, + deviceValidator, + twoFactorAuthenticationValidator, + organizationUserRepository, + mailService, + logger, + currentContext, + globalSettings, + userRepository, + policyService, + featureService, + ssoConfigRepository, + userDecryptionOptionsBuilder) { _userManager = userManager; _currentContext = currentContext; diff --git a/src/Identity/IdentityServer/TwoFactorValidator.cs b/src/Identity/IdentityServer/TwoFactorValidator.cs new file mode 100644 index 000000000000..361567290e94 --- /dev/null +++ b/src/Identity/IdentityServer/TwoFactorValidator.cs @@ -0,0 +1,270 @@ + +using System.Text.Json; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tokens; +using Bit.Core.Utilities; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Identity.IdentityServer; + +public interface ITwoFactorAuthenticationValidator +{ + Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request); + Task> BuildTwoFactorResultAsync(User user, Organization organization); + Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, string token); +} + +public class TwoFactorAuthenticationValidator( + IUserService userService, + UserManager userManager, + IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, + ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IFeatureService featureService, + IApplicationCacheService applicationCacheService, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository, + IDataProtectorTokenFactory ssoEmail2faSessionTokeFactory, + ICurrentContext currentContext) : ITwoFactorAuthenticationValidator +{ + private readonly IUserService _userService = userService; + private readonly UserManager _userManager = userManager; + private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; + private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService = duoWebV4SDKService; + private readonly IFeatureService _featureService = featureService; + private readonly IApplicationCacheService _applicationCacheService = applicationCacheService; + private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository; + private readonly IOrganizationRepository _organizationRepository = organizationRepository; + private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory; + private readonly ICurrentContext _currentContext = currentContext; + + public async Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) + { + if (request.GrantType == "client_credentials" || request.GrantType == "webauthn") + { + /* + Do not require MFA for api key logins. + We consider Fido2 userVerification a second factor, so we don't require a second factor here. + */ + return new Tuple(false, null); + } + + var individualRequired = _userManager.SupportsUserTwoFactor && + await _userManager.GetTwoFactorEnabledAsync(user) && + (await _userManager.GetValidTwoFactorProvidersAsync(user)).Count > 0; + + Organization firstEnabledOrg = null; + var orgs = (await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, user.Id)).ToList(); + if (orgs.Count > 0) + { + var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var twoFactorOrgs = orgs.Where(o => OrgUsing2fa(orgAbilities, o.Id)); + if (twoFactorOrgs.Any()) + { + var userOrgs = await _organizationRepository.GetManyByUserIdAsync(user.Id); + firstEnabledOrg = userOrgs.FirstOrDefault( + o => orgs.Any(om => om.Id == o.Id) && o.TwoFactorIsEnabled()); + } + } + + return new Tuple(individualRequired || firstEnabledOrg != null, firstEnabledOrg); + } + + public async Task> BuildTwoFactorResultAsync(User user, Organization organization) + { + var providerKeys = new List(); + var providers = new Dictionary>(); + + var enabledProviders = new List>(); + if (organization?.GetTwoFactorProviders() != null) + { + enabledProviders.AddRange(organization.GetTwoFactorProviders().Where( + p => organization.TwoFactorProviderIsEnabled(p.Key))); + } + + if (user.GetTwoFactorProviders() != null) + { + foreach (var p in user.GetTwoFactorProviders()) + { + if (await _userService.TwoFactorProviderIsEnabledAsync(p.Key, user)) + { + enabledProviders.Add(p); + } + } + } + + if (!enabledProviders.Any()) + { + return null; + // await BuildErrorResultAsync("No two-step providers enabled.", false, context, user); + } + + foreach (var provider in enabledProviders) + { + providerKeys.Add((byte)provider.Key); + var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); + providers.Add(((byte)provider.Key).ToString(), infoDict); + } + + var twoFactorResultDict = new Dictionary + { + { "TwoFactorProviders", providers.Keys }, + { "TwoFactorProviders2", providers }, + }; + + // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token + if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) + { + twoFactorResultDict.Add("SsoEmail2faSessionToken", + _ssoEmail2faSessionTokeFactory.Protect(new SsoEmail2faSessionTokenable(user))); + + twoFactorResultDict.Add("Email", user.Email); + } + + if (enabledProviders.Count == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) + { + // Send email now if this is their only 2FA method + await _userService.SendTwoFactorEmailAsync(user); + } + + return twoFactorResultDict; + } + + public async Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, + string token) + { + switch (type) + { + case TwoFactorProviderType.Authenticator: + case TwoFactorProviderType.Email: + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.YubiKey: + case TwoFactorProviderType.WebAuthn: + case TwoFactorProviderType.Remember: + if (type != TwoFactorProviderType.Remember && + !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) + { + return false; + } + // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt + if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + if (type == TwoFactorProviderType.Duo) + { + if (!token.Contains(':')) + { + // We have to send the provider to the DuoWebV4SDKService to create the DuoClient + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); + return await _duoWebV4SDKService.ValidateAsync(token, provider, user); + } + } + } + + return await _userManager.VerifyTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(type), token); + case TwoFactorProviderType.OrganizationDuo: + if (!organization?.TwoFactorProviderIsEnabled(type) ?? true) + { + return false; + } + + // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt + if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + if (type == TwoFactorProviderType.OrganizationDuo) + { + if (!token.Contains(':')) + { + // We have to send the provider to the DuoWebV4SDKService to create the DuoClient + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + return await _duoWebV4SDKService.ValidateAsync(token, provider, user); + } + } + } + + return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user); + default: + return false; + } + } + + private async Task> BuildTwoFactorParams(Organization organization, User user, + TwoFactorProviderType type, TwoFactorProvider provider) + { + switch (type) + { + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.WebAuthn: + case TwoFactorProviderType.Email: + case TwoFactorProviderType.YubiKey: + if (!await _userService.TwoFactorProviderIsEnabledAsync(type, user)) + { + return null; + } + + var token = await _userManager.GenerateTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(type)); + if (type == TwoFactorProviderType.Duo) + { + var duoResponse = new Dictionary + { + ["Host"] = provider.MetaData["Host"], + ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), + }; + + return duoResponse; + } + else if (type == TwoFactorProviderType.WebAuthn) + { + if (token == null) + { + return null; + } + + return JsonSerializer.Deserialize>(token); + } + else if (type == TwoFactorProviderType.Email) + { + var twoFactorEmail = (string)provider.MetaData["Email"]; + var redactedEmail = CoreHelpers.RedactEmailAddress(twoFactorEmail); + return new Dictionary { ["Email"] = redactedEmail }; + } + else if (type == TwoFactorProviderType.YubiKey) + { + return new Dictionary { ["Nfc"] = (bool)provider.MetaData["Nfc"] }; + } + + return null; + case TwoFactorProviderType.OrganizationDuo: + if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) + { + var duoResponse = new Dictionary + { + ["Host"] = provider.MetaData["Host"], + ["AuthUrl"] = await _duoWebV4SDKService.GenerateAsync(provider, user), + }; + + return duoResponse; + } + return null; + default: + return null; + } + } + + private bool OrgUsing2fa(IDictionary orgAbilities, Guid orgId) + { + return orgAbilities != null && orgAbilities.ContainsKey(orgId) && + orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; + } +} \ No newline at end of file diff --git a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs index 7bf90c756327..1b684aa9be9c 100644 --- a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs @@ -1,10 +1,8 @@ using System.Security.Claims; using System.Text.Json; using Bit.Core; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; @@ -34,11 +32,8 @@ public WebAuthnGrantValidator( IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, @@ -46,16 +41,27 @@ public WebAuthnGrantValidator( ISsoConfigRepository ssoConfigRepository, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IDataProtectorTokenFactory assertionOptionsDataProtector, IFeatureService featureService, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand ) - : base(userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, - userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) + : base( + userManager, + userService, + eventService, + deviceValidator, + twoFactorAuthenticationValidator, + organizationUserRepository, + mailService, + logger, + currentContext, + globalSettings, + userRepository, + policyService, + featureService, + ssoConfigRepository, + userDecryptionOptionsBuilder) { _assertionOptionsDataProtector = assertionOptionsDataProtector; _assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand; @@ -122,12 +128,6 @@ protected override ClaimsPrincipal GetSubject(ExtensionGrantValidationContext co return context.Result.Subject; } - protected override Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) - { - // We consider Fido2 userVerification a second factor, so we don't require a second factor here. - return Task.FromResult(new Tuple(false, null)); - } - protected override void SetTwoFactorResult(ExtensionGrantValidationContext context, Dictionary customResponse) { diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index 43532cb3f5e0..0774cef72961 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -21,6 +21,7 @@ public static IIdentityServerBuilder AddCustomIdentityServerServices(this IServi services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index 91d0ee01f700..5aa74e74ab81 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -237,6 +237,11 @@ await factory.RegisterAsync(new RegisterRequestModel MasterPasswordHash = DefaultPassword }); var userManager = factory.GetService>(); + await factory.RegisterAsync(new RegisterRequestModel + { + Email = DefaultUsername, + MasterPasswordHash = DefaultPassword + }); var user = await userManager.FindByEmailAsync(DefaultUsername); Assert.NotNull(user); diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 39b7edf8dd9c..43e53a6c7871 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -1,8 +1,7 @@ using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -11,7 +10,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Identity.IdentityServer; using Bit.Identity.Test.Wrappers; using Bit.Test.Common.AutoFixture.Attributes; @@ -32,19 +30,15 @@ public class BaseRequestValidatorTests private readonly IUserService _userService; private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; - private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IApplicationCacheService _applicationCacheService; + private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; + private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IPolicyService _policyService; - private readonly IDataProtectorTokenFactory _tokenDataFactory; - private readonly IFeatureService _featureService; + private readonly IFeatureService _featureService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder; @@ -52,43 +46,35 @@ public class BaseRequestValidatorTests public BaseRequestValidatorTests() { + _userManager = SubstituteUserManager(); _userService = Substitute.For(); _eventService = Substitute.For(); _deviceValidator = Substitute.For(); - _organizationDuoWebTokenProvider = Substitute.For(); - _duoWebV4SDKService = Substitute.For(); - _organizationRepository = Substitute.For(); + _twoFactorAuthenticationValidator = Substitute.For(); _organizationUserRepository = Substitute.For(); - _applicationCacheService = Substitute.For(); _mailService = Substitute.For(); _logger = Substitute.For>(); _currentContext = Substitute.For(); _globalSettings = Substitute.For(); _userRepository = Substitute.For(); _policyService = Substitute.For(); - _tokenDataFactory = Substitute.For>(); _featureService = Substitute.For(); _ssoConfigRepository = Substitute.For(); _userDecryptionOptionsBuilder = Substitute.For(); - _userManager = SubstituteUserManager(); _sut = new BaseRequestValidatorTestWrapper( _userManager, _userService, _eventService, _deviceValidator, - _organizationDuoWebTokenProvider, - _duoWebV4SDKService, - _organizationRepository, + _twoFactorAuthenticationValidator, _organizationUserRepository, - _applicationCacheService, _mailService, _logger, _currentContext, _globalSettings, _userRepository, _policyService, - _tokenDataFactory, _featureService, _ssoConfigRepository, _userDecryptionOptionsBuilder); @@ -202,6 +188,9 @@ public async Task ValidateAsync_AuthCodeGrantType_DeviceNull_ShouldError( { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, default))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -230,6 +219,9 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed( { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -271,6 +263,9 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_Should _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) .Returns(device); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -306,6 +301,9 @@ public async Task ValidateAsync_InvalidAuthType_ShouldSetSsoResult( _policyService.AnyPoliciesApplicableToUserAsync( Arg.Any(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed) .Returns(Task.FromResult(true)); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -330,6 +328,9 @@ public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync( context.ValidatedTokenRequest.ClientId = "Not Web"; _sut.isValid = true; _featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true); + _twoFactorAuthenticationValidator + .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -341,28 +342,6 @@ public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync( , errorResponse.Message); } - [Theory, BitAutoData] - public async Task RequiresTwoFactorAsync_ClientCredentialsGrantType_ShouldReturnFalse( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - context.ValidatedTokenRequest.GrantType = "client_credentials"; - - // Act - var result = await _sut.TestRequiresTwoFactorAsync( - context.CustomValidatorRequestContext.User, - context.ValidatedTokenRequest); - - // Assert - Assert.False(result.Item1); - Assert.Null(result.Item2); - } - private BaseRequestValidationContextFake CreateContext( ValidatedTokenRequest tokenRequest, CustomValidatorRequestContext requestContext, diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index 26043fd59217..47c8facd5c60 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -1,15 +1,11 @@ using System.Security.Claims; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Identity.IdentityServer; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; @@ -54,38 +50,30 @@ public BaseRequestValidatorTestWrapper( IUserService userService, IEventService eventService, IDeviceValidator deviceValidator, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, - IOrganizationRepository organizationRepository, + ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IApplicationCacheService applicationCacheService, IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, IUserRepository userRepository, IPolicyService policyService, - IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : - base( + base( userManager, userService, eventService, deviceValidator, - organizationDuoWebTokenProvider, - duoWebV4SDKService, - organizationRepository, + twoFactorAuthenticationValidator, organizationUserRepository, - applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, - tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) @@ -98,13 +86,6 @@ public async Task ValidateAsync( await ValidateAsync(context, context.ValidatedTokenRequest, context.CustomValidatorRequestContext); } - public async Task> TestRequiresTwoFactorAsync( - User user, - ValidatedTokenRequest context) - { - return await RequiresTwoFactorAsync(user, context); - } - protected override ClaimsPrincipal GetSubject( BaseRequestValidationContextFake context) { From 784cfbd9f7930c72e08a821c84b096d7903b61ff Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Thu, 10 Oct 2024 21:58:58 -0700 Subject: [PATCH 17/25] initial tests --- .../TwoFactorAuthenticationValidatorTests.cs | 248 ++++++++++++++++++ .../Wrappers/UserManagerTestWrapper.cs | 96 +++++++ 2 files changed, 344 insertions(+) create mode 100644 test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs create mode 100644 test/Identity.Test/Wrappers/UserManagerTestWrapper.cs diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs new file mode 100644 index 000000000000..a59a2018e1c4 --- /dev/null +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -0,0 +1,248 @@ +using System.Data; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Interfaces; +using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Bit.Identity.IdentityServer; +using Bit.Identity.Test.Wrappers; +using Bit.Test.Common.AutoFixture.Attributes; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using Xunit; +using AuthFixtures = Bit.Identity.Test.AutoFixture; + +namespace Bit.Identity.Test.IdentityServer; + +public class TwoFactorAuthenticationValidatorTests +{ + private readonly IUserService _userService; + private readonly UserManagerTestWrapper _userManager; + private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; + private readonly ITemporaryDuoWebV4SDKService _temporaryDuoWebV4SDKService; + private readonly IFeatureService _featureService; + private readonly IApplicationCacheService _applicationCacheService; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokenable; + private readonly ICurrentContext _currentContext; + private readonly TwoFactorAuthenticationValidator _sut; + + public TwoFactorAuthenticationValidatorTests() + { + _userService = Substitute.For(); + _userManager = SubstituteUserManager(); + _organizationDuoWebTokenProvider = Substitute.For(); + _temporaryDuoWebV4SDKService = Substitute.For(); + _featureService = Substitute.For(); + _applicationCacheService = Substitute.For(); + _organizationUserRepository = Substitute.For(); + _organizationRepository = Substitute.For(); + _ssoEmail2faSessionTokenable = Substitute.For>(); + _currentContext = Substitute.For(); + + _sut = new TwoFactorAuthenticationValidator( + _userService, + _userManager, + _organizationDuoWebTokenProvider, + _temporaryDuoWebV4SDKService, + _featureService, + _applicationCacheService, + _organizationUserRepository, + _organizationRepository, + _ssoEmail2faSessionTokenable, + _currentContext); + } + + [Theory] + [BitAutoData("password")] + [BitAutoData("authorization_code")] + public async void RequiresTwoFactorAsync_IndividualOnly_Required_ReturnTrue( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request.GrantType = grantType; + // All three of these must be true for the two factor authentication to be required + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.SUPPORTS_TWO_FACTOR = true; + // In order for the two factor authentication to be required, the user must have at least one two factor provider + _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + + + // Act + var result = await _sut.RequiresTwoFactorAsync(user, request); + + // Assert + Assert.True(result.Item1); + Assert.Null(result.Item2); + } + + [Theory] + [BitAutoData("client_credentials")] + [BitAutoData("webauthn")] + public async void RequiresTwoFactorAsync_NotRequired_ReturnFalse( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user) + { + // Arrange + request.GrantType = grantType; + + // Act + var result = await _sut.RequiresTwoFactorAsync(user, request); + + // Assert + Assert.False(result.Item1); + Assert.Null(result.Item2); + } + + [Theory] + [BitAutoData("password")] + [BitAutoData("authorization_code")] + public async void RequiresTwoFactorAsync_IndividualFalse_OrganizationRequired_ReturnTrue( + string grantType, + [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request, + User user, + OrganizationUserOrganizationDetails orgUser, + Organization organization, + ICollection organizationCollection) + { + // Arrange + request.GrantType = grantType; + // Link the orgUser to the User making the request + orgUser.UserId = user.Id; + // Link organization to the organization user + organization.Id = orgUser.OrganizationId; + + // Set Organization 2FA to required + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + // Make sure organization list is not empty + organizationCollection.Clear(); + // Fix OrganizationUser Permissions field + orgUser.Permissions = "{}"; + organizationCollection.Add(new CurrentContextOrganization(orgUser)); + + _currentContext.OrganizationMembershipAsync(Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(organizationCollection)); + + _applicationCacheService.GetOrganizationAbilitiesAsync() + .Returns(new Dictionary() + { + { orgUser.OrganizationId, new OrganizationAbility(organization)} + }); + + _organizationRepository.GetManyByUserIdAsync(Arg.Any()).Returns([organization]); + + // Act + var result = await _sut.RequiresTwoFactorAsync(user, request); + + // Assert + Assert.True(result.Item1); + Assert.NotNull(result.Item2); + Assert.IsType(result.Item2); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_NoProviders_ReturnsNull( + User user, + Organization organization) + { + // Arrange + organization.Use2fa = true; + organization.TwoFactorProviders = "{}"; + organization.Enabled = true; + + user.TwoFactorProviders = ""; + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, organization); + + // Assert + Assert.Null(result); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_OrganizationProviders_ReturnsNotNull( + User user, + Organization organization) + { + // Arrange + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + user.TwoFactorProviders = ""; + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, organization); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders")); + } + + [Theory] + [BitAutoData] + public async void BuildTwoFactorResultAsync_IndividualProviders_ReturnsNotNull( + User user, + Organization organization) + { + // Arrange + user.TwoFactorProviders = GetTwoFactorIndividualDuoProviderJson(); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, organization); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders")); + } + + private UserManagerTestWrapper SubstituteUserManager() + { + return new UserManagerTestWrapper( + Substitute.For>(), + Substitute.For>(), + Substitute.For>(), + Enumerable.Empty>(), + Enumerable.Empty>(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For>>()); + } + + private static string GetTwoFactorOrganizationDuoProviderJson() + { + return + "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + private static string GetTwoFactorIndividualDuoProviderJson() + { + return + "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } +} diff --git a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs new file mode 100644 index 000000000000..f1207a4b9a26 --- /dev/null +++ b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs @@ -0,0 +1,96 @@ + +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Bit.Identity.Test.Wrappers; + +public class UserManagerTestWrapper : UserManager where TUser : class +{ + /// + /// Modify this value to mock the responses from UserManager.GetTwoFactorEnabledAsync() + /// + public bool TWO_FACTOR_ENABLED { get; set; } = false; + /// + /// Modify this value to mock the responses from UserManager.GetValidTwoFactorProvidersAsync() + /// + public IList TWO_FACTOR_PROVIDERS { get; set; } = []; + /// + /// Modify this value to mock the responses from UserManager.GenerateTwoFactorTokenAsync() + /// + public string TWO_FACTOR_TOKEN { get; set; } = string.Empty; + /// + /// Modify this value to mock the responses from UserManager.VerifyTwoFactorTokenAsync() + /// + public bool TWO_FACTOR_TOKEN_VERIFIED { get; set; } = false; + + /// + /// Modify this value to mock the responses from UserManager.SupportsUserTwoFactor + /// + public bool SUPPORTS_TWO_FACTOR { get; set; } = false; + + public override bool SupportsUserTwoFactor + { + get + { + return SUPPORTS_TWO_FACTOR; + } + } + + public UserManagerTestWrapper( + IUserStore store, + IOptions optionsAccessor, + IPasswordHasher passwordHasher, + IEnumerable> userValidators, + IEnumerable> passwordValidators, + ILookupNormalizer keyNormalizer, + IdentityErrorDescriber errors, + IServiceProvider services, + ILogger> logger) + : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, + keyNormalizer, errors, services, logger) + { } + + /// + /// return class variable TWO_FACTOR_ENABLED + /// + /// + /// + public override async Task GetTwoFactorEnabledAsync(TUser user) + { + return TWO_FACTOR_ENABLED; + } + + /// + /// return class variable TWO_FACTOR_PROVIDERS + /// + /// + /// + public override async Task> GetValidTwoFactorProvidersAsync(TUser user) + { + return TWO_FACTOR_PROVIDERS; + } + + /// + /// return class variable TWO_FACTOR_TOKEN + /// + /// + /// + /// + public override async Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider) + { + return TWO_FACTOR_TOKEN; + } + + /// + /// return class variable TWO_FACTOR_TOKEN_VERIFIED + /// + /// + /// + /// + /// + public override async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) + { + return TWO_FACTOR_TOKEN_VERIFIED; + } +} From e6b0a80d1a36e1d1845cfa29d49ba725732f17e3 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 14 Oct 2024 22:11:12 -0700 Subject: [PATCH 18/25] Unit Tests --- .../IdentityServer/BaseRequestValidator.cs | 2 +- .../CustomTokenRequestValidator.cs | 21 +- .../IdentityServer/TwoFactorValidator.cs | 11 +- .../IdentityServer/WebAuthnGrantValidator.cs | 20 +- .../ResourceOwnerPasswordValidatorTests.cs | 8 +- .../BaseRequestValidatorTests.cs | 14 +- .../TwoFactorAuthenticationValidatorTests.cs | 206 ++++++++++++++++-- .../Wrappers/UserManagerTestWrapper.cs | 2 +- 8 files changed, 231 insertions(+), 53 deletions(-) diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index b5a4cd8fd776..8f2776ec924d 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -108,7 +108,7 @@ protected async Task ValidateAsync(T context, ValidatedTokenRequest request, if (isTwoFactorRequired) { // 2FA required and not provided response - if (!validTwoFactorRequest || + if (!validTwoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType)) { var resultDict = await _twoFactorAuthenticationValidator diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index c506ec8c84fc..8c2654ca6af6 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -1,9 +1,7 @@ using System.Diagnostics; using System.Security.Claims; using Bit.Core.AdminConsole.Services; -using Bit.Core.Auth.Identity; using Bit.Core.Auth.Models.Api.Response; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; @@ -11,7 +9,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Tokens; using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Validation; using HandlebarsDotNet; @@ -45,19 +42,19 @@ public CustomTokenRequestValidator( IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder ) : base( - userManager, - userService, - eventService, - deviceValidator, + userManager, + userService, + eventService, + deviceValidator, twoFactorAuthenticationValidator, organizationUserRepository, - mailService, - logger, - currentContext, + mailService, + logger, + currentContext, globalSettings, - userRepository, + userRepository, policyService, - featureService, + featureService, ssoConfigRepository, userDecryptionOptionsBuilder) { diff --git a/src/Identity/IdentityServer/TwoFactorValidator.cs b/src/Identity/IdentityServer/TwoFactorValidator.cs index 361567290e94..d78d29e668f5 100644 --- a/src/Identity/IdentityServer/TwoFactorValidator.cs +++ b/src/Identity/IdentityServer/TwoFactorValidator.cs @@ -112,8 +112,8 @@ public async Task> BuildTwoFactorResultAsync(User use foreach (var provider in enabledProviders) { providerKeys.Add((byte)provider.Key); - var infoDict = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); - providers.Add(((byte)provider.Key).ToString(), infoDict); + var twoFactorParams = await BuildTwoFactorParams(organization, user, provider.Key, provider.Value); + providers.Add(((byte)provider.Key).ToString(), twoFactorParams); } var twoFactorResultDict = new Dictionary @@ -140,7 +140,10 @@ public async Task> BuildTwoFactorResultAsync(User use return twoFactorResultDict; } - public async Task VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type, + public async Task VerifyTwoFactor( + User user, + Organization organization, + TwoFactorProviderType type, string token) { switch (type) @@ -267,4 +270,4 @@ private bool OrgUsing2fa(IDictionary orgAbilities, Gu return orgAbilities != null && orgAbilities.ContainsKey(orgId) && orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa; } -} \ No newline at end of file +} diff --git a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs index 1b684aa9be9c..df4b7870e716 100644 --- a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs @@ -46,20 +46,20 @@ public WebAuthnGrantValidator( IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand ) - : base( - userManager, - userService, - eventService, - deviceValidator, + : base( + userManager, + userService, + eventService, + deviceValidator, twoFactorAuthenticationValidator, organizationUserRepository, - mailService, - logger, - currentContext, + mailService, + logger, + currentContext, globalSettings, - userRepository, + userRepository, policyService, - featureService, + featureService, ssoConfigRepository, userDecryptionOptionsBuilder) { diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index 5aa74e74ab81..cd734dba0a62 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -238,10 +238,10 @@ await factory.RegisterAsync(new RegisterRequestModel }); var userManager = factory.GetService>(); await factory.RegisterAsync(new RegisterRequestModel - { - Email = DefaultUsername, - MasterPasswordHash = DefaultPassword - }); + { + Email = DefaultUsername, + MasterPasswordHash = DefaultPassword + }); var user = await userManager.FindByEmailAsync(DefaultUsername); Assert.NotNull(user); diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 43e53a6c7871..ef425dcdcafb 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -31,14 +31,14 @@ public class BaseRequestValidatorTests private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; - private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IPolicyService _policyService; - private readonly IFeatureService _featureService; + private readonly IFeatureService _featureService; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder; @@ -190,7 +190,7 @@ public async Task ValidateAsync_AuthCodeGrantType_DeviceNull_ShouldError( var context = CreateContext(tokenRequest, requestContext, grantResult); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, default))); + .Returns(Task.FromResult(new Tuple(false, default))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -221,7 +221,7 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed( var context = CreateContext(tokenRequest, requestContext, grantResult); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); + .Returns(Task.FromResult(new Tuple(false, null))); context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; @@ -265,7 +265,7 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_Should .Returns(device); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -303,7 +303,7 @@ public async Task ValidateAsync_InvalidAuthType_ShouldSetSsoResult( .Returns(Task.FromResult(true)); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); @@ -330,7 +330,7 @@ public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync( _featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true); _twoFactorAuthenticationValidator .RequiresTwoFactorAsync(Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(new Tuple(false, null))); + .Returns(Task.FromResult(new Tuple(false, null))); // Act await _sut.ValidateAsync(context); diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index a59a2018e1c4..a533deca7c5a 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -1,16 +1,15 @@ -using System.Data; +using Bit.Core; using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Interfaces; +using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Entities; -using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Identity.IdentityServer; using Bit.Identity.Test.Wrappers; @@ -81,7 +80,6 @@ public async void RequiresTwoFactorAsync_IndividualOnly_Required_ReturnTrue( // In order for the two factor authentication to be required, the user must have at least one two factor provider _userManager.TWO_FACTOR_PROVIDERS = ["email"]; - // Act var result = await _sut.RequiresTwoFactorAsync(user, request); @@ -189,7 +187,7 @@ public async void BuildTwoFactorResultAsync_OrganizationProviders_ReturnsNotNull organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); organization.Enabled = true; - user.TwoFactorProviders = ""; + user.TwoFactorProviders = null; // Act var result = await _sut.BuildTwoFactorResultAsync(user, organization); @@ -204,14 +202,16 @@ public async void BuildTwoFactorResultAsync_OrganizationProviders_ReturnsNotNull [Theory] [BitAutoData] public async void BuildTwoFactorResultAsync_IndividualProviders_ReturnsNotNull( - User user, - Organization organization) + User user) { // Arrange - user.TwoFactorProviders = GetTwoFactorIndividualDuoProviderJson(); + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(TwoFactorProviderType.Duo); + + _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) + .Returns(true); // Act - var result = await _sut.BuildTwoFactorResultAsync(user, organization); + var result = await _sut.BuildTwoFactorResultAsync(user, null); // Assert Assert.NotNull(result); @@ -220,7 +220,178 @@ public async void BuildTwoFactorResultAsync_IndividualProviders_ReturnsNotNull( Assert.True(result.ContainsKey("TwoFactorProviders")); } - private UserManagerTestWrapper SubstituteUserManager() + [Theory] + [BitAutoData(TwoFactorProviderType.Email)] + public async void BuildTwoFactorResultAsync_IndividualEmailProvider_SendsEmail_SetsSsoToken_ReturnsNotNull( + TwoFactorProviderType providerType, + User user) + { + // Arrange + var providerTypeInt = (int)providerType; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.SUPPORTS_TWO_FACTOR = true; + _userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()]; + + _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) + .Returns(true); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, null); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders2")); + var providers = (Dictionary>)result["TwoFactorProviders2"]; + Assert.True(providers.ContainsKey(providerTypeInt.ToString())); + Assert.True(result.ContainsKey("SsoEmail2faSessionToken")); + Assert.True(result.ContainsKey("Email")); + + await _userService.Received(1).SendTwoFactorEmailAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.WebAuthn)] + [BitAutoData(TwoFactorProviderType.Email)] + [BitAutoData(TwoFactorProviderType.YubiKey)] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void BuildTwoFactorResultAsync_IndividualProvider_ReturnMatchesType( + TwoFactorProviderType providerType, + User user) + { + // Arrange + var providerTypeInt = (int)providerType; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.SUPPORTS_TWO_FACTOR = true; + _userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()]; + _userManager.TWO_FACTOR_TOKEN = "{\"Key1\":\"WebauthnToken\"}"; + + _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) + .Returns(true); + + // Act + var result = await _sut.BuildTwoFactorResultAsync(user, null); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + Assert.NotEmpty(result); + Assert.True(result.ContainsKey("TwoFactorProviders2")); + var providers = (Dictionary>)result["TwoFactorProviders2"]; + Assert.True(providers.ContainsKey(providerTypeInt.ToString())); + } + + [Theory] + [BitAutoData] + public async void VerifyTwoFactorAsync_Individual_TypeNull_ReturnsFalse( + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + TwoFactorProviderType.Email, user).Returns(true); + + _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + + // Act + var result = await _sut.VerifyTwoFactor( + user, null, default, token); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.WebAuthn)] + [BitAutoData(TwoFactorProviderType.Email)] + [BitAutoData(TwoFactorProviderType.YubiKey)] + [BitAutoData(TwoFactorProviderType.Remember)] + public async void VerifyTwoFactorAsync_Individual_ValidToken_ReturnsTrue( + TwoFactorProviderType providerType, + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + providerType, user).Returns(true); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN = token; + + // Act + var result = await _sut.VerifyTwoFactor(user, null, providerType, token); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( + TwoFactorProviderType providerType, + User user, + Organization organization, + string token) + { + // Arrange + _organizationDuoWebTokenProvider.ValidateAsync( + token, organization, user).Returns(true); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN = token; + + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + // Act + var result = await _sut.VerifyTwoFactor( + user, organization, providerType, token); + + // Assert + Assert.True(result); + } + + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void VerifyTwoFactorAsync_TemporaryDuoService_ReturnsFalse( + TwoFactorProviderType providerType, + User user, + Organization organization, + string token) + { + // Arrange + _featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true); + _userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true); + _temporaryDuoWebV4SDKService.ValidateAsync( + token, Arg.Any(), user).Returns(true); + + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN = token; + + // Act + var result = await _sut.VerifyTwoFactor( + user, organization, providerType, token); + + // Assert + Assert.True(result); + + } + + private static UserManagerTestWrapper SubstituteUserManager() { return new UserManagerTestWrapper( Substitute.For>(), @@ -240,9 +411,16 @@ private static string GetTwoFactorOrganizationDuoProviderJson() "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - private static string GetTwoFactorIndividualDuoProviderJson() + private static string GetTwoFactorIndividualProviderJson(TwoFactorProviderType providerType) { - return - "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + return providerType switch + { + TwoFactorProviderType.Duo => "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}", + TwoFactorProviderType.Email => "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"user@test.dev\"}}}", + TwoFactorProviderType.WebAuthn => "{\"7\":{\"Enabled\":true,\"MetaData\":{\"Key1\":{\"Name\":\"key1\",\"Descriptor\":{\"Type\":0,\"Id\":\"keyId\",\"Transports\":null},\"PublicKey\":\"key\",\"UserHandle\":\"handle\",\"SignatureCounter\":0,\"CredType\":\"none\",\"RegDate\":\"2022-01-01T00:00:00Z\",\"AaGuid\":\"00000000-0000-0000-0000-000000000000\",\"Migrated\":false}}}}", + TwoFactorProviderType.YubiKey => "{\"3\":{\"Enabled\":true,\"MetaData\":{\"Id\":\"yubikeyId\",\"Nfc\":true}}}", + TwoFactorProviderType.OrganizationDuo => "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}", + _ => "{}", + }; } } diff --git a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs index f1207a4b9a26..aa06310a866a 100644 --- a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs +++ b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs @@ -91,6 +91,6 @@ public override async Task GenerateTwoFactorTokenAsync(TUser user, strin /// public override async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) { - return TWO_FACTOR_TOKEN_VERIFIED; + return token == TWO_FACTOR_TOKEN; } } From c702ec4f5b2f3b7daeff35458e1a4e0dee907b1e Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Tue, 15 Oct 2024 16:04:44 -0700 Subject: [PATCH 19/25] Fixing some tests --- .../TwoFactorAuthenticationValidatorTests.cs | 63 ++++++++++++++++++- .../Wrappers/UserManagerTestWrapper.cs | 2 +- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index a533deca7c5a..db2c123c8bee 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -323,7 +323,7 @@ public async void VerifyTwoFactorAsync_Individual_ValidToken_ReturnsTrue( providerType, user).Returns(true); _userManager.TWO_FACTOR_ENABLED = true; - _userManager.TWO_FACTOR_TOKEN = token; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; // Act var result = await _sut.VerifyTwoFactor(user, null, providerType, token); @@ -332,6 +332,31 @@ public async void VerifyTwoFactorAsync_Individual_ValidToken_ReturnsTrue( Assert.True(result); } + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.WebAuthn)] + [BitAutoData(TwoFactorProviderType.Email)] + [BitAutoData(TwoFactorProviderType.YubiKey)] + [BitAutoData(TwoFactorProviderType.Remember)] + public async void VerifyTwoFactorAsync_Individual_InvalidToken_ReturnsTrue( + TwoFactorProviderType providerType, + User user, + string token) + { + // Arrange + _userService.TwoFactorProviderIsEnabledAsync( + providerType, user).Returns(true); + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = false; + + // Act + var result = await _sut.VerifyTwoFactor(user, null, providerType, token); + + // Assert + Assert.False(result); + } + [Theory] [BitAutoData(TwoFactorProviderType.OrganizationDuo)] public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( @@ -345,7 +370,7 @@ public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( token, organization, user).Returns(true); _userManager.TWO_FACTOR_ENABLED = true; - _userManager.TWO_FACTOR_TOKEN = token; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; organization.Use2fa = true; organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); @@ -362,7 +387,7 @@ public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( [Theory] [BitAutoData(TwoFactorProviderType.Duo)] [BitAutoData(TwoFactorProviderType.OrganizationDuo)] - public async void VerifyTwoFactorAsync_TemporaryDuoService_ReturnsFalse( + public async void VerifyTwoFactorAsync_TemporaryDuoService_ValidToken_ReturnsTrue( TwoFactorProviderType providerType, User user, Organization organization, @@ -381,6 +406,7 @@ public async void VerifyTwoFactorAsync_TemporaryDuoService_ReturnsFalse( _userManager.TWO_FACTOR_ENABLED = true; _userManager.TWO_FACTOR_TOKEN = token; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; // Act var result = await _sut.VerifyTwoFactor( @@ -388,7 +414,38 @@ public async void VerifyTwoFactorAsync_TemporaryDuoService_ReturnsFalse( // Assert Assert.True(result); + } + [Theory] + [BitAutoData(TwoFactorProviderType.Duo)] + [BitAutoData(TwoFactorProviderType.OrganizationDuo)] + public async void VerifyTwoFactorAsync_TemporaryDuoService_InvalidToken_ReturnsFalse( + TwoFactorProviderType providerType, + User user, + Organization organization, + string token) + { + // Arrange + _featureService.IsEnabled(FeatureFlagKeys.DuoRedirect).Returns(true); + _userService.TwoFactorProviderIsEnabledAsync(providerType, user).Returns(true); + _temporaryDuoWebV4SDKService.ValidateAsync( + token, Arg.Any(), user).Returns(true); + + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); + organization.Use2fa = true; + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProviderJson(); + organization.Enabled = true; + + _userManager.TWO_FACTOR_ENABLED = true; + _userManager.TWO_FACTOR_TOKEN = token; + _userManager.TWO_FACTOR_TOKEN_VERIFIED = false; + + // Act + var result = await _sut.VerifyTwoFactor( + user, organization, providerType, token); + + // Assert + Assert.True(result); } private static UserManagerTestWrapper SubstituteUserManager() diff --git a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs index aa06310a866a..f1207a4b9a26 100644 --- a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs +++ b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs @@ -91,6 +91,6 @@ public override async Task GenerateTwoFactorTokenAsync(TUser user, strin /// public override async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) { - return token == TWO_FACTOR_TOKEN; + return TWO_FACTOR_TOKEN_VERIFIED; } } From 45b5a0454d865a422db49afc17646fd914d73788 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Wed, 25 Sep 2024 12:56:02 -0700 Subject: [PATCH 20/25] replace duo token provider --- .../Models/Request/TwoFactorRequestModels.cs | 52 ++------ .../TwoFactor/TwoFactorDuoResponseModel.cs | 65 +--------- src/Core/Auth/Identity/DuoTokenProvider.cs | 119 ++++++++++++++++++ .../Identity/DuoUniversalTokenProvider.cs | 79 ++++++++++++ src/Core/Auth/Identity/DuoWebTokenProvider.cs | 86 ------------- .../OrganizationDuoUniversalTokenProvider.cs | 79 ++++++++++++ .../OrganizationDuoWebTokenProvider.cs | 76 ----------- .../IdentityServer/BaseRequestValidator.cs | 7 +- .../CustomTokenRequestValidator.cs | 2 + .../ResourceOwnerPasswordValidator.cs | 2 + .../IdentityServer/WebAuthnGrantValidator.cs | 3 + .../Utilities/ServiceCollectionExtensions.cs | 6 +- 12 files changed, 304 insertions(+), 272 deletions(-) create mode 100644 src/Core/Auth/Identity/DuoTokenProvider.cs create mode 100644 src/Core/Auth/Identity/DuoUniversalTokenProvider.cs delete mode 100644 src/Core/Auth/Identity/DuoWebTokenProvider.cs create mode 100644 src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs delete mode 100644 src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs diff --git a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs index f2f01a2378e5..f62d2ae4ecdc 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -42,20 +42,12 @@ public User ToUser(User existingUser) public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject { - /* - To support both v2 and v4 we need to remove the required annotation from the properties. - todo - the required annotation will be added back in PM-8107. - */ + [Required] [StringLength(50)] public string ClientId { get; set; } + [Required] [StringLength(50)] public string ClientSecret { get; set; } - //todo - will remove SKey and IKey with PM-8107 - [StringLength(50)] - public string IntegrationKey { get; set; } - //todo - will remove SKey and IKey with PM-8107 - [StringLength(50)] - public string SecretKey { get; set; } [Required] [StringLength(50)] public string Host { get; set; } @@ -65,22 +57,17 @@ public User ToUser(User existingUser) var providers = existingUser.GetTwoFactorProviders(); if (providers == null) { - providers = new Dictionary(); + providers = []; } else if (providers.ContainsKey(TwoFactorProviderType.Duo)) { providers.Remove(TwoFactorProviderType.Duo); } - Temporary_SyncDuoParams(); - providers.Add(TwoFactorProviderType.Duo, new TwoFactorProvider { MetaData = new Dictionary { - //todo - will remove SKey and IKey with PM-8107 - ["SKey"] = SecretKey, - ["IKey"] = IntegrationKey, ["ClientSecret"] = ClientSecret, ["ClientId"] = ClientId, ["Host"] = Host @@ -96,22 +83,17 @@ public Organization ToOrganization(Organization existingOrg) var providers = existingOrg.GetTwoFactorProviders(); if (providers == null) { - providers = new Dictionary(); + providers = []; } else if (providers.ContainsKey(TwoFactorProviderType.OrganizationDuo)) { providers.Remove(TwoFactorProviderType.OrganizationDuo); } - Temporary_SyncDuoParams(); - providers.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider { MetaData = new Dictionary { - //todo - will remove SKey and IKey with PM-8107 - ["SKey"] = SecretKey, - ["IKey"] = IntegrationKey, ["ClientSecret"] = ClientSecret, ["ClientId"] = ClientId, ["Host"] = Host @@ -124,33 +106,13 @@ public Organization ToOrganization(Organization existingOrg) public override IEnumerable Validate(ValidationContext validationContext) { - if (!DuoApi.ValidHost(Host)) + if (!DuoApi.ValidHost(Host)) // TODO replace with DuoUniversal { yield return new ValidationResult("Host is invalid.", [nameof(Host)]); } - if (string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId) && - string.IsNullOrWhiteSpace(SecretKey) && string.IsNullOrWhiteSpace(IntegrationKey)) - { - yield return new ValidationResult("Neither v2 or v4 values are valid.", [nameof(IntegrationKey), nameof(SecretKey), nameof(ClientSecret), nameof(ClientId)]); - } - } - - /* - use this method to ensure that both v2 params and v4 params are in sync - todo will be removed in pm-8107 - */ - private void Temporary_SyncDuoParams() - { - // Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret - if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId)) - { - SecretKey = ClientSecret; - IntegrationKey = ClientId; - } - else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey)) + if (string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId)) { - ClientSecret = SecretKey; - ClientId = IntegrationKey; + yield return new ValidationResult("ClientSecret or ClientId are invalid", [nameof(ClientSecret), nameof(ClientId)]); } } } diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs index 8b8c36d2e8b4..d17ee7c8e03d 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs @@ -13,10 +13,7 @@ public class TwoFactorDuoResponseModel : ResponseModel public TwoFactorDuoResponseModel(User user) : base(ResponseObj) { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } + ArgumentNullException.ThrowIfNull(user); var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); Build(provider); @@ -25,10 +22,7 @@ public TwoFactorDuoResponseModel(User user) public TwoFactorDuoResponseModel(Organization org) : base(ResponseObj) { - if (org == null) - { - throw new ArgumentNullException(nameof(org)); - } + ArgumentNullException.ThrowIfNull(org); var provider = org.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); Build(provider); @@ -36,14 +30,9 @@ public TwoFactorDuoResponseModel(Organization org) public bool Enabled { get; set; } public string Host { get; set; } - //TODO - will remove SecretKey with PM-8107 - public string SecretKey { get; set; } - //TODO - will remove IntegrationKey with PM-8107 - public string IntegrationKey { get; set; } public string ClientSecret { get; set; } public string ClientId { get; set; } - // updated build to assist in the EDD migration for the Duo 2FA provider private void Build(TwoFactorProvider provider) { if (provider?.MetaData != null && provider.MetaData.Count > 0) @@ -54,36 +43,13 @@ private void Build(TwoFactorProvider provider) { Host = (string)host; } - - //todo - will remove SKey and IKey with PM-8107 - // check Skey and IKey first if they exist - if (provider.MetaData.TryGetValue("SKey", out var sKey)) - { - ClientSecret = MaskKey((string)sKey); - SecretKey = MaskKey((string)sKey); - } - if (provider.MetaData.TryGetValue("IKey", out var iKey)) - { - IntegrationKey = (string)iKey; - ClientId = (string)iKey; - } - - // Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret if (provider.MetaData.TryGetValue("ClientSecret", out var clientSecret)) { - if (!string.IsNullOrWhiteSpace((string)clientSecret)) - { - ClientSecret = MaskKey((string)clientSecret); - SecretKey = MaskKey((string)clientSecret); - } + ClientSecret = MaskKey((string)clientSecret); } if (provider.MetaData.TryGetValue("ClientId", out var clientId)) { - if (!string.IsNullOrWhiteSpace((string)clientId)) - { - ClientId = (string)clientId; - IntegrationKey = (string)clientId; - } + ClientId = (string)clientId; } } else @@ -92,29 +58,6 @@ private void Build(TwoFactorProvider provider) } } - /* - use this method to ensure that both v2 params and v4 params are in sync - todo will be removed in pm-8107 - */ - private void Temporary_SyncDuoParams() - { - // Even if IKey and SKey exist prioritize v4 params ClientId and ClientSecret - if (!string.IsNullOrWhiteSpace(ClientSecret) && !string.IsNullOrWhiteSpace(ClientId)) - { - SecretKey = ClientSecret; - IntegrationKey = ClientId; - } - else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey)) - { - ClientSecret = SecretKey; - ClientId = IntegrationKey; - } - else - { - throw new InvalidDataException("Invalid Duo parameters."); - } - } - private static string MaskKey(string key) { if (string.IsNullOrWhiteSpace(key) || key.Length <= 6) diff --git a/src/Core/Auth/Identity/DuoTokenProvider.cs b/src/Core/Auth/Identity/DuoTokenProvider.cs new file mode 100644 index 000000000000..df1621a01124 --- /dev/null +++ b/src/Core/Auth/Identity/DuoTokenProvider.cs @@ -0,0 +1,119 @@ +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Duo = DuoUniversal; + +namespace Bit.Core.Auth.Identity; + +/// +/// In the TwoFactorController before we write a configuration to the database we check the configuration +/// this interface creates a simple way to inject the process into those endpoints. +/// +public interface IDuoTokenProvider +{ + Task BuildDuoClientAsync(TwoFactorProvider provider); +} + +/// +/// OrganizationDuo and Duo types both use the same flows so both of those Token Providers will +/// inherit from this class +/// +public class DuoTokenProvider : IDuoTokenProvider +{ + private readonly ICurrentContext _currentContext; + private readonly GlobalSettings _globalSettings; + + public DuoTokenProvider(ICurrentContext currentContext, GlobalSettings globalSettings) + { + _currentContext = currentContext; + _globalSettings = globalSettings; + } + + protected bool HasProperMetaData(TwoFactorProvider provider) + { + return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") && + provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host"); + } + + protected async Task GenerateAuthUrlAsync( + TwoFactorProvider provider, + IDataProtectorTokenFactory tokenDataFactory, + User user) + { + var duoClient = await BuildDuoClientAsync(provider); + if (duoClient == null) + { + return null; + } + + var state = tokenDataFactory.Protect(new DuoUserStateTokenable(user)); + var authUrl = duoClient.GenerateAuthUri(user.Email, state); + + return authUrl; + } + + /// + /// Makes the request to Duo to validate the authCode and state token + /// + /// Duo or OrganizationDuo + /// Factory for decrypting the state + /// self + /// token received from the client + /// boolean based on result from Duo + protected async Task RequestDuoValidationAsync( + TwoFactorProvider provider, + IDataProtectorTokenFactory tokenDataFactory, + User user, + string token) + { + var duoClient = await BuildDuoClientAsync(provider); + if (duoClient == null) + { + return false; + } + + var parts = token.Split("|"); + var authCode = parts[0]; + var state = parts[1]; + tokenDataFactory.TryUnprotect(state, out var tokenable); + if (!tokenable.Valid || !tokenable.TokenIsValid(user)) + { + return false; + } + + // duoClient compares the email from the received IdToken with user.Email to verify a bad actor hasn't used + // their authCode with a victims credentials + var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(authCode, user.Email); + // If the result of the exchange doesn't throw an exception and it's not null, then it's valid + return res.AuthResult.Result == "allow"; + } + + /// + /// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation + /// + /// TwoFactorProvider Duo or OrganizationDuo + /// Duo.Client object or null + public async Task BuildDuoClientAsync(TwoFactorProvider provider) + { + // Fetch Client name from header value since duo auth can be initiated from multiple clients and we want + // to redirect back to the initiating client + _currentContext.HttpContext.Request.Headers.TryGetValue("Bitwarden-Client-Name", out var bitwardenClientName); + var redirectUri = string.Format("{0}/duo-redirect-connector.html?client={1}", + _globalSettings.BaseServiceUri.Vault, bitwardenClientName.FirstOrDefault() ?? "web"); + + var client = new Duo.ClientBuilder( + (string)provider.MetaData["ClientId"], + (string)provider.MetaData["ClientSecret"], + (string)provider.MetaData["Host"], + redirectUri).Build(); + + if (!await client.DoHealthCheck(false)) + { + return null; + } + return client; + } +} \ No newline at end of file diff --git a/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs new file mode 100644 index 000000000000..6f1344f63b71 --- /dev/null +++ b/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs @@ -0,0 +1,79 @@ +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Auth.Identity; + +public class DuoUniversalTokenProvider : DuoTokenProvider, IUserTwoFactorTokenProvider +{ + private readonly IServiceProvider _serviceProvider; + + public DuoUniversalTokenProvider( + IServiceProvider serviceProvider, + GlobalSettings globalSettings, + ICurrentContext currentContext) + : base(currentContext, globalSettings) + { + _serviceProvider = serviceProvider; + } + + public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + { + var provider = await GetTwoFactorProvideAsync(user); + if (provider == null) + { + return false; + } + + var userService = _serviceProvider.GetRequiredService(); + return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user); + } + + public async Task GenerateAsync(string purpose, UserManager manager, User user) + { + var provider = await GetTwoFactorProvideAsync(user); + if (provider == null) + { + return null; + } + + var tokenDataFactory = _serviceProvider.GetRequiredService>(); + return await GenerateAuthUrlAsync(provider, tokenDataFactory, user); + } + + public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) + { + var provider = await GetTwoFactorProvideAsync(user); + if (provider == null) + { + return false; + } + + var tokenDataFactory = _serviceProvider.GetRequiredService>(); + return await RequestDuoValidationAsync(provider, tokenDataFactory, user, token); + } + + private async Task GetTwoFactorProvideAsync(User user) + { + var userService = _serviceProvider.GetRequiredService(); + if (!await userService.CanAccessPremium(user)) + { + return null; + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); + if (!HasProperMetaData(provider)) + { + return null; + } + + return provider; + } +} diff --git a/src/Core/Auth/Identity/DuoWebTokenProvider.cs b/src/Core/Auth/Identity/DuoWebTokenProvider.cs deleted file mode 100644 index 6ab020326284..000000000000 --- a/src/Core/Auth/Identity/DuoWebTokenProvider.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; -using Bit.Core.Auth.Utilities.Duo; -using Bit.Core.Entities; -using Bit.Core.Services; -using Bit.Core.Settings; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; - -namespace Bit.Core.Auth.Identity; - -public class DuoWebTokenProvider : IUserTwoFactorTokenProvider -{ - private readonly IServiceProvider _serviceProvider; - private readonly GlobalSettings _globalSettings; - - public DuoWebTokenProvider( - IServiceProvider serviceProvider, - GlobalSettings globalSettings) - { - _serviceProvider = serviceProvider; - _globalSettings = globalSettings; - } - - public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) - { - var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) - { - return false; - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) - { - return false; - } - - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user); - } - - public async Task GenerateAsync(string purpose, UserManager manager, User user) - { - var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) - { - return null; - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) - { - return null; - } - - var signatureRequest = DuoWeb.SignRequest((string)provider.MetaData["IKey"], - (string)provider.MetaData["SKey"], _globalSettings.Duo.AKey, user.Email); - return signatureRequest; - } - - public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) - { - var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) - { - return false; - } - - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) - { - return false; - } - - var response = DuoWeb.VerifyResponse((string)provider.MetaData["IKey"], (string)provider.MetaData["SKey"], - _globalSettings.Duo.AKey, token); - - return response == user.Email; - } - - private bool HasProperMetaData(TwoFactorProvider provider) - { - return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") && - provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host"); - } -} diff --git a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs new file mode 100644 index 000000000000..df52946d0786 --- /dev/null +++ b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs @@ -0,0 +1,79 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Auth.Identity; + +public interface IOrganizationDuoUniversalTokenProvider : IOrganizationTwoFactorTokenProvider { } + +public class OrganizationDuoUniversalTokenProvider : DuoTokenProvider, IOrganizationDuoUniversalTokenProvider +{ + private readonly IServiceProvider _serviceProvider; + public OrganizationDuoUniversalTokenProvider( + GlobalSettings globalSettings, + IServiceProvider serviceProvider, + ICurrentContext currentContext + ) : base(currentContext, globalSettings) + { + _serviceProvider = serviceProvider; + } + + public Task CanGenerateTwoFactorTokenAsync(Organization organization) + { + if (organization == null || !organization.Enabled || !organization.Use2fa) + { + return Task.FromResult(false); + } + + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo) + && HasProperMetaData(provider); + return Task.FromResult(canGenerate); + } + + public async Task GenerateAsync(Organization organization, User user) + { + var provider = GetTwoFactorProvider(organization); + if (provider == null) + { + return null; + } + + var tokenDataFactory = _serviceProvider.GetRequiredService>(); + return await GenerateAuthUrlAsync(provider, tokenDataFactory, user); + } + + public async Task ValidateAsync(string token, Organization organization, User user) + { + var provider = GetTwoFactorProvider(organization); + if (provider == null) + { + return false; + } + + var tokenDataFactory = _serviceProvider.GetRequiredService>(); + return await RequestDuoValidationAsync(provider, tokenDataFactory, user, token); + } + + private TwoFactorProvider GetTwoFactorProvider(Organization organization) + { + if (organization == null || !organization.Enabled || !organization.Use2fa) + { + return null; + } + + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + if (!HasProperMetaData(provider)) + { + return null; + } + + return provider; + } +} diff --git a/src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs b/src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs deleted file mode 100644 index 58bcf5efd8db..000000000000 --- a/src/Core/Auth/Identity/OrganizationDuoWebTokenProvider.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; -using Bit.Core.Auth.Utilities.Duo; -using Bit.Core.Entities; -using Bit.Core.Settings; - -namespace Bit.Core.Auth.Identity; - -public interface IOrganizationDuoWebTokenProvider : IOrganizationTwoFactorTokenProvider { } - -public class OrganizationDuoWebTokenProvider : IOrganizationDuoWebTokenProvider -{ - private readonly GlobalSettings _globalSettings; - - public OrganizationDuoWebTokenProvider(GlobalSettings globalSettings) - { - _globalSettings = globalSettings; - } - - public Task CanGenerateTwoFactorTokenAsync(Organization organization) - { - if (organization == null || !organization.Enabled || !organization.Use2fa) - { - return Task.FromResult(false); - } - - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo) - && HasProperMetaData(provider); - return Task.FromResult(canGenerate); - } - - public Task GenerateAsync(Organization organization, User user) - { - if (organization == null || !organization.Enabled || !organization.Use2fa) - { - return Task.FromResult(null); - } - - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - if (!HasProperMetaData(provider)) - { - return Task.FromResult(null); - } - - var signatureRequest = DuoWeb.SignRequest(provider.MetaData["IKey"].ToString(), - provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, user.Email); - return Task.FromResult(signatureRequest); - } - - public Task ValidateAsync(string token, Organization organization, User user) - { - if (organization == null || !organization.Enabled || !organization.Use2fa) - { - return Task.FromResult(false); - } - - var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - if (!HasProperMetaData(provider)) - { - return Task.FromResult(false); - } - - var response = DuoWeb.VerifyResponse(provider.MetaData["IKey"].ToString(), - provider.MetaData["SKey"].ToString(), _globalSettings.Duo.AKey, token); - - return Task.FromResult(response == user.Email); - } - - private bool HasProperMetaData(TwoFactorProvider provider) - { - return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") && - provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host"); - } -} diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index 8f2776ec924d..e4552beacdf6 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -27,6 +27,7 @@ public abstract class BaseRequestValidator where T : class private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; + private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoWebTokenProvider; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; private readonly ILogger _logger; @@ -46,6 +47,8 @@ public BaseRequestValidator( IEventService eventService, IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, + IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IMailService mailService, ILogger logger, @@ -62,6 +65,8 @@ public BaseRequestValidator( _eventService = eventService; _deviceValidator = deviceValidator; _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator; + _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; + _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _mailService = mailService; _logger = logger; @@ -326,7 +331,7 @@ private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid } /// - /// checks to see if a user is trying to log into a new device + /// checks to see if a user is trying to log into a new device /// and has reached the maximum number of failed login attempts. /// /// boolean diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index 8c2654ca6af6..1385e362f15b 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -30,6 +30,7 @@ public CustomTokenRequestValidator( IEventService eventService, IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IOrganizationUserRepository organizationUserRepository, IMailService mailService, ILogger logger, @@ -47,6 +48,7 @@ IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder eventService, deviceValidator, twoFactorAuthenticationValidator, + organizationDuoWebTokenProvider, organizationUserRepository, mailService, logger, diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs index 6e488bb48255..f11e15f2029c 100644 --- a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -29,6 +29,7 @@ public ResourceOwnerPasswordValidator( IEventService eventService, IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IOrganizationUserRepository organizationUserRepository, IMailService mailService, ILogger logger, @@ -47,6 +48,7 @@ public ResourceOwnerPasswordValidator( eventService, deviceValidator, twoFactorAuthenticationValidator, + organizationDuoWebTokenProvider, organizationUserRepository, mailService, logger, diff --git a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs index df4b7870e716..e8b06f5131d7 100644 --- a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs @@ -33,6 +33,8 @@ public WebAuthnGrantValidator( IEventService eventService, IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, + IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IMailService mailService, ILogger logger, @@ -53,6 +55,7 @@ IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand deviceValidator, twoFactorAuthenticationValidator, organizationUserRepository, + organizationDuoWebTokenProvider, mailService, logger, currentContext, diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index bd3aecf2f5f7..b2928d96af9e 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -369,8 +369,8 @@ public static void AddNoopServices(this IServiceCollection services) public static IdentityBuilder AddCustomIdentityServices( this IServiceCollection services, GlobalSettings globalSettings) { - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.Configure(options => options.IterationCount = 100000); services.Configure(options => { @@ -411,7 +411,7 @@ public static IdentityBuilder AddCustomIdentityServices( CoreHelpers.CustomProviderName(TwoFactorProviderType.Email)) .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey)) - .AddTokenProvider( + .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.Duo)) .AddTokenProvider( CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)) From caac7c753bc7bafe2c40330979e5c700033bdc2c Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Thu, 26 Sep 2024 11:55:21 -0700 Subject: [PATCH 21/25] removed last bits and replaced validation --- .../Auth/Controllers/TwoFactorController.cs | 39 +-- .../Models/Request/TwoFactorRequestModels.cs | 13 - src/Core/Auth/Identity/DuoTokenProvider.cs | 31 +- src/Core/Auth/Utilities/DuoApi.cs | 277 ------------------ src/Core/Auth/Utilities/DuoUtilities.cs | 16 + src/Core/Auth/Utilities/DuoWeb.cs | 240 --------------- 6 files changed, 47 insertions(+), 569 deletions(-) delete mode 100644 src/Core/Auth/Utilities/DuoApi.cs create mode 100644 src/Core/Auth/Utilities/DuoUtilities.cs delete mode 100644 src/Core/Auth/Utilities/DuoWeb.cs diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 149237e2887c..10762d530e92 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -4,9 +4,9 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Response; using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -29,11 +29,11 @@ public class TwoFactorController : Controller private readonly IUserService _userService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationService _organizationService; - private readonly GlobalSettings _globalSettings; private readonly UserManager _userManager; private readonly ICurrentContext _currentContext; private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand; private readonly IFeatureService _featureService; + private readonly IDuoTokenProvider _duoTokenProvider; private readonly IDataProtectorTokenFactory _twoFactorAuthenticatorDataProtector; private readonly IDataProtectorTokenFactory _ssoEmailTwoFactorSessionDataProtector; @@ -46,17 +46,18 @@ public TwoFactorController( ICurrentContext currentContext, IVerifyAuthRequestCommand verifyAuthRequestCommand, IFeatureService featureService, + IDuoTokenProvider duoTokenProvider, IDataProtectorTokenFactory twoFactorAuthenticatorDataProtector, IDataProtectorTokenFactory ssoEmailTwoFactorSessionDataProtector) { _userService = userService; _organizationRepository = organizationRepository; _organizationService = organizationService; - _globalSettings = globalSettings; _userManager = userManager; _currentContext = currentContext; _verifyAuthRequestCommand = verifyAuthRequestCommand; _featureService = featureService; + _duoTokenProvider = duoTokenProvider; _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector; _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector; } @@ -184,21 +185,7 @@ public async Task GetDuo([FromBody] SecretVerificatio public async Task PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model) { var user = await CheckAsync(model, true); - try - { - // for backwards compatibility - will be removed with PM-8107 - DuoApi duoApi = null; - if (model.ClientId != null && model.ClientSecret != null) - { - duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host); - } - else - { - duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host); - } - await duoApi.JSONApiCall("GET", "/auth/v2/check"); - } - catch (DuoException) + if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); @@ -241,21 +228,7 @@ public async Task PutOrganizationDuo(string id, } var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException(); - try - { - // for backwards compatibility - will be removed with PM-8107 - DuoApi duoApi = null; - if (model.ClientId != null && model.ClientSecret != null) - { - duoApi = new DuoApi(model.ClientId, model.ClientSecret, model.Host); - } - else - { - duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host); - } - await duoApi.JSONApiCall("GET", "/auth/v2/check"); - } - catch (DuoException) + if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); diff --git a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs index f62d2ae4ecdc..7a280d526bc0 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; -using Bit.Core.Auth.Utilities; using Bit.Core.Entities; using Fido2NetLib; @@ -103,18 +102,6 @@ public Organization ToOrganization(Organization existingOrg) existingOrg.SetTwoFactorProviders(providers); return existingOrg; } - - public override IEnumerable Validate(ValidationContext validationContext) - { - if (!DuoApi.ValidHost(Host)) // TODO replace with DuoUniversal - { - yield return new ValidationResult("Host is invalid.", [nameof(Host)]); - } - if (string.IsNullOrWhiteSpace(ClientSecret) && string.IsNullOrWhiteSpace(ClientId)) - { - yield return new ValidationResult("ClientSecret or ClientId are invalid", [nameof(ClientSecret), nameof(ClientId)]); - } - } } public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestModel, IValidatableObject diff --git a/src/Core/Auth/Identity/DuoTokenProvider.cs b/src/Core/Auth/Identity/DuoTokenProvider.cs index df1621a01124..69fa78b0fc1b 100644 --- a/src/Core/Auth/Identity/DuoTokenProvider.cs +++ b/src/Core/Auth/Identity/DuoTokenProvider.cs @@ -1,5 +1,6 @@ using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Settings; @@ -14,7 +15,7 @@ namespace Bit.Core.Auth.Identity; /// public interface IDuoTokenProvider { - Task BuildDuoClientAsync(TwoFactorProvider provider); + Task ValidateDuoConfiguration(string clientId, string clientSecret, string host); } /// @@ -34,8 +35,11 @@ public DuoTokenProvider(ICurrentContext currentContext, GlobalSettings globalSet protected bool HasProperMetaData(TwoFactorProvider provider) { - return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") && - provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host"); + return provider?.MetaData != null && + provider.MetaData.ContainsKey("ClientId") && + provider.MetaData.ContainsKey("ClientSecret") && + provider.MetaData.ContainsKey("Host") && + DuoUtilities.ValidHost((string)provider.MetaData["Host"]); } protected async Task GenerateAuthUrlAsync( @@ -43,7 +47,7 @@ protected async Task GenerateAuthUrlAsync( IDataProtectorTokenFactory tokenDataFactory, User user) { - var duoClient = await BuildDuoClientAsync(provider); + var duoClient = await BuildDuoTwoFactorClientAsync(provider); if (duoClient == null) { return null; @@ -69,7 +73,7 @@ protected async Task RequestDuoValidationAsync( User user, string token) { - var duoClient = await BuildDuoClientAsync(provider); + var duoClient = await BuildDuoTwoFactorClientAsync(provider); if (duoClient == null) { return false; @@ -96,7 +100,7 @@ protected async Task RequestDuoValidationAsync( /// /// TwoFactorProvider Duo or OrganizationDuo /// Duo.Client object or null - public async Task BuildDuoClientAsync(TwoFactorProvider provider) + protected async Task BuildDuoTwoFactorClientAsync(TwoFactorProvider provider) { // Fetch Client name from header value since duo auth can be initiated from multiple clients and we want // to redirect back to the initiating client @@ -116,4 +120,19 @@ protected async Task RequestDuoValidationAsync( } return client; } + + /// + /// Generates a Duo.Client object for use with Duo SDK v4. This combines the health check and the client generation + /// + /// + /// + /// + /// + public async Task ValidateDuoConfiguration(string clientId, string clientSecret, string host) + { + // The AuthURI isn't important for this health check so we pass in a non-empty string + var client = new Duo.ClientBuilder(clientId, clientSecret, host, "non-empty").Build(); + + return await client.DoHealthCheck(false); + } } \ No newline at end of file diff --git a/src/Core/Auth/Utilities/DuoApi.cs b/src/Core/Auth/Utilities/DuoApi.cs deleted file mode 100644 index 8bf5f16a91b4..000000000000 --- a/src/Core/Auth/Utilities/DuoApi.cs +++ /dev/null @@ -1,277 +0,0 @@ -/* -Original source modified from https://github.com/duosecurity/duo_api_csharp - -============================================================================= -============================================================================= - -Copyright (c) 2018 Duo Security -All rights reserved -*/ - -using System.Globalization; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Text.RegularExpressions; -using System.Web; -using Bit.Core.Models.Api.Response.Duo; - -namespace Bit.Core.Auth.Utilities; - -public class DuoApi -{ - private const string UrlScheme = "https"; - private const string UserAgent = "Bitwarden_DuoAPICSharp/1.0 (.NET Core)"; - - private readonly string _host; - private readonly string _ikey; - private readonly string _skey; - - private readonly HttpClient _httpClient = new(); - - public DuoApi(string ikey, string skey, string host) - { - _ikey = ikey; - _skey = skey; - _host = host; - - if (!ValidHost(host)) - { - throw new DuoException("Invalid Duo host configured.", new ArgumentException(nameof(host))); - } - } - - public static bool ValidHost(string host) - { - if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri)) - { - return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") && - uri.Host.StartsWith("api-") && - (uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com")); - } - return false; - } - - public static string CanonicalizeParams(Dictionary parameters) - { - var ret = new List(); - foreach (var pair in parameters) - { - var p = string.Format("{0}={1}", HttpUtility.UrlEncode(pair.Key), HttpUtility.UrlEncode(pair.Value)); - // Signatures require upper-case hex digits. - p = Regex.Replace(p, "(%[0-9A-Fa-f][0-9A-Fa-f])", c => c.Value.ToUpperInvariant()); - // Escape only the expected characters. - p = Regex.Replace(p, "([!'()*])", c => "%" + Convert.ToByte(c.Value[0]).ToString("X")); - p = p.Replace("%7E", "~"); - // UrlEncode converts space (" ") to "+". The - // signature algorithm requires "%20" instead. Actual - // + has already been replaced with %2B. - p = p.Replace("+", "%20"); - ret.Add(p); - } - - ret.Sort(StringComparer.Ordinal); - return string.Join("&", ret.ToArray()); - } - - protected string CanonicalizeRequest(string method, string path, string canonParams, string date) - { - string[] lines = { - date, - method.ToUpperInvariant(), - _host.ToLower(), - path, - canonParams, - }; - return string.Join("\n", lines); - } - - public string Sign(string method, string path, string canonParams, string date) - { - var canon = CanonicalizeRequest(method, path, canonParams, date); - var sig = HmacSign(canon); - var auth = string.Concat(_ikey, ':', sig); - return string.Concat("Basic ", Encode64(auth)); - } - - /// The request timeout, in milliseconds. - /// Specify 0 to use the system-default timeout. Use caution if - /// you choose to specify a custom timeout - some API - /// calls (particularly in the Auth APIs) will not - /// return a response until an out-of-band authentication process - /// has completed. In some cases, this may take as much as a - /// small number of minutes. - private async Task<(string result, HttpStatusCode statusCode)> ApiCall(string method, string path, Dictionary parameters, int timeout) - { - if (parameters == null) - { - parameters = new Dictionary(); - } - - var canonParams = CanonicalizeParams(parameters); - var query = string.Empty; - if (!method.Equals("POST") && !method.Equals("PUT")) - { - if (parameters.Count > 0) - { - query = "?" + canonParams; - } - } - var url = $"{UrlScheme}://{_host}{path}{query}"; - - var dateString = RFC822UtcNow(); - var auth = Sign(method, path, canonParams, dateString); - - var request = new HttpRequestMessage - { - Method = new HttpMethod(method), - RequestUri = new Uri(url), - }; - request.Headers.Add("Authorization", auth); - request.Headers.Add("X-Duo-Date", dateString); - request.Headers.UserAgent.ParseAdd(UserAgent); - - if (timeout > 0) - { - _httpClient.Timeout = TimeSpan.FromMilliseconds(timeout); - } - - if (method.Equals("POST") || method.Equals("PUT")) - { - request.Content = new StringContent(canonParams, Encoding.UTF8, "application/x-www-form-urlencoded"); - } - - var response = await _httpClient.SendAsync(request); - var result = await response.Content.ReadAsStringAsync(); - var statusCode = response.StatusCode; - return (result, statusCode); - } - - public async Task JSONApiCall(string method, string path, Dictionary parameters = null) - { - return await JSONApiCall(method, path, parameters, 0); - } - - /// The request timeout, in milliseconds. - /// Specify 0 to use the system-default timeout. Use caution if - /// you choose to specify a custom timeout - some API - /// calls (particularly in the Auth APIs) will not - /// return a response until an out-of-band authentication process - /// has completed. In some cases, this may take as much as a - /// small number of minutes. - private async Task JSONApiCall(string method, string path, Dictionary parameters, int timeout) - { - var (res, statusCode) = await ApiCall(method, path, parameters, timeout); - try - { - var obj = JsonSerializer.Deserialize(res); - if (obj.Stat == "OK") - { - return obj.Response; - } - - throw new ApiException(obj.Code ?? 0, (int)statusCode, obj.Message, obj.MessageDetail); - } - catch (ApiException) - { - throw; - } - catch (Exception e) - { - throw new BadResponseException((int)statusCode, e); - } - } - - private int? ToNullableInt(string s) - { - int i; - if (int.TryParse(s, out i)) - { - return i; - } - return null; - } - - private string HmacSign(string data) - { - var keyBytes = Encoding.ASCII.GetBytes(_skey); - var dataBytes = Encoding.ASCII.GetBytes(data); - - using (var hmac = new HMACSHA1(keyBytes)) - { - var hash = hmac.ComputeHash(dataBytes); - var hex = BitConverter.ToString(hash); - return hex.Replace("-", string.Empty).ToLower(); - } - } - - private static string Encode64(string plaintext) - { - var plaintextBytes = Encoding.ASCII.GetBytes(plaintext); - return Convert.ToBase64String(plaintextBytes); - } - - private static string RFC822UtcNow() - { - // Can't use the "zzzz" format because it adds a ":" - // between the offset's hours and minutes. - var dateString = DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture); - var offset = 0; - var zone = "+" + offset.ToString(CultureInfo.InvariantCulture).PadLeft(2, '0'); - dateString += " " + zone.PadRight(5, '0'); - return dateString; - } -} - -public class DuoException : Exception -{ - public int HttpStatus { get; private set; } - - public DuoException(string message, Exception inner) - : base(message, inner) - { } - - public DuoException(int httpStatus, string message, Exception inner) - : base(message, inner) - { - HttpStatus = httpStatus; - } -} - -public class ApiException : DuoException -{ - public int Code { get; private set; } - public string ApiMessage { get; private set; } - public string ApiMessageDetail { get; private set; } - - public ApiException(int code, int httpStatus, string apiMessage, string apiMessageDetail) - : base(httpStatus, FormatMessage(code, apiMessage, apiMessageDetail), null) - { - Code = code; - ApiMessage = apiMessage; - ApiMessageDetail = apiMessageDetail; - } - - private static string FormatMessage(int code, string apiMessage, string apiMessageDetail) - { - return string.Format("Duo API Error {0}: '{1}' ('{2}')", code, apiMessage, apiMessageDetail); - } -} - -public class BadResponseException : DuoException -{ - public BadResponseException(int httpStatus, Exception inner) - : base(httpStatus, FormatMessage(httpStatus, inner), inner) - { } - - private static string FormatMessage(int httpStatus, Exception inner) - { - var innerMessage = "(null)"; - if (inner != null) - { - innerMessage = string.Format("'{0}'", inner.Message); - } - return string.Format("Got error {0} with HTTP Status {1}", innerMessage, httpStatus); - } -} diff --git a/src/Core/Auth/Utilities/DuoUtilities.cs b/src/Core/Auth/Utilities/DuoUtilities.cs new file mode 100644 index 000000000000..b84334be2487 --- /dev/null +++ b/src/Core/Auth/Utilities/DuoUtilities.cs @@ -0,0 +1,16 @@ +namespace Bit.Core.Auth.Utilities; + +public class DuoUtilities +{ + public static bool ValidHost(string host) + { + if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri)) + { + return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") && + uri.Host.StartsWith("api-") && + (uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com")); + } + throw new ArgumentException("Invalid Duo host configured.", nameof(host)); + } +} + diff --git a/src/Core/Auth/Utilities/DuoWeb.cs b/src/Core/Auth/Utilities/DuoWeb.cs deleted file mode 100644 index 98fa974ab28a..000000000000 --- a/src/Core/Auth/Utilities/DuoWeb.cs +++ /dev/null @@ -1,240 +0,0 @@ -/* -Original source modified from https://github.com/duosecurity/duo_dotnet - -============================================================================= -============================================================================= - -ref: https://github.com/duosecurity/duo_dotnet/blob/master/LICENSE - -Copyright (c) 2011, Duo Security, Inc. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -using System.Security.Cryptography; -using System.Text; - -namespace Bit.Core.Auth.Utilities.Duo; - -public static class DuoWeb -{ - private const string DuoProfix = "TX"; - private const string AppPrefix = "APP"; - private const string AuthPrefix = "AUTH"; - private const int DuoExpire = 300; - private const int AppExpire = 3600; - private const int IKeyLength = 20; - private const int SKeyLength = 40; - private const int AKeyLength = 40; - - public static string ErrorUser = "ERR|The username passed to sign_request() is invalid."; - public static string ErrorIKey = "ERR|The Duo integration key passed to sign_request() is invalid."; - public static string ErrorSKey = "ERR|The Duo secret key passed to sign_request() is invalid."; - public static string ErrorAKey = "ERR|The application secret key passed to sign_request() must be at least " + - "40 characters."; - public static string ErrorUnknown = "ERR|An unknown error has occurred."; - - // throw on invalid bytes - private static Encoding _encoding = new UTF8Encoding(false, true); - private static DateTime _epoc = new DateTime(1970, 1, 1); - - /// - /// Generate a signed request for Duo authentication. - /// The returned value should be passed into the Duo.init() call - /// in the rendered web page used for Duo authentication. - /// - /// Duo integration key - /// Duo secret key - /// Application secret key - /// Primary-authenticated username - /// (optional) The current UTC time - /// signed request - public static string SignRequest(string ikey, string skey, string akey, string username, - DateTime? currentTime = null) - { - string duoSig; - string appSig; - - var currentTimeValue = currentTime ?? DateTime.UtcNow; - - if (username == string.Empty) - { - return ErrorUser; - } - if (username.Contains("|")) - { - return ErrorUser; - } - if (ikey.Length != IKeyLength) - { - return ErrorIKey; - } - if (skey.Length != SKeyLength) - { - return ErrorSKey; - } - if (akey.Length < AKeyLength) - { - return ErrorAKey; - } - - try - { - duoSig = SignVals(skey, username, ikey, DuoProfix, DuoExpire, currentTimeValue); - appSig = SignVals(akey, username, ikey, AppPrefix, AppExpire, currentTimeValue); - } - catch - { - return ErrorUnknown; - } - - return $"{duoSig}:{appSig}"; - } - - /// - /// Validate the signed response returned from Duo. - /// Returns the username of the authenticated user, or null. - /// - /// Duo integration key - /// Duo secret key - /// Application secret key - /// The signed response POST'ed to the server - /// (optional) The current UTC time - /// authenticated username, or null - public static string VerifyResponse(string ikey, string skey, string akey, string sigResponse, - DateTime? currentTime = null) - { - string authUser = null; - string appUser = null; - var currentTimeValue = currentTime ?? DateTime.UtcNow; - - try - { - var sigs = sigResponse.Split(':'); - var authSig = sigs[0]; - var appSig = sigs[1]; - - authUser = ParseVals(skey, authSig, AuthPrefix, ikey, currentTimeValue); - appUser = ParseVals(akey, appSig, AppPrefix, ikey, currentTimeValue); - } - catch - { - return null; - } - - if (authUser != appUser) - { - return null; - } - - return authUser; - } - - private static string SignVals(string key, string username, string ikey, string prefix, long expire, - DateTime currentTime) - { - var ts = (long)(currentTime - _epoc).TotalSeconds; - expire = ts + expire; - var val = $"{username}|{ikey}|{expire.ToString()}"; - var cookie = $"{prefix}|{Encode64(val)}"; - var sig = Sign(key, cookie); - return $"{cookie}|{sig}"; - } - - private static string ParseVals(string key, string val, string prefix, string ikey, DateTime currentTime) - { - var ts = (long)(currentTime - _epoc).TotalSeconds; - - var parts = val.Split('|'); - if (parts.Length != 3) - { - return null; - } - - var uPrefix = parts[0]; - var uB64 = parts[1]; - var uSig = parts[2]; - - var sig = Sign(key, $"{uPrefix}|{uB64}"); - if (Sign(key, sig) != Sign(key, uSig)) - { - return null; - } - - if (uPrefix != prefix) - { - return null; - } - - var cookie = Decode64(uB64); - var cookieParts = cookie.Split('|'); - if (cookieParts.Length != 3) - { - return null; - } - - var username = cookieParts[0]; - var uIKey = cookieParts[1]; - var expire = cookieParts[2]; - - if (uIKey != ikey) - { - return null; - } - - var expireTs = Convert.ToInt32(expire); - if (ts >= expireTs) - { - return null; - } - - return username; - } - - private static string Sign(string skey, string data) - { - var keyBytes = Encoding.ASCII.GetBytes(skey); - var dataBytes = Encoding.ASCII.GetBytes(data); - - using (var hmac = new HMACSHA1(keyBytes)) - { - var hash = hmac.ComputeHash(dataBytes); - var hex = BitConverter.ToString(hash); - return hex.Replace("-", "").ToLower(); - } - } - - private static string Encode64(string plaintext) - { - var plaintextBytes = _encoding.GetBytes(plaintext); - return Convert.ToBase64String(plaintextBytes); - } - - private static string Decode64(string encoded) - { - var plaintextBytes = Convert.FromBase64String(encoded); - return _encoding.GetString(plaintextBytes); - } -} From 668b4518b625c86d5616c12e1cbb2f3ae44014d9 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Fri, 27 Sep 2024 11:55:28 -0700 Subject: [PATCH 22/25] fixed tests --- .../TwoFactor/TwoFactorDuoResponseModel.cs | 10 +-- src/Core/Auth/Identity/DuoTokenProvider.cs | 10 --- .../Identity/DuoUniversalTokenProvider.cs | 3 +- .../OrganizationDuoUniversalTokenProvider.cs | 5 +- src/Core/Auth/Utilities/DuoUtilities.cs | 16 ++++- ...ganizationTwoFactorDuoRequestModelTests.cs | 60 ----------------- ...TwoFactorDuoRequestModelValidationTests.cs | 67 ------------------- .../UserTwoFactorDuoRequestModelTests.cs | 60 ----------------- ...anizationTwoFactorDuoResponseModelTests.cs | 57 +++------------- .../UserTwoFactorDuoResponseModelTests.cs | 57 +++------------- .../Auth/Utilities/DuoUtilitiesTests.cs | 39 +++++++++++ 11 files changed, 84 insertions(+), 300 deletions(-) delete mode 100644 test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs create mode 100644 test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs index d17ee7c8e03d..dbdc66d6d8f1 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs @@ -45,7 +45,7 @@ private void Build(TwoFactorProvider provider) } if (provider.MetaData.TryGetValue("ClientSecret", out var clientSecret)) { - ClientSecret = MaskKey((string)clientSecret); + ClientSecret = MaskSecret((string)clientSecret); } if (provider.MetaData.TryGetValue("ClientId", out var clientId)) { @@ -58,14 +58,14 @@ private void Build(TwoFactorProvider provider) } } - private static string MaskKey(string key) + private static string MaskSecret(string secret) { - if (string.IsNullOrWhiteSpace(key) || key.Length <= 6) + if (string.IsNullOrWhiteSpace(secret) || secret.Length <= 6) { - return key; + return secret; } // Mask all but the first 6 characters. - return string.Concat(key.AsSpan(0, 6), new string('*', key.Length - 6)); + return string.Concat(secret.AsSpan(0, 6), new string('*', secret.Length - 6)); } } diff --git a/src/Core/Auth/Identity/DuoTokenProvider.cs b/src/Core/Auth/Identity/DuoTokenProvider.cs index 69fa78b0fc1b..384106668e84 100644 --- a/src/Core/Auth/Identity/DuoTokenProvider.cs +++ b/src/Core/Auth/Identity/DuoTokenProvider.cs @@ -1,6 +1,5 @@ using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Settings; @@ -33,15 +32,6 @@ public DuoTokenProvider(ICurrentContext currentContext, GlobalSettings globalSet _globalSettings = globalSettings; } - protected bool HasProperMetaData(TwoFactorProvider provider) - { - return provider?.MetaData != null && - provider.MetaData.ContainsKey("ClientId") && - provider.MetaData.ContainsKey("ClientSecret") && - provider.MetaData.ContainsKey("Host") && - DuoUtilities.ValidHost((string)provider.MetaData["Host"]); - } - protected async Task GenerateAuthUrlAsync( TwoFactorProvider provider, IDataProtectorTokenFactory tokenDataFactory, diff --git a/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs index 6f1344f63b71..4a0e1e861289 100644 --- a/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/DuoUniversalTokenProvider.cs @@ -1,6 +1,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Services; @@ -69,7 +70,7 @@ private async Task GetTwoFactorProvideAsync(User user) } var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); - if (!HasProperMetaData(provider)) + if (!DuoUtilities.HasProperDuoMetadata(provider)) { return null; } diff --git a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs index df52946d0786..a1aa0e02d9bf 100644 --- a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Utilities; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Settings; @@ -33,7 +34,7 @@ public Task CanGenerateTwoFactorTokenAsync(Organization organization) var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo) - && HasProperMetaData(provider); + && DuoUtilities.HasProperDuoMetadata(provider);; return Task.FromResult(canGenerate); } @@ -69,7 +70,7 @@ private TwoFactorProvider GetTwoFactorProvider(Organization organization) } var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); - if (!HasProperMetaData(provider)) + if (!DuoUtilities.HasProperDuoMetadata(provider)) { return null; } diff --git a/src/Core/Auth/Utilities/DuoUtilities.cs b/src/Core/Auth/Utilities/DuoUtilities.cs index b84334be2487..d34fd598cc7d 100644 --- a/src/Core/Auth/Utilities/DuoUtilities.cs +++ b/src/Core/Auth/Utilities/DuoUtilities.cs @@ -1,8 +1,19 @@ -namespace Bit.Core.Auth.Utilities; +using Bit.Core.Auth.Models; + +namespace Bit.Core.Auth.Utilities; public class DuoUtilities { - public static bool ValidHost(string host) + public static bool HasProperDuoMetadata(TwoFactorProvider provider) + { + return provider?.MetaData != null && + provider.MetaData.ContainsKey("ClientId") && + provider.MetaData.ContainsKey("ClientSecret") && + provider.MetaData.ContainsKey("Host") && + ValidDuoHost((string)provider.MetaData["Host"]); + } + + public static bool ValidDuoHost(string host) { if (Uri.TryCreate($"https://{host}", UriKind.Absolute, out var uri)) { @@ -13,4 +24,3 @@ public static bool ValidHost(string host) throw new ArgumentException("Invalid Duo host configured.", nameof(host)); } } - diff --git a/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs index 5fbaf88671c5..361adea536d8 100644 --- a/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs +++ b/test/Api.Test/Auth/Models/Request/OrganizationTwoFactorDuoRequestModelTests.cs @@ -18,8 +18,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( { ClientId = "clientId", ClientSecret = "clientSecret", - IntegrationKey = "integrationKey", - SecretKey = "secretKey", Host = "example.com" }; @@ -30,8 +28,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); } @@ -49,8 +45,6 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() { ClientId = "newClientId", ClientSecret = "newClientSecret", - IntegrationKey = "newIntegrationKey", - SecretKey = "newSecretKey", Host = "newExample.com" }; @@ -61,61 +55,7 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); Assert.Equal("newExample.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); } - - [Fact] - public void DuoV2ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingOrg = new Organization(); - var model = new UpdateTwoFactorDuoRequestModel - { - IntegrationKey = "integrationKey", - SecretKey = "secretKey", - Host = "example.com" - }; - - // Act - var result = model.ToOrganization(existingOrg); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); - } - - [Fact] - public void DuoV4ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingOrg = new Organization(); - var model = new UpdateTwoFactorDuoRequestModel - { - ClientId = "clientId", - ClientSecret = "clientSecret", - Host = "example.com" - }; - - // Act - var result = model.ToOrganization(existingOrg); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.OrganizationDuo)); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientId"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.OrganizationDuo].Enabled); - } } diff --git a/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs b/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs deleted file mode 100644 index ab05a94f13fd..000000000000 --- a/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Bit.Api.Auth.Models.Request; -using Xunit; - -namespace Bit.Api.Test.Auth.Models.Request; - -public class TwoFactorDuoRequestModelValidationTests -{ - [Fact] - public void ShouldReturnValidationError_WhenHostIsInvalid() - { - // Arrange - var model = new UpdateTwoFactorDuoRequestModel - { - Host = "invalidHost", - ClientId = "clientId", - ClientSecret = "clientSecret", - }; - - // Act - var result = model.Validate(new ValidationContext(model)); - - // Assert - Assert.Single(result); - Assert.Equal("Host is invalid.", result.First().ErrorMessage); - Assert.Equal("Host", result.First().MemberNames.First()); - } - - [Fact] - public void ShouldReturnValidationError_WhenValuesAreInvalid() - { - // Arrange - var model = new UpdateTwoFactorDuoRequestModel - { - Host = "api-12345abc.duosecurity.com" - }; - - // Act - var result = model.Validate(new ValidationContext(model)); - - // Assert - Assert.Single(result); - Assert.Equal("Neither v2 or v4 values are valid.", result.First().ErrorMessage); - Assert.Contains("ClientId", result.First().MemberNames); - Assert.Contains("ClientSecret", result.First().MemberNames); - Assert.Contains("IntegrationKey", result.First().MemberNames); - Assert.Contains("SecretKey", result.First().MemberNames); - } - - [Fact] - public void ShouldReturnSuccess_WhenValuesAreValid() - { - // Arrange - var model = new UpdateTwoFactorDuoRequestModel - { - Host = "api-12345abc.duosecurity.com", - ClientId = "clientId", - ClientSecret = "clientSecret", - }; - - // Act - var result = model.Validate(new ValidationContext(model)); - - // Assert - Assert.Empty(result); - } -} diff --git a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs index 28dfc83a2de7..b35bc846f6a2 100644 --- a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs +++ b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs @@ -17,8 +17,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( { ClientId = "clientId", ClientSecret = "clientSecret", - IntegrationKey = "integrationKey", - SecretKey = "secretKey", Host = "example.com" }; @@ -30,8 +28,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); } @@ -49,8 +45,6 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() { ClientId = "newClientId", ClientSecret = "newClientSecret", - IntegrationKey = "newIntegrationKey", - SecretKey = "newSecretKey", Host = "newExample.com" }; @@ -62,61 +56,7 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); Assert.Equal("newExample.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); } - - [Fact] - public void DuoV2ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingUser = new User(); - var model = new UpdateTwoFactorDuoRequestModel - { - IntegrationKey = "integrationKey", - SecretKey = "secretKey", - Host = "example.com" - }; - - // Act - var result = model.ToUser(existingUser); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("integrationKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("secretKey", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); - } - - [Fact] - public void DuoV4ParamsSync_WhenExistingProviderDoesNotExist() - { - // Arrange - var existingUser = new User(); - var model = new UpdateTwoFactorDuoRequestModel - { - ClientId = "clientId", - ClientSecret = "clientSecret", - Host = "example.com" - }; - - // Act - var result = model.ToUser(existingUser); - - // Assert - // IKey and SKey should be the same as ClientId and ClientSecret - Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); - Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["IKey"]); - Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["SKey"]); - Assert.Equal("example.com", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["Host"]); - Assert.True(result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].Enabled); - } } diff --git a/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs index dea76b2cdbc8..3e81e3e5f545 100644 --- a/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs @@ -10,64 +10,40 @@ public class OrganizationTwoFactorDuoResponseModelTests { [Theory] [BitAutoData] - public void Organization_WithDuoV4_ShouldBuildModel(Organization organization) + public void Organization_WithDuo_ShouldBuildModel(Organization organization) { // Arrange - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoV4ProvidersJson(); + organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProvidersJson(); // Act var model = new TwoFactorDuoResponseModel(organization); - // Assert if v4 data Ikey and Skey are set to clientId and clientSecret + /// Assert Even if both versions are present priority is given to v4 data Assert.NotNull(model); Assert.Equal("clientId", model.ClientId); Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); - } - - [Theory] - [BitAutoData] - public void Organization_WithDuoV2_ShouldBuildModel(Organization organization) - { - // Arrange - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoV2ProvidersJson(); - - // Act - var model = new TwoFactorDuoResponseModel(organization); - - // Assert if only v2 data clientId and clientSecret are set to Ikey and Sk - Assert.NotNull(model); - Assert.Equal("IKey", model.ClientId); - Assert.Equal("SKey", model.ClientSecret); - Assert.Equal("IKey", model.IntegrationKey); - Assert.Equal("SKey", model.SecretKey); } [Theory] [BitAutoData] - public void Organization_WithDuo_ShouldBuildModel(Organization organization) + public void Organization_WithDuoEmpty_ShouldFail(Organization organization) { // Arrange - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoProvidersJson(); + organization.TwoFactorProviders = "{\"6\" : {}}"; // Act var model = new TwoFactorDuoResponseModel(organization); - /// Assert Even if both versions are present priority is given to v4 data - Assert.NotNull(model); - Assert.Equal("clientId", model.ClientId); - Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); + /// Assert + Assert.False(model.Enabled); } [Theory] [BitAutoData] - public void Organization_WithDuoEmpty_ShouldFail(Organization organization) + public void Organization_WithTwoFactorProvidersNull_ShouldFail(Organization organization) { // Arrange - organization.TwoFactorProviders = "{\"6\" : {}}"; + organization.TwoFactorProviders = null; // Act var model = new TwoFactorDuoResponseModel(organization); @@ -78,10 +54,10 @@ public void Organization_WithDuoEmpty_ShouldFail(Organization organization) [Theory] [BitAutoData] - public void Organization_WithTwoFactorProvidersNull_ShouldFail(Organization organization) + public void Organization_WithTwoFactorProvidersEmpty_ShouldFail(Organization organization) { // Arrange - organization.TwoFactorProviders = "{\"6\" : {}}"; + organization.TwoFactorProviders = "{}"; // Act var model = new TwoFactorDuoResponseModel(organization); @@ -91,19 +67,8 @@ public void Organization_WithTwoFactorProvidersNull_ShouldFail(Organization orga } private string GetTwoFactorOrganizationDuoProvidersJson() - { - return - "{\"6\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; - } - - private string GetTwoFactorOrganizationDuoV4ProvidersJson() { return "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - - private string GetTwoFactorOrganizationDuoV2ProvidersJson() - { - return "{\"6\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"Host\":\"example.com\"}}}"; - } } diff --git a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs index cb46273a60d2..32fd434c4c68 100644 --- a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs @@ -10,64 +10,40 @@ public class UserTwoFactorDuoResponseModelTests { [Theory] [BitAutoData] - public void User_WithDuoV4_ShouldBuildModel(User user) + public void User_WithDuo_ShouldBuildModel(User user) { // Arrange - user.TwoFactorProviders = GetTwoFactorDuoV4ProvidersJson(); + user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); // Act var model = new TwoFactorDuoResponseModel(user); - // Assert if v4 data Ikey and Skey are set to clientId and clientSecret + // Assert Even if both versions are present priority is given to v4 data Assert.NotNull(model); Assert.Equal("clientId", model.ClientId); Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); } [Theory] [BitAutoData] - public void User_WithDuov2_ShouldBuildModel(User user) - { - // Arrange - user.TwoFactorProviders = GetTwoFactorDuoV2ProvidersJson(); - - // Act - var model = new TwoFactorDuoResponseModel(user); - - // Assert if only v2 data clientId and clientSecret are set to Ikey and Skey - Assert.NotNull(model); - Assert.Equal("IKey", model.ClientId); - Assert.Equal("SKey", model.ClientSecret); - Assert.Equal("IKey", model.IntegrationKey); - Assert.Equal("SKey", model.SecretKey); - } - - [Theory] - [BitAutoData] - public void User_WithDuo_ShouldBuildModel(User user) + public void User_WithDuoEmpty_ShouldFail(User user) { // Arrange - user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); + user.TwoFactorProviders = "{\"2\" : {}}"; // Act var model = new TwoFactorDuoResponseModel(user); - // Assert Even if both versions are present priority is given to v4 data - Assert.NotNull(model); - Assert.Equal("clientId", model.ClientId); - Assert.Equal("secret************", model.ClientSecret); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); + /// Assert + Assert.False(model.Enabled); } [Theory] [BitAutoData] - public void User_WithDuoEmpty_ShouldFail(User user) + public void User_WithTwoFactorProvidersNull_ShouldFail(User user) { // Arrange - user.TwoFactorProviders = "{\"2\" : {}}"; + user.TwoFactorProviders = null; // Act var model = new TwoFactorDuoResponseModel(user); @@ -78,10 +54,10 @@ public void User_WithDuoEmpty_ShouldFail(User user) [Theory] [BitAutoData] - public void User_WithTwoFactorProvidersNull_ShouldFail(User user) + public void User_WithTwoFactorProvidersEmpty_ShouldFail(User user) { // Arrange - user.TwoFactorProviders = null; + user.TwoFactorProviders = "{}"; // Act var model = new TwoFactorDuoResponseModel(user); @@ -91,19 +67,8 @@ public void User_WithTwoFactorProvidersNull_ShouldFail(User user) } private string GetTwoFactorDuoProvidersJson() - { - return - "{\"2\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; - } - - private string GetTwoFactorDuoV4ProvidersJson() { return "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - - private string GetTwoFactorDuoV2ProvidersJson() - { - return "{\"2\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"Host\":\"example.com\"}}}"; - } } diff --git a/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs new file mode 100644 index 000000000000..bd45fd4bc5e1 --- /dev/null +++ b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs @@ -0,0 +1,39 @@ +using Bit.Core.Auth.Models; +using Bit.Core.Auth.Utilities; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Auth.Utilities; + +public class DuoUtilitiesTests +{ + [Theory] + [BitAutoData] + public void HasProperMetaData_ReturnsTrue(TwoFactorProvider twoFactorProvider) + { + twoFactorProvider.MetaData = DuoMetaData(); + var result = DuoUtilities.HasProperDuoMetadata(twoFactorProvider); + + Assert.True(result); + } + + [Theory] + [BitAutoData] + public void HasProperMetaData_ReturnsFalse(TwoFactorProvider twoFactorProvider) + { + twoFactorProvider.MetaData = null; + var result = DuoUtilities.HasProperDuoMetadata(twoFactorProvider); + + Assert.False(result); + } + + private Dictionary DuoMetaData() + { + return new() + { + { "ClientId", "clientId" }, + { "ClientSecret", "clientSecret" }, + { "Host", "api-abcd1234.duosecurity.com" } + }; + } +} From 80fc7c1a17d6d5986c23bf4717a6a33cbb9462ad Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 30 Sep 2024 12:27:24 -0700 Subject: [PATCH 23/25] Testing Duo two factor implementation --- .../Auth/Controllers/TwoFactorController.cs | 42 +-- .../OrganizationDuoUniversalTokenProvider.cs | 2 +- src/Core/Auth/Utilities/DuoUtilities.cs | 2 +- .../Controllers/TwoFactorControllerTests.cs | 296 ++++++++++++++++++ .../UserTwoFactorDuoRequestModelTests.cs | 2 - .../UserTwoFactorDuoResponseModelTests.cs | 27 +- .../Attributes/BitCustomizeAttribute.cs | 4 +- .../Auth/Identity/BaseTokenProviderTests.cs | 3 + .../DuoTwoFactorTokenProviderTests.cs | 61 ++++ .../BaseRequestValidatorTests.cs | 18 +- .../BaseRequestValidatorTestWrapper.cs | 4 + 11 files changed, 418 insertions(+), 43 deletions(-) create mode 100644 test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs create mode 100644 test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 10762d530e92..11a98ee4f6c6 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -3,6 +3,8 @@ using Bit.Api.Auth.Models.Response.TwoFactor; using Bit.Api.Models.Request; using Bit.Api.Models.Response; +using Bit.Core; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; @@ -202,14 +204,8 @@ public async Task GetOrganizationDuo(string id, [FromBody] SecretVerificationRequestModel model) { await CheckAsync(model, false, true); + var organization = await CheckOrganizationAsync(new Guid(id)); - var orgIdGuid = new Guid(id); - if (!await _currentContext.ManagePolicies(orgIdGuid)) - { - throw new NotFoundException(); - } - - var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException(); var response = new TwoFactorDuoResponseModel(organization); return response; } @@ -220,14 +216,8 @@ public async Task PutOrganizationDuo(string id, [FromBody] UpdateTwoFactorDuoRequestModel model) { await CheckAsync(model, false); + var organization = await CheckOrganizationAsync(new Guid(id)); - var orgIdGuid = new Guid(id); - if (!await _currentContext.ManagePolicies(orgIdGuid)) - { - throw new NotFoundException(); - } - - var organization = await _organizationRepository.GetByIdAsync(orgIdGuid) ?? throw new NotFoundException(); if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( @@ -379,19 +369,8 @@ public async Task PutDisable([FromBody] TwoFacto public async Task PutOrganizationDisable(string id, [FromBody] TwoFactorProviderRequestModel model) { - var user = await CheckAsync(model, false); - - var orgIdGuid = new Guid(id); - if (!await _currentContext.ManagePolicies(orgIdGuid)) - { - throw new NotFoundException(); - } - - var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); - if (organization == null) - { - throw new NotFoundException(); - } + await CheckAsync(model, false); + var organization = await CheckOrganizationAsync(new Guid(id)); await _organizationService.DisableTwoFactorProviderAsync(organization, model.Type.Value); var response = new TwoFactorProviderResponseModel(model.Type.Value, organization); @@ -433,6 +412,15 @@ public Task PutDeviceVerificationSettings( return Task.FromResult(new DeviceVerificationResponseModel(false, false)); } + private async Task CheckOrganizationAsync(Guid organizationId){ + if (!await _currentContext.ManagePolicies(organizationId)) + { + throw new NotFoundException(); + } + var organization = await _organizationRepository.GetByIdAsync(organizationId) ?? throw new NotFoundException(); + return organization; + } + private async Task CheckAsync(SecretVerificationRequestModel model, bool premium, bool skipVerification = false) { diff --git a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs index a1aa0e02d9bf..a43b59a1db93 100644 --- a/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/OrganizationDuoUniversalTokenProvider.cs @@ -34,7 +34,7 @@ public Task CanGenerateTwoFactorTokenAsync(Organization organization) var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); var canGenerate = organization.TwoFactorProviderIsEnabled(TwoFactorProviderType.OrganizationDuo) - && DuoUtilities.HasProperDuoMetadata(provider);; + && DuoUtilities.HasProperDuoMetadata(provider); return Task.FromResult(canGenerate); } diff --git a/src/Core/Auth/Utilities/DuoUtilities.cs b/src/Core/Auth/Utilities/DuoUtilities.cs index d34fd598cc7d..ddeeb923f971 100644 --- a/src/Core/Auth/Utilities/DuoUtilities.cs +++ b/src/Core/Auth/Utilities/DuoUtilities.cs @@ -21,6 +21,6 @@ public static bool ValidDuoHost(string host) uri.Host.StartsWith("api-") && (uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com")); } - throw new ArgumentException("Invalid Duo host configured.", nameof(host)); + return false; } } diff --git a/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs new file mode 100644 index 000000000000..576ee629af5a --- /dev/null +++ b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs @@ -0,0 +1,296 @@ +using Xunit; +using NSubstitute; +using Bit.Api.Auth.Controllers; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.AutoFixture; +using Bit.Core.Services; +using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Core.Entities; +using Bit.Api.Auth.Models.Response.TwoFactor; +using Bit.Core.Exceptions; +using Bit.Api.Auth.Models.Request; +using Bit.Core.Auth.Identity; +using Bit.Core.Context; +using Bit.Core.Repositories; +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Api.Test.Auth.Controllers; + +[ControllerCustomize(typeof(TwoFactorController))] +[SutProviderCustomize] +public class TwoFactorControllerTests +{ + [Theory, BitAutoData] + public async Task CheckAsync_UserNull_ThrowsUnauthorizedException(SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(null as User); + + // Act + var result = () => sutProvider.Sut.GetDuo(request); + + // Assert + await Assert.ThrowsAsync(result); + + } + + [Theory, BitAutoData] + public async Task CheckAsync_BadSecret_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + + sutProvider.GetDependency() + .VerifySecretAsync(default, default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.GetDuo(request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("The model state is invalid.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task CheckAsync_CannotAccessPremium_ThrowsBadRequestException(User user, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + + sutProvider.GetDependency() + .VerifySecretAsync(default, default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .CanAccessPremium(default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.GetDuo(request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("Premium status is required.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task GetDuo_Success(User user, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + // Act + var result = await sutProvider.Sut.GetDuo(request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PutDuo_InvalidConfiguration_ThrowsBadRequestException(User user, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.PutDuo(request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("Duo configuration settings are not valid. Please re-check the Duo Admin panel.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task PutDuo_Success(User user, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + user.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(true); + + // Act + var result = await sutProvider.Sut.PutDuo(request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(user.TwoFactorProviders, request.ToUser(user).TwoFactorProviders); + } + + [Theory, BitAutoData] + public async Task CheckOrganizationAsync_ManagePolicies_ThrowsNotFoundException( + User user, Organization organization, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ManagePolicies(default) + .ReturnsForAnyArgs(false); + + // Act + var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + public async Task CheckOrganizationAsync_GetByIdAsync_ThrowsNotFoundException( + User user, Organization organization, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + + sutProvider.GetDependency() + .ManagePolicies(default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .GetByIdAsync(default) + .ReturnsForAnyArgs(null as Organization); + + // Act + var result = () => sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request); + + // Assert + await Assert.ThrowsAsync(result); + } + + [Theory, BitAutoData] + public async Task GetOrganizationDuo_Success( + User user, Organization organization, SecretVerificationRequestModel request, SutProvider sutProvider) + { + // Arrange + organization.TwoFactorProviders = GetOrganizationTwoFactorDuoProvidersJson(); + SetupCheckAsyncToPass(sutProvider, user); + SetupCheckOrganizationAsyncToPass(sutProvider, organization); + + // Act + var result = await sutProvider.Sut.GetOrganizationDuo(organization.Id.ToString(), request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task PutOrganizationDuo_InvalidConfiguration_ThrowsBadRequestException( + User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + SetupCheckAsyncToPass(sutProvider, user); + SetupCheckOrganizationAsyncToPass(sutProvider, organization); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(false); + + // Act + try + { + await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request); + } + catch (BadRequestException e) + { + // Assert + Assert.Equal("Duo configuration settings are not valid. Please re-check the Duo Admin panel.", e.Message); + } + } + + [Theory, BitAutoData] + public async Task PutOrganizationDuo_Success( + User user, Organization organization, UpdateTwoFactorDuoRequestModel request, SutProvider sutProvider) + { + // Arrange + SetupCheckAsyncToPass(sutProvider, user); + SetupCheckOrganizationAsyncToPass(sutProvider, organization); + organization.TwoFactorProviders = GetUserTwoFactorDuoProvidersJson(); + + sutProvider.GetDependency() + .ValidateDuoConfiguration(default, default, default) + .ReturnsForAnyArgs(true); + + // Act + var result = + await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(organization.TwoFactorProviders, request.ToOrganization(organization).TwoFactorProviders); + } + + + private string GetUserTwoFactorDuoProvidersJson() + { + return + "{\"2\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + private string GetOrganizationTwoFactorDuoProvidersJson() + { + return + "{\"6\":{\"Enabled\":true,\"MetaData\":{\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; + } + + /// + /// Sets up the CheckAsync method to pass. + /// + /// uses bit auto data + /// uses bit auto data + private void SetupCheckAsyncToPass(SutProvider sutProvider, User user) + { + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + + sutProvider.GetDependency() + .VerifySecretAsync(default, default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .CanAccessPremium(default) + .ReturnsForAnyArgs(true); + } + + private void SetupCheckOrganizationAsyncToPass(SutProvider sutProvider, Organization organization){ + sutProvider.GetDependency() + .ManagePolicies(default) + .ReturnsForAnyArgs(true); + + sutProvider.GetDependency() + .GetByIdAsync(default) + .ReturnsForAnyArgs(organization); + } +} diff --git a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs index b35bc846f6a2..56c9af1e0d7e 100644 --- a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs +++ b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs @@ -24,7 +24,6 @@ public void ShouldAddOrUpdateTwoFactorProvider_WhenExistingProviderDoesNotExist( var result = model.ToUser(existingUser); // Assert - // IKey and SKey should be the same as ClientId and ClientSecret Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("clientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("clientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); @@ -52,7 +51,6 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() var result = model.ToUser(existingUser); // Assert - // IKey and SKey should be the same as ClientId and ClientSecret Assert.True(result.GetTwoFactorProviders().ContainsKey(TwoFactorProviderType.Duo)); Assert.Equal("newClientId", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientId"]); Assert.Equal("newClientSecret", result.GetTwoFactorProviders()[TwoFactorProviderType.Duo].MetaData["ClientSecret"]); diff --git a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs index 32fd434c4c68..699bbeb33d2b 100644 --- a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs @@ -8,6 +8,25 @@ namespace Bit.Api.Test.Auth.Models.Response; public class UserTwoFactorDuoResponseModelTests { + [Theory] + [BitAutoData] + public void User_WithDuo_UserNull_ThrowsArgumentException(User user) + { + // Arrange + user.TwoFactorProviders = GetTwoFactorDuoProvidersJson(); + + // Act + try + { + var model = new TwoFactorDuoResponseModel(null as User); + } + catch (ArgumentNullException e) + { + // Assert + Assert.Equal("Value cannot be null. (Parameter 'user')", e.Message); + } + } + [Theory] [BitAutoData] public void User_WithDuo_ShouldBuildModel(User user) @@ -18,7 +37,7 @@ public void User_WithDuo_ShouldBuildModel(User user) // Act var model = new TwoFactorDuoResponseModel(user); - // Assert Even if both versions are present priority is given to v4 data + // Assert Assert.NotNull(model); Assert.Equal("clientId", model.ClientId); Assert.Equal("secret************", model.ClientSecret); @@ -34,7 +53,7 @@ public void User_WithDuoEmpty_ShouldFail(User user) // Act var model = new TwoFactorDuoResponseModel(user); - /// Assert + // Assert Assert.False(model.Enabled); } @@ -48,7 +67,7 @@ public void User_WithTwoFactorProvidersNull_ShouldFail(User user) // Act var model = new TwoFactorDuoResponseModel(user); - /// Assert + // Assert Assert.False(model.Enabled); } @@ -62,7 +81,7 @@ public void User_WithTwoFactorProvidersEmpty_ShouldFail(User user) // Act var model = new TwoFactorDuoResponseModel(user); - /// Assert + // Assert Assert.False(model.Enabled); } diff --git a/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs b/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs index 105a6632d893..e8a88c684838 100644 --- a/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs +++ b/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs @@ -13,8 +13,8 @@ namespace Bit.Test.Common.AutoFixture.Attributes; public abstract class BitCustomizeAttribute : Attribute { /// - /// /// Gets a customization for the method's parameters. + /// Gets a customization for the method's parameters. /// - /// A customization for the method's paramters. + /// A customization for the method's parameters. public abstract ICustomization GetCustomization(); } diff --git a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs index b90f71ae71bc..12620ed05509 100644 --- a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs @@ -48,6 +48,9 @@ protected virtual void SetupUserService(IUserService userService, User user) userService .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType, user) .Returns(true); + userService + .CanAccessPremium(user) + .Returns(true); } protected static UserManager SubstituteUserManager() diff --git a/test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs new file mode 100644 index 000000000000..0ae9af91635a --- /dev/null +++ b/test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs @@ -0,0 +1,61 @@ +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity; +using Bit.Core.Entities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Auth.Identity; + +public class DuoTwoFactorTokenProviderTests : BaseTokenProviderTests +{ + public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Duo; + + public static IEnumerable CanGenerateTwoFactorTokenAsyncData + => SetupCanGenerateData( + ( + new Dictionary + { + ["ClientId"] = "ClientId", + ["ClientSecret"] = "ClientSecret", + ["Host"] = "api-abcd1234.duosecurity.com", + }, + true + ), + ( + new Dictionary + { + ["ClientId"] = "ClientId", + ["ClientSecret"] = "ClientSecret", + ["Host"] = "api-abcd1234.duofederal.com", + }, + true + ), + ( + new Dictionary + { + ["ClientId"] = "ClientId", + ["ClientSecret"] = "ClientSecret", + ["Host"] = "", + }, + false + ), + ( + new Dictionary + { + ["ClientSecret"] = "ClientSecret", + ["Host"] = "api-abcd1234.duofederal.com", + }, + false + ) + ); + + [Theory, BitMemberAutoData(nameof(CanGenerateTwoFactorTokenAsyncData))] + public override async Task RunCanGenerateTwoFactorTokenAsync(Dictionary metaData, bool expectedResponse, + User user, SutProvider sutProvider) + { + user.Premium = true; + user.PremiumExpirationDate = DateTime.UtcNow.AddDays(1); + await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider); + } +} diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index ef425dcdcafb..0004864c8c65 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -31,6 +31,8 @@ public class BaseRequestValidatorTests private readonly IEventService _eventService; private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; + private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider; + private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IMailService _mailService; private readonly ILogger _logger; @@ -51,6 +53,8 @@ public BaseRequestValidatorTests() _eventService = Substitute.For(); _deviceValidator = Substitute.For(); _twoFactorAuthenticationValidator = Substitute.For(); + _organizationDuoUniversalTokenProvider = Substitute.For(); + _organizationRepository = Substitute.For(); _organizationUserRepository = Substitute.For(); _mailService = Substitute.For(); _logger = Substitute.For>(); @@ -68,6 +72,8 @@ public BaseRequestValidatorTests() _eventService, _deviceValidator, _twoFactorAuthenticationValidator, + _organizationDuoUniversalTokenProvider, + _organizationRepository, _organizationUserRepository, _mailService, _logger, @@ -102,7 +108,7 @@ public async Task ValidateAsync_IsBot_UserNotNull_ShouldBuildErrorResult_ShouldL var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - // Assert + // Assert await _eventService.Received(1) .LogUserEventAsync(context.CustomValidatorRequestContext.User.Id, Core.Enums.EventType.User_FailedLogIn); @@ -113,7 +119,7 @@ await _eventService.Received(1) /* Logic path ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - (self hosted) |-> _logger.LogWarning() + (self hosted) |-> _logger.LogWarning() |-> SetErrorResult */ [Theory, BitAutoData] @@ -140,7 +146,7 @@ public async Task ValidateAsync_ContextNotValid_SelfHosted_ShouldBuildErrorResul /* Logic path ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync - |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync + |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync |-> SetErrorResult */ [Theory, BitAutoData] @@ -229,7 +235,7 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed( context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1); _globalSettings.DisableEmailNewDevice = false; - context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device + context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) .Returns(device); @@ -259,7 +265,7 @@ public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_Should context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1); _globalSettings.DisableEmailNewDevice = false; - context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device + context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device _deviceValidator.SaveDeviceAsync(Arg.Any(), Arg.Any()) .Returns(device); @@ -307,7 +313,7 @@ public async Task ValidateAsync_InvalidAuthType_ShouldSetSsoResult( // Act await _sut.ValidateAsync(context); - // Assert + // Assert Assert.True(context.GrantResult.IsError); var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; Assert.Equal("SSO authentication is required.", errorResponse.Message); diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index 47c8facd5c60..9882147af341 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -51,6 +51,8 @@ public BaseRequestValidatorTestWrapper( IEventService eventService, IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, + IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IMailService mailService, ILogger logger, @@ -67,6 +69,8 @@ public BaseRequestValidatorTestWrapper( eventService, deviceValidator, twoFactorAuthenticationValidator, + organizationDuoWebTokenProvider, + organizationRepository, organizationUserRepository, mailService, logger, From f227fa9099cb079ab795fcd451a258f6db115807 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 30 Sep 2024 13:33:33 -0700 Subject: [PATCH 24/25] Formatting --- .../Auth/Controllers/TwoFactorController.cs | 7 +++-- src/Core/Auth/Utilities/DuoUtilities.cs | 4 +-- .../Controllers/TwoFactorControllerTests.cs | 28 +++++++++---------- .../UserTwoFactorDuoResponseModelTests.cs | 2 +- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 11a98ee4f6c6..d0eb823dddaf 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -187,7 +187,7 @@ public async Task GetDuo([FromBody] SecretVerificatio public async Task PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model) { var user = await CheckAsync(model, true); - if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) + if (!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); @@ -218,7 +218,7 @@ public async Task PutOrganizationDuo(string id, await CheckAsync(model, false); var organization = await CheckOrganizationAsync(new Guid(id)); - if(!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) + if (!await _duoTokenProvider.ValidateDuoConfiguration(model.ClientId, model.ClientSecret, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); @@ -412,7 +412,8 @@ public Task PutDeviceVerificationSettings( return Task.FromResult(new DeviceVerificationResponseModel(false, false)); } - private async Task CheckOrganizationAsync(Guid organizationId){ + private async Task CheckOrganizationAsync(Guid organizationId) + { if (!await _currentContext.ManagePolicies(organizationId)) { throw new NotFoundException(); diff --git a/src/Core/Auth/Utilities/DuoUtilities.cs b/src/Core/Auth/Utilities/DuoUtilities.cs index ddeeb923f971..8db23fa15f71 100644 --- a/src/Core/Auth/Utilities/DuoUtilities.cs +++ b/src/Core/Auth/Utilities/DuoUtilities.cs @@ -6,9 +6,9 @@ public class DuoUtilities { public static bool HasProperDuoMetadata(TwoFactorProvider provider) { - return provider?.MetaData != null && + return provider?.MetaData != null && provider.MetaData.ContainsKey("ClientId") && - provider.MetaData.ContainsKey("ClientSecret") && + provider.MetaData.ContainsKey("ClientSecret") && provider.MetaData.ContainsKey("Host") && ValidDuoHost((string)provider.MetaData["Host"]); } diff --git a/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs index 576ee629af5a..8b3faebd74c0 100644 --- a/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs @@ -1,18 +1,18 @@ -using Xunit; -using NSubstitute; -using Bit.Api.Auth.Controllers; -using Bit.Test.Common.AutoFixture.Attributes; -using Bit.Test.Common.AutoFixture; -using Bit.Core.Services; +using Bit.Api.Auth.Controllers; +using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Core.Entities; using Bit.Api.Auth.Models.Response.TwoFactor; -using Bit.Core.Exceptions; -using Bit.Api.Auth.Models.Request; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Identity; using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; namespace Bit.Api.Test.Auth.Controllers; @@ -33,7 +33,6 @@ public async Task CheckAsync_UserNull_ThrowsUnauthorizedException(SecretVerifica // Assert await Assert.ThrowsAsync(result); - } [Theory, BitAutoData] @@ -175,7 +174,7 @@ public async Task CheckOrganizationAsync_GetByIdAsync_ThrowsNotFoundException( sutProvider.GetDependency() .ManagePolicies(default) .ReturnsForAnyArgs(true); - + sutProvider.GetDependency() .GetByIdAsync(default) .ReturnsForAnyArgs(null as Organization); @@ -242,7 +241,7 @@ public async Task PutOrganizationDuo_Success( .ReturnsForAnyArgs(true); // Act - var result = + var result = await sutProvider.Sut.PutOrganizationDuo(organization.Id.ToString(), request); // Assert @@ -284,7 +283,8 @@ private void SetupCheckAsyncToPass(SutProvider sutProvider, .ReturnsForAnyArgs(true); } - private void SetupCheckOrganizationAsyncToPass(SutProvider sutProvider, Organization organization){ + private void SetupCheckOrganizationAsyncToPass(SutProvider sutProvider, Organization organization) + { sutProvider.GetDependency() .ManagePolicies(default) .ReturnsForAnyArgs(true); diff --git a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs index 699bbeb33d2b..9d4e961da466 100644 --- a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs @@ -22,7 +22,7 @@ public void User_WithDuo_UserNull_ThrowsArgumentException(User user) } catch (ArgumentNullException e) { - // Assert + // Assert Assert.Equal("Value cannot be null. (Parameter 'user')", e.Message); } } From 5452259fa217e0ed830e90804cf3bbed0bf18b76 Mon Sep 17 00:00:00 2001 From: Ike Kottlowski Date: Mon, 30 Sep 2024 14:50:25 -0700 Subject: [PATCH 25/25] formatting --- src/Core/Auth/Identity/DuoTokenProvider.cs | 4 ++-- test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core/Auth/Identity/DuoTokenProvider.cs b/src/Core/Auth/Identity/DuoTokenProvider.cs index 384106668e84..2bceb889b3d9 100644 --- a/src/Core/Auth/Identity/DuoTokenProvider.cs +++ b/src/Core/Auth/Identity/DuoTokenProvider.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Entities; @@ -125,4 +125,4 @@ public async Task ValidateDuoConfiguration(string clientId, string clientS return await client.DoHealthCheck(false); } -} \ No newline at end of file +} diff --git a/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs index bd45fd4bc5e1..4ee9f44ae3f4 100644 --- a/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs +++ b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models; using Bit.Core.Auth.Utilities; using Bit.Test.Common.AutoFixture.Attributes; using Xunit;