diff --git a/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs b/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs new file mode 100644 index 000000000..316abbe9a --- /dev/null +++ b/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs @@ -0,0 +1,121 @@ +using Bit.Core.Auth.Models; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Settings; +using Duo = DuoUniversal; + +namespace Bit.Core.Auth.Identity; + +/* + PM-5156 addresses tech debt + Interface to allow for DI, will end up being removed as part of the removal of the old Duo SDK v2 flows. + This service is to support SDK v4 flows for Duo. At some time in the future we will need + to combine this service with the DuoWebTokenProvider and OrganizationDuoWebTokenProvider to support SDK v4. +*/ +public interface ITemporaryDuoWebV4SDKService +{ + Task GenerateAsync(TwoFactorProvider provider, User user); + Task ValidateAsync(string token, TwoFactorProvider provider, User user); +} + +public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService +{ + private readonly ICurrentContext _currentContext; + private readonly GlobalSettings _globalSettings; + + /// + /// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK + /// + /// used to fetch initiating Client + /// used to fetch vault URL for Redirect URL + public TemporaryDuoWebV4SDKService( + ICurrentContext currentContext, + GlobalSettings globalSettings) + { + _currentContext = currentContext; + _globalSettings = globalSettings; + } + + /// + /// Provider agnostic (either Duo or OrganizationDuo) method to generate a Duo Auth URL + /// + /// Either Duo or OrganizationDuo + /// self + /// AuthUrl for DUO SDK v4 + public async Task GenerateAsync(TwoFactorProvider provider, User user) + { + if (!HasProperMetaData(provider)) + { + return null; + } + + + var duoClient = await BuildDuoClientAsync(provider); + if (duoClient == null) + { + return null; + } + + var state = Duo.Client.GenerateState(); //? Not sure on this yet. But required for GenerateAuthUrl + var authUrl = duoClient.GenerateAuthUri(user.Email, state); + + return authUrl; + } + + /// + /// Validates Duo SDK v4 response + /// + /// response form Duo + /// TwoFactorProviderType Duo or OrganizationDuo + /// self + /// true or false depending on result of verification + public async Task ValidateAsync(string token, TwoFactorProvider provider, User user) + { + if (!HasProperMetaData(provider)) + { + return false; + } + + var duoClient = await BuildDuoClientAsync(provider); + if (duoClient == null) + { + return false; + } + + // If the result of the exchange doesn't throw an exception and it's not null, then it's valid + var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(token, user.Email); + return res.AuthResult.Result == "allow"; + } + + private bool HasProperMetaData(TwoFactorProvider provider) + { + return provider?.MetaData != null && provider.MetaData.ContainsKey("IKey") && + provider.MetaData.ContainsKey("SKey") && provider.MetaData.ContainsKey("Host"); + } + + /// + /// 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 + private async Task BuildDuoClientAsync(TwoFactorProvider provider) + { + // Fetch Client name from header value since duo auth can be initiated from multiple clients and we want + // to redirect back to the correct 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["IKey"], + (string)provider.MetaData["SKey"], + (string)provider.MetaData["Host"], + redirectUri).Build(); + + if (!await client.DoHealthCheck()) + { + return null; + } + return client; + } +} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7b1524aa5..07fe17638 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -105,6 +105,7 @@ public static class FeatureFlagKeys public const string AutofillOverlay = "autofill-overlay"; public const string ItemShare = "item-share"; public const string KeyRotationImprovements = "key-rotation-improvements"; + public const string DuoRedirect = "duo-redirect"; public const string FlexibleCollectionsMigration = "flexible-collections-migration"; public const string FlexibleCollectionsSignup = "flexible-collections-signup"; diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index ce2e3832b..f50412495 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -28,6 +28,7 @@ + diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index f638783a5..f6d8b3b23 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -38,6 +38,7 @@ public abstract class BaseRequestValidator where T : class private readonly IDeviceService _deviceService; private readonly IEventService _eventService; private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider; + private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IApplicationCacheService _applicationCacheService; @@ -63,6 +64,7 @@ public abstract class BaseRequestValidator where T : class IUserService userService, IEventService eventService, IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider, + ITemporaryDuoWebV4SDKService duoWebV4SDKService, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IApplicationCacheService applicationCacheService, @@ -84,6 +86,7 @@ public abstract class BaseRequestValidator where T : class _userService = userService; _eventService = eventService; _organizationDuoWebTokenProvider = organizationDuoWebTokenProvider; + _duoWebV4SDKService = duoWebV4SDKService; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _applicationCacheService = applicationCacheService; @@ -167,7 +170,7 @@ public abstract class BaseRequestValidator where T : class } return; } - // We only want to track TOTPs in the chache to enforce one time use. + // We only want to track TOTPs in the cache to enforce one time use. if (twoFactorProviderType == TwoFactorProviderType.Authenticator || twoFactorProviderType == TwoFactorProviderType.Email) { await Core.Utilities.DistributedCacheExtensions.SetAsync(_distributedCache, cacheKey, twoFactorToken, _cacheEntryOptions); @@ -428,10 +431,23 @@ public abstract class BaseRequestValidator where T : class case TwoFactorProviderType.WebAuthn: case TwoFactorProviderType.Remember: if (type != TwoFactorProviderType.Remember && - !(await _userService.TwoFactorProviderIsEnabledAsync(type, user))) + !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) { return false; } + // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt + if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + if (type == TwoFactorProviderType.Duo) + { + if (!token.Contains(':')) + { + // We have to send the provider to the DuoWebV4SDKService to create the DuoClient + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Duo); + return await _duoWebV4SDKService.ValidateAsync(token, provider, user); + } + } + } return await _userManager.VerifyTwoFactorTokenAsync(user, CoreHelpers.CustomProviderName(type), token); @@ -441,6 +457,20 @@ public abstract class BaseRequestValidator where T : class return false; } + // DUO SDK v4 Update: try to validate the token - PM-5156 addresses tech debt + if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + if (type == TwoFactorProviderType.OrganizationDuo) + { + if (!token.Contains(':')) + { + // We have to send the provider to the DuoWebV4SDKService to create the DuoClient + var provider = organization.GetTwoFactorProvider(TwoFactorProviderType.OrganizationDuo); + return await _duoWebV4SDKService.ValidateAsync(token, provider, user); + } + } + } + return await _organizationDuoWebTokenProvider.ValidateAsync(token, organization, user); default: return false; @@ -465,11 +495,19 @@ public abstract class BaseRequestValidator where T : class CoreHelpers.CustomProviderName(type)); if (type == TwoFactorProviderType.Duo) { - return new Dictionary + var duoResponse = new Dictionary { ["Host"] = provider.MetaData["Host"], ["Signature"] = token }; + + // DUO SDK v4 Update: Duo-Redirect + if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + // Generate AuthUrl from DUO SDK v4 token provider + duoResponse.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user)); + } + return duoResponse; } else if (type == TwoFactorProviderType.WebAuthn) { @@ -493,13 +531,19 @@ public abstract class BaseRequestValidator where T : class case TwoFactorProviderType.OrganizationDuo: if (await _organizationDuoWebTokenProvider.CanGenerateTwoFactorTokenAsync(organization)) { - return new Dictionary + var duoResponse = new Dictionary { ["Host"] = provider.MetaData["Host"], ["Signature"] = await _organizationDuoWebTokenProvider.GenerateAsync(organization, user) }; + // DUO SDK v4 Update: DUO-Redirect + if (FeatureService.IsEnabled(FeatureFlagKeys.DuoRedirect)) + { + // Generate AuthUrl from DUO SDK v4 token provider + duoResponse.Add("AuthUrl", await _duoWebV4SDKService.GenerateAsync(provider, user)); + } + return duoResponse; } - return null; default: return null; diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index 4710184f7..96243533e 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -33,6 +33,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator(); + services.AddScoped(); + services.AddScoped(); services.Configure(options => options.IterationCount = 100000); services.Configure(options => {