diff --git a/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs b/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs index 316abbe9a2..0f9f982a8b 100644 --- a/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs +++ b/src/Core/Auth/Identity/TemporaryDuoWebV4SDKService.cs @@ -1,12 +1,14 @@ 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; -/* +/* 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 @@ -22,6 +24,7 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService { private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; + private readonly IDataProtectorTokenFactory _tokenDataFactory; /// /// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK @@ -30,10 +33,12 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService /// used to fetch vault URL for Redirect URL public TemporaryDuoWebV4SDKService( ICurrentContext currentContext, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + IDataProtectorTokenFactory tokenDataFactory) { _currentContext = currentContext; _globalSettings = globalSettings; + _tokenDataFactory = tokenDataFactory; } /// @@ -56,7 +61,7 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService return null; } - var state = Duo.Client.GenerateState(); //? Not sure on this yet. But required for GenerateAuthUrl + var state = _tokenDataFactory.Protect(new DuoUserStateTokenable(user)); var authUrl = duoClient.GenerateAuthUri(user.Email, state); return authUrl; @@ -82,8 +87,20 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService 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 - var res = await duoClient.ExchangeAuthorizationCodeFor2faResult(token, user.Email); return res.AuthResult.Result == "allow"; } @@ -100,7 +117,7 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService /// 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 + // 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}", diff --git a/src/Core/Auth/Models/Business/Tokenables/DuoUserStateTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/DuoUserStateTokenable.cs new file mode 100644 index 0000000000..45d00bd865 --- /dev/null +++ b/src/Core/Auth/Models/Business/Tokenables/DuoUserStateTokenable.cs @@ -0,0 +1,37 @@ +using Bit.Core.Entities; +using Bit.Core.Tokens; +using Newtonsoft.Json; + +namespace Bit.Core.Auth.Models.Business.Tokenables; + +public class DuoUserStateTokenable : Tokenable +{ + public const string ClearTextPrefix = "BwDuoUserId"; + public const string DataProtectorPurpose = "DuoUserIdTokenDataProtector"; + public const string TokenIdentifier = "DuoUserIdToken"; + public string Identifier { get; set; } = TokenIdentifier; + public Guid UserId { get; set; } + + public override bool Valid => Identifier == TokenIdentifier && + UserId != default; + + [JsonConstructor] + public DuoUserStateTokenable() + { + } + + public DuoUserStateTokenable(User user) + { + UserId = user?.Id ?? default; + } + + public bool TokenIsValid(User user) + { + if (UserId == default || user == null) + { + return false; + } + + return UserId == user.Id; + } +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 1bd805fb8e..7535aba882 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -197,6 +197,12 @@ public static class ServiceCollectionExtensions OrgUserInviteTokenable.DataProtectorPurpose, serviceProvider.GetDataProtectionProvider(), serviceProvider.GetRequiredService>>())); + services.AddSingleton>(serviceProvider => + new DataProtectorTokenFactory( + DuoUserStateTokenable.ClearTextPrefix, + DuoUserStateTokenable.DataProtectorPurpose, + serviceProvider.GetDataProtectionProvider(), + serviceProvider.GetRequiredService>>())); } public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)