diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs index be94124c03..70aba8ef75 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTokenProvider.cs @@ -1,5 +1,6 @@ using System.Text; using Bit.Core.Entities; +using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; @@ -7,6 +8,9 @@ using Microsoft.Extensions.DependencyInjection; namespace Bit.Core.Auth.Identity.TokenProviders; +/// +/// Generates and validates tokens for email OTPs. +/// public class EmailTokenProvider : IUserTwoFactorTokenProvider { private const string CacheKeyFormat = "EmailToken_{0}_{1}_{2}"; @@ -16,16 +20,25 @@ public class EmailTokenProvider : IUserTwoFactorTokenProvider public EmailTokenProvider( [FromKeyedServices("persistent")] - IDistributedCache distributedCache) + IDistributedCache distributedCache, + IFeatureService featureService) { _distributedCache = distributedCache; _distributedCacheEntryOptions = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; + if (featureService.IsEnabled(FeatureFlagKeys.Otp6Digits)) + { + TokenLength = 6; + } + else + { + TokenLength = 8; + } } - public int TokenLength { get; protected set; } = 8; + public int TokenLength { get; protected set; } public bool TokenAlpha { get; protected set; } = false; public bool TokenNumeric { get; protected set; } = true; diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index c4b4c1d2ca..2d72781569 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -4,19 +4,27 @@ using Bit.Core.Auth.Enums; 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.TokenProviders; +/// +/// Generates tokens for email two-factor authentication. +/// It inherits from the EmailTokenProvider class, which manages the persistence and validation of tokens, +/// and adds additional validation to ensure that 2FA is enabled for the user. +/// public class EmailTwoFactorTokenProvider : EmailTokenProvider { public EmailTwoFactorTokenProvider( [FromKeyedServices("persistent")] - IDistributedCache distributedCache) : - base(distributedCache) + IDistributedCache distributedCache, + IFeatureService featureService) : + base(distributedCache, featureService) { + // This can be removed when the pm-18612-otp-6-digits feature flag is removed because the base implementation will match. TokenAlpha = false; TokenNumeric = true; TokenLength = 6; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index bb7758c12b..2ef9a20ae3 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -124,6 +124,7 @@ public static class FeatureFlagKeys public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; + public const string Otp6Digits = "pm-18612-otp-6-digits"; /* Autofill Team */ public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; diff --git a/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs b/test/Core.Test/Auth/Identity/AuthenticationTwoFactorTokenProviderTests.cs similarity index 91% rename from test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs rename to test/Core.Test/Auth/Identity/AuthenticationTwoFactorTokenProviderTests.cs index c9646e627c..b3f7f4f1c4 100644 --- a/test/Core.Test/Auth/Identity/AuthenticationTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/AuthenticationTwoFactorTokenProviderTests.cs @@ -7,7 +7,7 @@ using Xunit; namespace Bit.Core.Test.Auth.Identity; -public class AuthenticationTokenProviderTests : BaseTokenProviderTests +public class AuthenticationTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests { public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Authenticator; diff --git a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs b/test/Core.Test/Auth/Identity/BaseTwoFactorTokenProviderTests.cs similarity index 98% rename from test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs rename to test/Core.Test/Auth/Identity/BaseTwoFactorTokenProviderTests.cs index ff09e1f141..04cbe026ba 100644 --- a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/BaseTwoFactorTokenProviderTests.cs @@ -14,7 +14,7 @@ using Xunit; namespace Bit.Core.Test.Auth.Identity; [SutProviderCustomize] -public abstract class BaseTokenProviderTests +public abstract class BaseTwoFactorTokenProviderTests where T : IUserTwoFactorTokenProvider { public abstract TwoFactorProviderType TwoFactorProviderType { get; } diff --git a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs index 5715403974..99d2dd3938 100644 --- a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs @@ -12,7 +12,7 @@ using Duo = DuoUniversal; namespace Bit.Core.Test.Auth.Identity; -public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests +public class DuoUniversalTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests { private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For(); public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Duo; diff --git a/test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs b/test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs new file mode 100644 index 0000000000..f29b11b935 --- /dev/null +++ b/test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs @@ -0,0 +1,68 @@ +using Bit.Core; +using Bit.Core.Auth.Identity.TokenProviders; +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NSubstitute; +using Xunit; + +[SutProviderCustomize] +public class EmailTokenProviderTests +{ + private readonly IDistributedCache _cache; + + public EmailTokenProviderTests() + { + _cache = Substitute.For(); + } + + [Theory] + [BitAutoData] + public async Task GenerateAsync_GeneratesSixDigitToken_WhenFeatureFlagIsEnabled(User user) + { + // Arrange + var purpose = "test-purpose"; + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.Otp6Digits).Returns(true); + var tokenProvider = new EmailTokenProvider(_cache, featureService); + + // Act + var code = await tokenProvider.GenerateAsync(purpose, SubstituteUserManager(), user); + + // Assert + Assert.Equal(6, code.Length); + } + + [Theory] + [BitAutoData] + public async Task GenerateAsync_GeneratesEightDigitToken_WhenFeatureFlagIsDisabled(User user) + { + // Arrange + var purpose = "test-purpose"; + var featureService = Substitute.For(); + featureService.IsEnabled(FeatureFlagKeys.Otp6Digits).Returns(false); + var tokenProvider = new EmailTokenProvider(_cache, featureService); + // Act + var code = await tokenProvider.GenerateAsync(purpose, SubstituteUserManager(), user); + + // Assert + Assert.Equal(8, code.Length); + } + + protected static UserManager SubstituteUserManager() + { + return new UserManager(Substitute.For>(), + Substitute.For>(), + Substitute.For>(), + Enumerable.Empty>(), + Enumerable.Empty>(), + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For>>()); + } +} diff --git a/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs index 46bfba549e..aa801dce58 100644 --- a/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/EmailTwoFactorTokenProviderTests.cs @@ -1,13 +1,15 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Entities; +using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; using Xunit; namespace Bit.Core.Test.Auth.Identity; -public class EmailTwoFactorTokenProviderTests : BaseTokenProviderTests +public class EmailTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests { public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Email; @@ -42,4 +44,48 @@ public class EmailTwoFactorTokenProviderTests : BaseTokenProviderTests sutProvider) + { + // Arrange + user.TwoFactorProviders = GetTwoFactorEmailProvidersJson(); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.Otp6Digits) + .Returns(true); + + // Act + var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user); + + // Assert + Assert.NotNull(token); + Assert.Equal(6, token.Length); + } + + [Theory] + [BitAutoData] + public async Task GenerateAsync_ShouldReturnSixDigitToken_WithFeatureFlagDisabled( + User user, SutProvider sutProvider) + { + // Arrange + user.TwoFactorProviders = GetTwoFactorEmailProvidersJson(); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.Otp6Digits) + .Returns(false); + + // Act + var token = await sutProvider.Sut.GenerateAsync("purpose", SubstituteUserManager(), user); + + // Assert + Assert.NotNull(token); + Assert.Equal(6, token.Length); + } + + private string GetTwoFactorEmailProvidersJson() + { + return + "{\"1\":{\"Enabled\":true,\"MetaData\":{\"Email\":\"test@email.com\"}}}"; + } }