diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index f2578fbc6551..53dac52582d2 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -6,13 +6,12 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Utilities; +using Bit.Core.Auth.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Core.Utilities; using Fido2NetLib; @@ -29,11 +28,10 @@ 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 IDuoUniversalConfigService _duoUniversalConfigService; private readonly IDataProtectorTokenFactory _twoFactorAuthenticatorDataProtector; private readonly IDataProtectorTokenFactory _ssoEmailTwoFactorSessionDataProtector; @@ -41,22 +39,20 @@ public TwoFactorController( IUserService userService, IOrganizationRepository organizationRepository, IOrganizationService organizationService, - GlobalSettings globalSettings, UserManager userManager, ICurrentContext currentContext, IVerifyAuthRequestCommand verifyAuthRequestCommand, - IFeatureService featureService, + IDuoUniversalConfigService duoUniversalConfigService, IDataProtectorTokenFactory twoFactorAuthenticatorDataProtector, IDataProtectorTokenFactory ssoEmailTwoFactorSessionDataProtector) { _userService = userService; _organizationRepository = organizationRepository; _organizationService = organizationService; - _globalSettings = globalSettings; _userManager = userManager; _currentContext = currentContext; _verifyAuthRequestCommand = verifyAuthRequestCommand; - _featureService = featureService; + _duoUniversalConfigService = duoUniversalConfigService; _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector; _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector; } @@ -184,21 +180,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 _duoUniversalConfigService.ValidateDuoConfiguration(model.ClientSecret, model.ClientId, model.Host)) { throw new BadRequestException( "Duo configuration settings are not valid. Please re-check the Duo Admin panel."); @@ -241,21 +223,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 _duoUniversalConfigService.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 f2f01a2378e5..f05c9eff3264 100644 --- a/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Auth/Models/Request/TwoFactorRequestModels.cs @@ -43,21 +43,16 @@ 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. + String lengths based on Duo's documentation + https://github.com/duosecurity/duo_universal_csharp/blob/main/DuoUniversal/Client.cs */ - [StringLength(50)] + [Required] + [StringLength(20, MinimumLength = 20, ErrorMessage = "Client Id must be exactly 20 characters.")] public string ClientId { get; set; } - [StringLength(50)] + [Required] + [StringLength(40, MinimumLength = 40, ErrorMessage = "Client Secret must be exactly 40 characters.")] 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; } public User ToUser(User existingUser) @@ -65,22 +60,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 +86,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,34 +109,22 @@ public Organization ToOrganization(Organization existingOrg) public override IEnumerable Validate(ValidationContext validationContext) { - if (!DuoApi.ValidHost(Host)) + var results = new List(); + if (string.IsNullOrWhiteSpace(ClientId)) { - yield return new ValidationResult("Host is invalid.", [nameof(Host)]); + results.Add(new ValidationResult("ClientId is required.", [nameof(ClientId)])); } - 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)) + if (string.IsNullOrWhiteSpace(ClientSecret)) { - SecretKey = ClientSecret; - IntegrationKey = ClientId; + results.Add(new ValidationResult("ClientSecret is required.", [nameof(ClientSecret)])); } - else if (!string.IsNullOrWhiteSpace(SecretKey) && !string.IsNullOrWhiteSpace(IntegrationKey)) + + if (string.IsNullOrWhiteSpace(Host) || !DuoUtilities.ValidDuoHost(Host)) { - ClientSecret = SecretKey; - ClientId = IntegrationKey; + results.Add(new ValidationResult("Host is invalid.", [nameof(Host)])); } + return results; } } diff --git a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs index 8b8c36d2e8b4..79012783a4df 100644 --- a/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs +++ b/src/Api/Auth/Models/Response/TwoFactor/TwoFactorDuoResponseModel.cs @@ -13,37 +13,26 @@ 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); } - public TwoFactorDuoResponseModel(Organization org) + public TwoFactorDuoResponseModel(Organization organization) : base(ResponseObj) { - if (org == null) - { - throw new ArgumentNullException(nameof(org)); - } + ArgumentNullException.ThrowIfNull(organization); - var provider = org.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); Build(provider); } 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 = MaskSecret((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,30 +58,7 @@ 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) + private static string MaskSecret(string key) { if (string.IsNullOrWhiteSpace(key) || key.Length <= 6) { diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 8a7721bcbfb6..0b95dfdd58f1 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -23,7 +23,6 @@ using Bit.SharedWeb.Utilities; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection.Extensions; -using Bit.Core.Auth.Identity; using Bit.Core.Auth.UserFeatures; using Bit.Core.Entities; using Bit.Core.Billing.Extensions; @@ -32,7 +31,7 @@ using Bit.Core.Vault.Entities; using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Core.Auth.Models.Data; - +using Bit.Core.Auth.Identity.TokenProviders; #if !OSS using Bit.Commercial.Core.SecretsManager; 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/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/Core/Auth/Identity/AuthenticatorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs similarity index 97% rename from src/Core/Auth/Identity/AuthenticatorTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs index fae2d23b19dd..9468e4d57179 100644 --- a/src/Core/Auth/Identity/AuthenticatorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using OtpNet; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider { diff --git a/src/Core/Auth/Identity/TokenProviders/DuoTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/DuoTokenProvider.cs new file mode 100644 index 000000000000..0af1bcb2a0f4 --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/DuoTokenProvider.cs @@ -0,0 +1,104 @@ +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.TokenProviders; + +/// +/// OrganizationDuo and Duo types both use the same flows so both of those Token Providers will +/// inherit from this class +/// +public class DuoTokenProvider +{ + private readonly ICurrentContext _currentContext; + private readonly GlobalSettings _globalSettings; + + public DuoTokenProvider(ICurrentContext currentContext, GlobalSettings globalSettings) + { + _currentContext = currentContext; + _globalSettings = globalSettings; + } + + protected async Task GenerateAuthUrlAsync( + TwoFactorProvider provider, + IDataProtectorTokenFactory tokenDataFactory, + User user) + { + var duoClient = await BuildDuoTwoFactorClientAsync(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 BuildDuoTwoFactorClientAsync(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 + 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 + _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; + } +} diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs new file mode 100644 index 000000000000..37fc62c50783 --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs @@ -0,0 +1,80 @@ +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; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Auth.Identity.TokenProviders; + +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 (!DuoUtilities.HasProperDuoMetadata(provider)) + { + return null; + } + + return provider; + } +} diff --git a/src/Core/Auth/Identity/EmailTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs similarity index 97% rename from src/Core/Auth/Identity/EmailTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs index 1db9e13ee535..be94124c03eb 100644 --- a/src/Core/Auth/Identity/EmailTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class EmailTokenProvider : IUserTwoFactorTokenProvider { diff --git a/src/Core/Auth/Identity/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs similarity index 97% rename from src/Core/Auth/Identity/EmailTwoFactorTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index 607d86a13a8f..b0ad9bd4801b 100644 --- a/src/Core/Auth/Identity/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class EmailTwoFactorTokenProvider : EmailTokenProvider { diff --git a/src/Core/Auth/Identity/IOrganizationTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/IOrganizationTwoFactorTokenProvider.cs similarity index 87% rename from src/Core/Auth/Identity/IOrganizationTwoFactorTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/IOrganizationTwoFactorTokenProvider.cs index 4226cd036165..7e2a8c2ac2d2 100644 --- a/src/Core/Auth/Identity/IOrganizationTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/IOrganizationTwoFactorTokenProvider.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public interface IOrganizationTwoFactorTokenProvider { diff --git a/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs new file mode 100644 index 000000000000..df2b774b444a --- /dev/null +++ b/src/Core/Auth/Identity/TokenProviders/OrganizationDuoUniversalTokenProvider.cs @@ -0,0 +1,80 @@ +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.Auth.Utilities; +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.TokenProviders; + +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) + && DuoUtilities.HasProperDuoMetadata(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 (!DuoUtilities.HasProperDuoMetadata(provider)) + { + return null; + } + + return provider; + } +} diff --git a/src/Core/Auth/Identity/TwoFactorRememberTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/TwoFactorRememberTokenProvider.cs similarity index 92% rename from src/Core/Auth/Identity/TwoFactorRememberTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/TwoFactorRememberTokenProvider.cs index cf35eb2eb8ed..44b5d48b8249 100644 --- a/src/Core/Auth/Identity/TwoFactorRememberTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/TwoFactorRememberTokenProvider.cs @@ -4,7 +4,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class TwoFactorRememberTokenProvider : DataProtectorTokenProvider { diff --git a/src/Core/Auth/Identity/WebAuthnTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs similarity index 99% rename from src/Core/Auth/Identity/WebAuthnTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs index a3b4aebea529..202ba3a38cc7 100644 --- a/src/Core/Auth/Identity/WebAuthnTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider { diff --git a/src/Core/Auth/Identity/YubicoOtpTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs similarity index 93% rename from src/Core/Auth/Identity/YubicoOtpTokenProvider.cs rename to src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs index 75785118b8dc..9794a51ae920 100644 --- a/src/Core/Auth/Identity/YubicoOtpTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using YubicoDotNetClient; -namespace Bit.Core.Auth.Identity; +namespace Bit.Core.Auth.Identity.TokenProviders; public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider { @@ -24,7 +24,7 @@ public YubicoOtpTokenProvider( public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) + if (!await userService.CanAccessPremium(user)) { return false; } @@ -46,7 +46,7 @@ public Task GenerateAsync(string purpose, UserManager manager, Use public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) { var userService = _serviceProvider.GetRequiredService(); - if (!(await userService.CanAccessPremium(user))) + if (!await userService.CanAccessPremium(user)) { return false; } diff --git a/src/Core/Auth/Services/IDuoUniversalConfigSerivce.cs b/src/Core/Auth/Services/IDuoUniversalConfigSerivce.cs new file mode 100644 index 000000000000..85b586b8b121 --- /dev/null +++ b/src/Core/Auth/Services/IDuoUniversalConfigSerivce.cs @@ -0,0 +1,19 @@ +namespace Bit.Core.Auth.Services; + +/// +/// In the TwoFactorController before we write a configuration to the database we check if the Duo configuration +/// is valid. This interface creates a simple way to inject the process into those endpoints. +/// +public interface IDuoUniversalConfigService +{ + /// + /// Generates a Duo.Client object for use with Duo SDK v4. This method is to validate a Duo configuration + /// when adding or updating the configuration. This method makes a web request to Duo to verify the configuration. + /// Throws exception if configuration is invalid. + /// + /// Duo client Secret + /// Duo client Id + /// Duo host + /// Boolean + Task ValidateDuoConfiguration(string clientSecret, string clientId, string host); +} diff --git a/src/Core/Auth/Services/Implementations/DuoUniversalConfigService.cs b/src/Core/Auth/Services/Implementations/DuoUniversalConfigService.cs new file mode 100644 index 000000000000..e6965e536a34 --- /dev/null +++ b/src/Core/Auth/Services/Implementations/DuoUniversalConfigService.cs @@ -0,0 +1,23 @@ +using Bit.Core.Auth.Utilities; +using Duo = DuoUniversal; + +namespace Bit.Core.Auth.Services; + +public class DuoUniversalConfigService() : IDuoUniversalConfigService +{ + public async Task ValidateDuoConfiguration(string clientSecret, string clientId, string host) + { + // Do some simple checks to ensure data integrity + if (!DuoUtilities.ValidDuoHost(host) || + string.IsNullOrWhiteSpace(clientSecret) || + string.IsNullOrWhiteSpace(clientId)) + { + return false; + } + // The AuthURI is not important for this health check so we pass in a non-empty string + var client = new Duo.ClientBuilder(clientId, clientSecret, host, "non-empty").Build(); + + // This could throw an exception, the false flag will allow the exception to bubble up + return await client.DoHealthCheck(false); + } +} 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..8db23fa15f71 --- /dev/null +++ b/src/Core/Auth/Utilities/DuoUtilities.cs @@ -0,0 +1,26 @@ +using Bit.Core.Auth.Models; + +namespace Bit.Core.Auth.Utilities; + +public class DuoUtilities +{ + 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)) + { + return (string.IsNullOrWhiteSpace(uri.PathAndQuery) || uri.PathAndQuery == "/") && + uri.Host.StartsWith("api-") && + (uri.Host.EndsWith(".duosecurity.com") || uri.Host.EndsWith(".duofederal.com")); + } + return false; + } +} 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); - } -} diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index 323d09c0e213..26c710944ca3 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -1,9 +1,7 @@ - -using System.Text.Json; -using Bit.Core; +using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; @@ -52,8 +50,7 @@ public interface ITwoFactorAuthenticationValidator public class TwoFactorAuthenticationValidator( IUserService userService, UserManager userManager, - IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, - ITemporaryDuoWebV4SDKService duoWebV4SDKService, + IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, IFeatureService featureService, IApplicationCacheService applicationCacheService, IOrganizationUserRepository organizationUserRepository, @@ -63,8 +60,7 @@ public class TwoFactorAuthenticationValidator( { private readonly IUserService _userService = userService; private readonly UserManager _userManager = userManager; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService = duoWebV4SDKService; + private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider; private readonly IFeatureService _featureService = featureService; private readonly IApplicationCacheService _applicationCacheService = applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository; @@ -153,17 +149,7 @@ public async Task VerifyTwoFactor( { if (organization.TwoFactorProviderIsEnabled(type)) { - // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt - if (_featureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) - { - 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); + return await _organizationDuoUniversalTokenProvider.ValidateAsync(token, organization, user); } return false; } @@ -181,19 +167,6 @@ public async Task VerifyTwoFactor( { 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); default: @@ -248,10 +221,11 @@ private async Task> BuildTwoFactorParams(Organization in the future the `AuthUrl` will be the generated "token" - PM-8107 */ if (type == TwoFactorProviderType.OrganizationDuo && - await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) + await _organizationDuoUniversalTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) { twoFactorParams.Add("Host", provider.MetaData["Host"]); - twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user)); + twoFactorParams.Add("AuthUrl", + await _organizationDuoUniversalTokenProvider.GenerateAsync(organization, user)); return twoFactorParams; } @@ -261,13 +235,9 @@ await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organizati CoreHelpers.CustomProviderName(type)); switch (type) { - /* - Note: Duo is in the midst of being updated to use the UserManager built-in TwoFactor class - in the future the `AuthUrl` will be the generated "token" - PM-8107 - */ case TwoFactorProviderType.Duo: twoFactorParams.Add("Host", provider.MetaData["Host"]); - twoFactorParams.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user)); + twoFactorParams.Add("AuthUrl", token); break; case TwoFactorProviderType.WebAuthn: if (token != null) diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 5a5585952460..bef5e267ef46 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.IdentityServer; using Bit.Core.Auth.LoginFeatures; using Bit.Core.Auth.Models.Business.Tokenables; @@ -112,6 +113,7 @@ public static void AddBaseServices(this IServiceCollection services, IGlobalSett services.AddSingleton(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddLoginServices(); services.AddScoped(); @@ -386,7 +388,7 @@ public static void AddNoopServices(this IServiceCollection services) public static IdentityBuilder AddCustomIdentityServices( this IServiceCollection services, GlobalSettings globalSettings) { - services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.Configure(options => options.IterationCount = 100000); services.Configure(options => @@ -428,7 +430,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)) diff --git a/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs new file mode 100644 index 000000000000..7f10f9ea00a2 --- /dev/null +++ b/test/Api.Test/Auth/Controllers/TwoFactorControllerTests.cs @@ -0,0 +1,295 @@ +using Bit.Api.Auth.Controllers; +using Bit.Api.Auth.Models.Request; +using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Api.Auth.Models.Response.TwoFactor; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Services; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +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; + +[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) + .Returns(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/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 index ab05a94f13fd..295d7cbb5afa 100644 --- a/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs +++ b/test/Api.Test/Auth/Models/Request/TwoFactorDuoRequestModelValidationTests.cs @@ -39,12 +39,9 @@ public void ShouldReturnValidationError_WhenValuesAreInvalid() 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); + Assert.NotEmpty(result); + Assert.True(result.Select(x => x.MemberNames.Contains("ClientId")).Any()); + Assert.True(result.Select(x => x.MemberNames.Contains("ClientSecret")).Any()); } [Fact] diff --git a/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs b/test/Api.Test/Auth/Models/Request/UserTwoFactorDuoRequestModelTests.cs index 28dfc83a2de7..56c9af1e0d7e 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" }; @@ -26,12 +24,9 @@ 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"]); - 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 +44,6 @@ public void ShouldUpdateTwoFactorProvider_WhenExistingProviderExists() { ClientId = "newClientId", ClientSecret = "newClientSecret", - IntegrationKey = "newIntegrationKey", - SecretKey = "newSecretKey", Host = "newExample.com" }; @@ -58,65 +51,10 @@ 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"]); - 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..dfe27467dd1d 100644 --- a/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/OrganizationTwoFactorDuoResponseModelTests.cs @@ -8,42 +8,6 @@ namespace Bit.Api.Test.Auth.Models.Response; public class OrganizationTwoFactorDuoResponseModelTests { - [Theory] - [BitAutoData] - public void Organization_WithDuoV4_ShouldBuildModel(Organization organization) - { - // Arrange - organization.TwoFactorProviders = GetTwoFactorOrganizationDuoV4ProvidersJson(); - - // Act - var model = new TwoFactorDuoResponseModel(organization); - - // Assert if v4 data Ikey and Skey are set to clientId and clientSecret - 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) @@ -54,12 +18,10 @@ public void Organization_WithDuo_ShouldBuildModel(Organization organization) // Act var model = new TwoFactorDuoResponseModel(organization); - /// 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); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); } [Theory] @@ -72,38 +34,33 @@ public void Organization_WithDuoEmpty_ShouldFail(Organization organization) // Act var model = new TwoFactorDuoResponseModel(organization); - /// Assert + // Assert Assert.False(model.Enabled); } [Theory] [BitAutoData] - public void Organization_WithTwoFactorProvidersNull_ShouldFail(Organization organization) + public void Organization_WithTwoFactorProvidersNull_ShouldThrow(Organization organization) { // Arrange - organization.TwoFactorProviders = "{\"6\" : {}}"; + organization.TwoFactorProviders = null; // Act - var model = new TwoFactorDuoResponseModel(organization); - - /// Assert - Assert.False(model.Enabled); + try + { + var model = new TwoFactorDuoResponseModel(organization); + + } + catch (Exception ex) + { + // Assert + Assert.IsType(ex); + } } 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..2441f104b34f 100644 --- a/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs +++ b/test/Api.Test/Auth/Models/Response/UserTwoFactorDuoResponseModelTests.cs @@ -10,38 +10,21 @@ public class UserTwoFactorDuoResponseModelTests { [Theory] [BitAutoData] - public void User_WithDuoV4_ShouldBuildModel(User user) + public void User_WithDuo_UserNull_ThrowsArgumentException(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.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); + try + { + var model = new TwoFactorDuoResponseModel(null as User); + } + catch (ArgumentNullException e) + { + // Assert + Assert.Equal("Value cannot be null. (Parameter 'user')", e.Message); + } } [Theory] @@ -54,12 +37,10 @@ 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); - Assert.Equal("clientId", model.IntegrationKey); - Assert.Equal("secret************", model.SecretKey); } [Theory] @@ -84,26 +65,23 @@ public void User_WithTwoFactorProvidersNull_ShouldFail(User user) user.TwoFactorProviders = null; // Act - var model = new TwoFactorDuoResponseModel(user); + try + { + var model = new TwoFactorDuoResponseModel(user); - /// Assert - Assert.False(model.Enabled); - } + } + catch (Exception ex) + { + // Assert + Assert.IsType(ex); + + } - private string GetTwoFactorDuoProvidersJson() - { - return - "{\"2\":{\"Enabled\":true,\"MetaData\":{\"SKey\":\"SKey\",\"IKey\":\"IKey\",\"ClientSecret\":\"secretClientSecret\",\"ClientId\":\"clientId\",\"Host\":\"example.com\"}}}"; } - private string GetTwoFactorDuoV4ProvidersJson() + private string GetTwoFactorDuoProvidersJson() { 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/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/AuthenticationTokenProviderTests.cs b/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs index a76216f3fab7..c9646e627c00 100644 --- a/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs @@ -1,5 +1,5 @@ using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Entities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs index b90f71ae71bc..da2d4a282adb 100644 --- a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs @@ -19,7 +19,6 @@ public abstract class BaseTokenProviderTests { public abstract TwoFactorProviderType TwoFactorProviderType { get; } - #region Helpers protected static IEnumerable SetupCanGenerateData(params (Dictionary MetaData, bool ExpectedResponse)[] data) { return data.Select(d => @@ -48,6 +47,9 @@ protected virtual void SetupUserService(IUserService userService, User user) userService .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType, user) .Returns(true); + userService + .CanAccessPremium(user) + .Returns(true); } protected static UserManager SubstituteUserManager() @@ -76,7 +78,6 @@ protected void MockDatabase(User user, Dictionary metaData) user.TwoFactorProviders = JsonHelpers.LegacySerialize(providers); } - #endregion public virtual async Task RunCanGenerateTwoFactorTokenAsync(Dictionary metaData, bool expectedResponse, User user, SutProvider sutProvider) diff --git a/test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs new file mode 100644 index 000000000000..eee709260c6f --- /dev/null +++ b/test/Core.Test/Auth/Identity/DuoTwoFactorTokenProviderTests.cs @@ -0,0 +1,61 @@ +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Identity.TokenProviders; +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/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs index c5855c2343a4..46bfba549e80 100644 --- a/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs @@ -1,5 +1,5 @@ using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Entities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/Auth/Services/DuoUniversalConfigServiceTests.cs b/test/Core.Test/Auth/Services/DuoUniversalConfigServiceTests.cs new file mode 100644 index 000000000000..05a9faef759c --- /dev/null +++ b/test/Core.Test/Auth/Services/DuoUniversalConfigServiceTests.cs @@ -0,0 +1,31 @@ +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Auth.Services; + +public class DuoUniversalConfigServiceTests +{ + private IDuoUniversalConfigService _sut; + + public DuoUniversalConfigServiceTests() + { + _sut = new DuoUniversalConfigService(); + } + + [Theory] + [BitAutoData("", "ClientId", "ClientSecret")] + [BitAutoData("api-valid.duosecurity.com", "", "ClientSecret")] + [BitAutoData("api-valid.duosecurity.com", "ClientId", "")] + public async void ValidateDuoConfiguration_InvalidConfig_ReturnsFalse( + string host, string clientId, string clientSecret) + { + // Arrange + /* AutoData handles arrangement */ + + // Act + var result = await _sut.ValidateDuoConfiguration(clientSecret, clientId, host); + + // Assert + Assert.False(result); + } +} diff --git a/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs new file mode 100644 index 000000000000..2bcad7c67a14 --- /dev/null +++ b/test/Core.Test/Auth/Utilities/DuoUtilitiesTests.cs @@ -0,0 +1,59 @@ +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(true, "api-valid.duosecurity.com")] + [BitAutoData(false, "invalid")] + [BitAutoData(false, "api-valid.duosecurity.com", null, "clientSecret")] + [BitAutoData(false, "api-valid.duosecurity.com", "ClientId", null)] + [BitAutoData(false, "api-valid.duosecurity.com", null, null)] + public void HasProperDuoMetadata_ReturnsMatchesExpected( + bool expectedResponse, string host, string clientId, string clientSecret) + { + // Arrange + var provider = new TwoFactorProvider + { + MetaData = new Dictionary + { + ["ClientId"] = clientId, + ["ClientSecret"] = clientSecret, + ["Host"] = host, + } + }; + + // Act + var result = DuoUtilities.HasProperDuoMetadata(provider); + + // Assert + Assert.Equal(result, expectedResponse); + } + + [Fact] + public void HasProperDuoMetadata_ProviderIsNull_ReturnsMatchesExpected() + { + // Act + var result = DuoUtilities.HasProperDuoMetadata(null); + + // Assert + Assert.False(result); + } + + [Theory] + [BitAutoData("api-valid.duosecurity.com", true)] + [BitAutoData("api-valid.duofederal.com", true)] + [BitAutoData("invalid", false)] + public void ValidDuoHost_HostIsValid_ReturnTrue(string host, bool expectedResponse) + { + // Act + var result = DuoUtilities.ValidDuoHost(host); + + // Assert + Assert.Equal(result, expectedResponse); + } +} diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs index 468ef5a169ba..4e598c436d6f 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs @@ -28,7 +28,7 @@ namespace Bit.Identity.IntegrationTest.Endpoints; public class IdentityServerTwoFactorTests : IClassFixture { - const string _organizationTwoFactor = """{"6":{"Enabled":true,"MetaData":{"IKey":"DIEFB13LB49IEB3459N2","SKey":"0ZnsZHav0KcNPBZTS6EOUwqLPoB0sfMd5aJeWExQ","Host":"api-example.duosecurity.com"}}}"""; + const string _organizationTwoFactor = """{"6":{"Enabled":true,"MetaData":{"ClientId":"DIEFB13LB49IEB3459N2","ClientSecret":"0ZnsZHav0KcNPBZTS6EOUwqLPoB0sfMd5aJeWExQ","Host":"api-example.duosecurity.com"}}}"""; const string _testEmail = "test+2farequired@email.com"; const string _testPassword = "master_password_hash"; const string _userEmailTwoFactor = """{"1": { "Enabled": true, "MetaData": { "Email": "test+2farequired@email.com"}}}"""; @@ -140,7 +140,7 @@ await CreateSsoOrganizationAndUserAsync( { "password", _testPassword }, }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); - // Assert + // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); var root = responseBody.RootElement; var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString(); @@ -168,7 +168,7 @@ public async Task TokenEndpoint_GrantTypePassword_RememberTwoFactorType_InvalidT [Theory, BitAutoData] public async Task TokenEndpoint_GrantTypeClientCredential_OrgTwoFactorRequired_Success(Organization organization, OrganizationApiKey organizationApiKey) { - // Arrange + // Arrange organization.Enabled = true; organization.UseApi = true; organization.Use2fa = true; @@ -258,7 +258,7 @@ await CreateSsoOrganizationAndUserAsync( { "redirect_uri", "https://localhost:8080/sso-connector.html" } }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); - // Assert + // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); var root = responseBody.RootElement; var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString(); @@ -320,7 +320,7 @@ await CreateSsoOrganizationAndUserAsync( }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); - // Assert + // Assert var body = await AssertHelper.AssertResponseTypeIs(twoFactorProvidedContext); var root = body.RootElement; @@ -338,6 +338,7 @@ public async Task TokenEndpoint_GrantTypeAuthCode_OrgTwoFactorRequired_OrgDuoTwo { MemberDecryptionType = MemberDecryptionType.MasterPassword, }; + await CreateSsoOrganizationAndUserAsync( localFactory, ssoConfigData, challenge, _testEmail, orgTwoFactor: _organizationTwoFactor); @@ -355,7 +356,7 @@ await CreateSsoOrganizationAndUserAsync( { "redirect_uri", "https://localhost:8080/sso-connector.html" } }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); - // Assert + // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); var root = responseBody.RootElement; var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString(); diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index 5783375ff788..dfb877b8d680 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -1,8 +1,6 @@ -using Bit.Core; -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Identity; -using Bit.Core.Auth.Models; +using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Context; using Bit.Core.Entities; @@ -28,8 +26,7 @@ public class TwoFactorAuthenticationValidatorTests { private readonly IUserService _userService; private readonly UserManagerTestWrapper _userManager; - private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; - private readonly ITemporaryDuoWebV4SDKService _temporaryDuoWebV4SDKService; + private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider; private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository; @@ -42,8 +39,7 @@ public TwoFactorAuthenticationValidatorTests() { _userService = Substitute.For(); _userManager = SubstituteUserManager(); - _organizationDuoWebTokenProvider = Substitute.For(); - _temporaryDuoWebV4SDKService = Substitute.For(); + _organizationDuoUniversalTokenProvider = Substitute.For(); _featureService = Substitute.For(); _applicationCacheService = Substitute.For(); _organizationUserRepository = Substitute.For(); @@ -54,8 +50,7 @@ public TwoFactorAuthenticationValidatorTests() _sut = new TwoFactorAuthenticationValidator( _userService, _userManager, - _organizationDuoWebTokenProvider, - _temporaryDuoWebV4SDKService, + _organizationDuoUniversalTokenProvider, _featureService, _applicationCacheService, _organizationUserRepository, @@ -439,7 +434,7 @@ public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( string token) { // Arrange - _organizationDuoWebTokenProvider.ValidateAsync( + _organizationDuoUniversalTokenProvider.ValidateAsync( token, organization, user).Returns(true); _userManager.TWO_FACTOR_ENABLED = true; @@ -457,70 +452,6 @@ public async void VerifyTwoFactorAsync_Organization_ValidToken_ReturnsTrue( Assert.True(result); } - [Theory] - [BitAutoData(TwoFactorProviderType.Duo)] - [BitAutoData(TwoFactorProviderType.OrganizationDuo)] - public async void VerifyTwoFactorAsync_TemporaryDuoService_ValidToken_ReturnsTrue( - 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 = 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_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() { return new UserManagerTestWrapper(