diff --git a/src/Core/Auth/Identity/AuthenticatorTokenProvider.cs b/src/Core/Auth/Identity/AuthenticatorTokenProvider.cs index d6b3d1526f..fae2d23b19 100644 --- a/src/Core/Auth/Identity/AuthenticatorTokenProvider.cs +++ b/src/Core/Auth/Identity/AuthenticatorTokenProvider.cs @@ -2,6 +2,7 @@ using Bit.Core.Entities; using Bit.Core.Services; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; using OtpNet; @@ -9,11 +10,23 @@ namespace Bit.Core.Auth.Identity; public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider { - private readonly IServiceProvider _serviceProvider; + private const string CacheKeyFormat = "Authenticator_TOTP_{0}_{1}"; - public AuthenticatorTokenProvider(IServiceProvider serviceProvider) + private readonly IServiceProvider _serviceProvider; + private readonly IDistributedCache _distributedCache; + private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions; + + public AuthenticatorTokenProvider( + IServiceProvider serviceProvider, + [FromKeyedServices("persistent")] + IDistributedCache distributedCache) { _serviceProvider = serviceProvider; + _distributedCache = distributedCache; + _distributedCacheEntryOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2) + }; } public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) @@ -32,14 +45,24 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider return Task.FromResult(null); } - public Task ValidateAsync(string purpose, string token, UserManager manager, User user) + public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) { + var cacheKey = string.Format(CacheKeyFormat, user.Id, token); + var cachedValue = await _distributedCache.GetAsync(cacheKey); + if (cachedValue != null) + { + return false; + } + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); var otp = new Totp(Base32Encoding.ToBytes((string)provider.MetaData["Key"])); + var valid = otp.VerifyTotp(token, out _, new VerificationWindow(1, 1)); - long timeStepMatched; - var valid = otp.VerifyTotp(token, out timeStepMatched, new VerificationWindow(1, 1)); + if (valid) + { + await _distributedCache.SetAsync(cacheKey, [1], _distributedCacheEntryOptions); + } - return Task.FromResult(valid); + return valid; } } diff --git a/src/Core/Auth/Identity/EmailTokenProvider.cs b/src/Core/Auth/Identity/EmailTokenProvider.cs index e8961c0638..6ef473c4b3 100644 --- a/src/Core/Auth/Identity/EmailTokenProvider.cs +++ b/src/Core/Auth/Identity/EmailTokenProvider.cs @@ -3,17 +3,30 @@ using Bit.Core.Auth.Models; using Bit.Core.Entities; using Bit.Core.Services; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Auth.Identity; public class EmailTokenProvider : IUserTwoFactorTokenProvider { - private readonly IServiceProvider _serviceProvider; + private const string CacheKeyFormat = "Email_TOTP_{0}_{1}"; - public EmailTokenProvider(IServiceProvider serviceProvider) + private readonly IServiceProvider _serviceProvider; + private readonly IDistributedCache _distributedCache; + private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions; + + public EmailTokenProvider( + IServiceProvider serviceProvider, + [FromKeyedServices("persistent")] + IDistributedCache distributedCache) { _serviceProvider = serviceProvider; + _distributedCache = distributedCache; + _distributedCacheEntryOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(20) + }; } public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) @@ -39,9 +52,22 @@ public class EmailTokenProvider : IUserTwoFactorTokenProvider return Task.FromResult(RedactEmail((string)provider.MetaData["Email"])); } - public Task ValidateAsync(string purpose, string token, UserManager manager, User user) + public async Task ValidateAsync(string purpose, string token, UserManager manager, User user) { - return _serviceProvider.GetRequiredService().VerifyTwoFactorEmailAsync(user, token); + var cacheKey = string.Format(CacheKeyFormat, user.Id, token); + var cachedValue = await _distributedCache.GetAsync(cacheKey); + if (cachedValue != null) + { + return false; + } + + var valid = await _serviceProvider.GetRequiredService().VerifyTwoFactorEmailAsync(user, token); + if (valid) + { + await _distributedCache.SetAsync(cacheKey, [1], _distributedCacheEntryOptions); + } + + return valid; } private bool HasProperMetaData(TwoFactorProvider provider) diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index f6d8b3b23a..406fd5bf7e 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -27,7 +27,6 @@ using Bit.Core.Tokens; using Bit.Core.Utilities; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Caching.Distributed; namespace Bit.Identity.IdentityServer; @@ -47,8 +46,6 @@ public abstract class BaseRequestValidator where T : class private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IDataProtectorTokenFactory _tokenDataFactory; - private readonly IDistributedCache _distributedCache; - private readonly DistributedCacheEntryOptions _cacheEntryOptions; protected ICurrentContext CurrentContext { get; } protected IPolicyService PolicyService { get; } @@ -77,7 +74,6 @@ public abstract class BaseRequestValidator where T : class IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, - IDistributedCache distributedCache, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) { _userManager = userManager; @@ -99,14 +95,6 @@ public abstract class BaseRequestValidator where T : class _tokenDataFactory = tokenDataFactory; FeatureService = featureService; SsoConfigRepository = ssoConfigRepository; - _distributedCache = distributedCache; - _cacheEntryOptions = new DistributedCacheEntryOptions - { - // This sets the time an item is cached to 17 minutes. This value is hard coded - // to 17 because to it covers all time-out windows for both Authenticators and - // Email TOTP. - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(17) - }; UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder; } @@ -153,11 +141,7 @@ public abstract class BaseRequestValidator where T : class var verified = await VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); - - var cacheKey = "TOTP_" + user.Email + "_" + twoFactorToken; - - var isOtpCached = Core.Utilities.DistributedCacheExtensions.TryGetValue(_distributedCache, cacheKey, out string _); - if (!verified || isBot || isOtpCached) + if (!verified || isBot) { if (twoFactorProviderType != TwoFactorProviderType.Remember) { @@ -170,11 +154,6 @@ public abstract class BaseRequestValidator where T : class } return; } - // 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); - } } else { diff --git a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs index fbd522c814..b69f4dacb6 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -15,7 +15,6 @@ using Duende.IdentityServer.Extensions; using Duende.IdentityServer.Validation; using IdentityModel; using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Caching.Distributed; #nullable enable @@ -46,14 +45,12 @@ public class CustomTokenRequestValidator : BaseRequestValidator tokenDataFactory, IFeatureService featureService, - [FromKeyedServices("persistent")] - IDistributedCache distributedCache, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : base(userManager, deviceRepository, deviceService, userService, eventService, organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, - distributedCache, userDecryptionOptionsBuilder) + userDecryptionOptionsBuilder) { _userManager = userManager; } diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs index 4ca31e8bf3..30a5d821da 100644 --- a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -15,7 +15,6 @@ using Bit.Core.Utilities; using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Caching.Distributed; namespace Bit.Identity.IdentityServer; @@ -49,13 +48,11 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator tokenDataFactory, IFeatureService featureService, ISsoConfigRepository ssoConfigRepository, - [FromKeyedServices("persistent")] - IDistributedCache distributedCache, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) : base(userManager, deviceRepository, deviceService, userService, eventService, organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, - tokenDataFactory, featureService, ssoConfigRepository, distributedCache, userDecryptionOptionsBuilder) + tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) { _userManager = userManager; _userService = userService; diff --git a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs index c9a4ee4ade..fed631eb36 100644 --- a/src/Identity/IdentityServer/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/WebAuthnGrantValidator.cs @@ -18,7 +18,6 @@ using Duende.IdentityServer.Models; using Duende.IdentityServer.Validation; using Fido2NetLib; using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.Caching.Distributed; namespace Bit.Identity.IdentityServer; @@ -50,15 +49,13 @@ public class WebAuthnGrantValidator : BaseRequestValidator tokenDataFactory, IDataProtectorTokenFactory assertionOptionsDataProtector, IFeatureService featureService, - [FromKeyedServices("persistent")] - IDistributedCache distributedCache, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand ) : base(userManager, deviceRepository, deviceService, userService, eventService, organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, - userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, distributedCache, userDecryptionOptionsBuilder) + userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder) { _assertionOptionsDataProtector = assertionOptionsDataProtector; _assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;