mirror of
https://github.com/bitwarden/server.git
synced 2024-11-22 12:15:36 +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:
parent
a19ae0159f
commit
17118bc74f
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user