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\"}}}";
+ }
}