From 917c657439ff5a58dbfc4c5ae1e683b20e952a93 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Sat, 9 Sep 2023 14:35:08 -0700 Subject: [PATCH] PM-2128 Enforce one time use of TOTP (#3152) * enforcing one time MFA token use * Updated cache TTL * renamed the cache * removed IP limit, added comment, updated cache Key * fixed build errors --- .../IdentityServer/BaseRequestValidator.cs | 24 ++++++++++++++++++- .../CustomTokenRequestValidator.cs | 7 ++++-- .../ResourceOwnerPasswordValidator.cs | 6 +++-- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/Identity/IdentityServer/BaseRequestValidator.cs b/src/Identity/IdentityServer/BaseRequestValidator.cs index e0cafbfa13..3e35cb5335 100644 --- a/src/Identity/IdentityServer/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/BaseRequestValidator.cs @@ -26,6 +26,7 @@ using Bit.Core.Utilities; using Bit.Identity.Utilities; using IdentityServer4.Validation; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Distributed; namespace Bit.Identity.IdentityServer; @@ -45,6 +46,8 @@ 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; } @@ -69,7 +72,8 @@ public abstract class BaseRequestValidator where T : class IPolicyService policyService, IDataProtectorTokenFactory tokenDataFactory, IFeatureService featureService, - ISsoConfigRepository ssoConfigRepository) + ISsoConfigRepository ssoConfigRepository, + IDistributedCache distributedCache) { _userManager = userManager; _deviceRepository = deviceRepository; @@ -89,6 +93,14 @@ 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 15 minutes. This value is hard coded + // to 15 because to it covers all time-out windows for both Authenticators and + // Email TOTP. + AbsoluteExpirationRelativeToNow = new TimeSpan(0, 15, 0) + }; } protected async Task ValidateAsync(T context, ValidatedTokenRequest request, @@ -135,6 +147,15 @@ public abstract class BaseRequestValidator where T : class var verified = await VerifyTwoFactor(user, twoFactorOrganization, twoFactorProviderType, twoFactorToken); + var cacheKey = "TOTP_" + user.Email; + + var isOtpCached = Core.Utilities.DistributedCacheExtensions.TryGetValue(_distributedCache, cacheKey, out string _); + if (isOtpCached) + { + await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); + return; + } + if ((!verified || isBot) && twoFactorProviderType != TwoFactorProviderType.Remember) { await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice); @@ -148,6 +169,7 @@ public abstract class BaseRequestValidator where T : class await BuildTwoFactorResultAsync(user, twoFactorOrganization, context); return; } + 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 e6e39bade1..a15a738372 100644 --- a/src/Identity/IdentityServer/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/CustomTokenRequestValidator.cs @@ -14,6 +14,7 @@ using IdentityModel; using IdentityServer4.Extensions; using IdentityServer4.Validation; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Distributed; #nullable enable @@ -42,11 +43,13 @@ public class CustomTokenRequestValidator : BaseRequestValidator tokenDataFactory, - IFeatureService featureService) + IFeatureService featureService, + IDistributedCache distributedCache) : base(userManager, deviceRepository, deviceService, userService, eventService, organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, - userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository) + userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, + distributedCache) { _userManager = userManager; } diff --git a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs index df17a474a3..8b8b0be52d 100644 --- a/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -13,6 +13,7 @@ using Bit.Core.Utilities; using IdentityServer4.Models; using IdentityServer4.Validation; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Distributed; namespace Bit.Identity.IdentityServer; @@ -44,11 +45,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator tokenDataFactory, IFeatureService featureService, - ISsoConfigRepository ssoConfigRepository) + ISsoConfigRepository ssoConfigRepository, + IDistributedCache distributedCache) : base(userManager, deviceRepository, deviceService, userService, eventService, organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository, applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService, - tokenDataFactory, featureService, ssoConfigRepository) + tokenDataFactory, featureService, ssoConfigRepository, distributedCache) { _userManager = userManager; _userService = userService;