1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-26 12:55:17 +01:00

[PM-6208] Move TOTP cache validation logic to providers (#3779)

* move totp cache validation logic to providers

* remove unused usings

* reduce TTL
This commit is contained in:
Kyle Spearrin 2024-02-09 15:44:31 -05:00 committed by GitHub
parent a19ae0159f
commit 17118bc74f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 63 additions and 44 deletions

View File

@ -2,6 +2,7 @@
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OtpNet; using OtpNet;
@ -9,11 +10,23 @@ namespace Bit.Core.Auth.Identity;
public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User> public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
{ {
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; _serviceProvider = serviceProvider;
_distributedCache = distributedCache;
_distributedCacheEntryOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2)
};
} }
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user) public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
@ -32,14 +45,24 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider<User>
return Task.FromResult<string>(null); return Task.FromResult<string>(null);
} }
public Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user) public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> 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 provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);
var otp = new Totp(Base32Encoding.ToBytes((string)provider.MetaData["Key"])); var otp = new Totp(Base32Encoding.ToBytes((string)provider.MetaData["Key"]));
var valid = otp.VerifyTotp(token, out _, new VerificationWindow(1, 1));
long timeStepMatched; if (valid)
var valid = otp.VerifyTotp(token, out timeStepMatched, new VerificationWindow(1, 1)); {
await _distributedCache.SetAsync(cacheKey, [1], _distributedCacheEntryOptions);
}
return Task.FromResult(valid); return valid;
} }
} }

View File

@ -3,17 +3,30 @@ using Bit.Core.Auth.Models;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Services; using Bit.Core.Services;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Auth.Identity; namespace Bit.Core.Auth.Identity;
public class EmailTokenProvider : IUserTwoFactorTokenProvider<User> public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
{ {
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; _serviceProvider = serviceProvider;
_distributedCache = distributedCache;
_distributedCacheEntryOptions = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(20)
};
} }
public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user) public async Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<User> manager, User user)
@ -39,9 +52,22 @@ public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
return Task.FromResult(RedactEmail((string)provider.MetaData["Email"])); return Task.FromResult(RedactEmail((string)provider.MetaData["Email"]));
} }
public Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user) public async Task<bool> ValidateAsync(string purpose, string token, UserManager<User> manager, User user)
{ {
return _serviceProvider.GetRequiredService<IUserService>().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<IUserService>().VerifyTwoFactorEmailAsync(user, token);
if (valid)
{
await _distributedCache.SetAsync(cacheKey, [1], _distributedCacheEntryOptions);
}
return valid;
} }
private bool HasProperMetaData(TwoFactorProvider provider) private bool HasProperMetaData(TwoFactorProvider provider)

View File

@ -27,7 +27,6 @@ using Bit.Core.Tokens;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer;
@ -47,8 +46,6 @@ public abstract class BaseRequestValidator<T> where T : class
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory; private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
private readonly IDistributedCache _distributedCache;
private readonly DistributedCacheEntryOptions _cacheEntryOptions;
protected ICurrentContext CurrentContext { get; } protected ICurrentContext CurrentContext { get; }
protected IPolicyService PolicyService { get; } protected IPolicyService PolicyService { get; }
@ -77,7 +74,6 @@ public abstract class BaseRequestValidator<T> where T : class
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory, IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
IDistributedCache distributedCache,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
{ {
_userManager = userManager; _userManager = userManager;
@ -99,14 +95,6 @@ public abstract class BaseRequestValidator<T> where T : class
_tokenDataFactory = tokenDataFactory; _tokenDataFactory = tokenDataFactory;
FeatureService = featureService; FeatureService = featureService;
SsoConfigRepository = ssoConfigRepository; 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; UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
} }
@ -153,11 +141,7 @@ public abstract class BaseRequestValidator<T> where T : class
var verified = await VerifyTwoFactor(user, twoFactorOrganization, var verified = await VerifyTwoFactor(user, twoFactorOrganization,
twoFactorProviderType, twoFactorToken); twoFactorProviderType, twoFactorToken);
if (!verified || isBot)
var cacheKey = "TOTP_" + user.Email + "_" + twoFactorToken;
var isOtpCached = Core.Utilities.DistributedCacheExtensions.TryGetValue(_distributedCache, cacheKey, out string _);
if (!verified || isBot || isOtpCached)
{ {
if (twoFactorProviderType != TwoFactorProviderType.Remember) if (twoFactorProviderType != TwoFactorProviderType.Remember)
{ {
@ -170,11 +154,6 @@ public abstract class BaseRequestValidator<T> where T : class
} }
return; 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 else
{ {

View File

@ -15,7 +15,6 @@ using Duende.IdentityServer.Extensions;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using IdentityModel; using IdentityModel;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
#nullable enable #nullable enable
@ -46,14 +45,12 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IPolicyService policyService, IPolicyService policyService,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory, IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
[FromKeyedServices("persistent")]
IDistributedCache distributedCache,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
: base(userManager, deviceRepository, deviceService, userService, eventService, : base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, applicationCacheService, mailService, logger, currentContext, globalSettings,
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository,
distributedCache, userDecryptionOptionsBuilder) userDecryptionOptionsBuilder)
{ {
_userManager = userManager; _userManager = userManager;
} }

View File

@ -15,7 +15,6 @@ using Bit.Core.Utilities;
using Duende.IdentityServer.Models; using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer;
@ -49,13 +48,11 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory, IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IFeatureService featureService, IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository, ISsoConfigRepository ssoConfigRepository,
[FromKeyedServices("persistent")]
IDistributedCache distributedCache,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
: base(userManager, deviceRepository, deviceService, userService, eventService, : base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService,
tokenDataFactory, featureService, ssoConfigRepository, distributedCache, userDecryptionOptionsBuilder) tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
{ {
_userManager = userManager; _userManager = userManager;
_userService = userService; _userService = userService;

View File

@ -18,7 +18,6 @@ using Duende.IdentityServer.Models;
using Duende.IdentityServer.Validation; using Duende.IdentityServer.Validation;
using Fido2NetLib; using Fido2NetLib;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Caching.Distributed;
namespace Bit.Identity.IdentityServer; namespace Bit.Identity.IdentityServer;
@ -50,15 +49,13 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory, IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector, IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
IFeatureService featureService, IFeatureService featureService,
[FromKeyedServices("persistent")]
IDistributedCache distributedCache,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder, IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
) )
: base(userManager, deviceRepository, deviceService, userService, eventService, : base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository, organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, applicationCacheService, mailService, logger, currentContext, globalSettings,
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, distributedCache, userDecryptionOptionsBuilder) userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
{ {
_assertionOptionsDataProtector = assertionOptionsDataProtector; _assertionOptionsDataProtector = assertionOptionsDataProtector;
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand; _assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;