mirror of
https://github.com/bitwarden/server.git
synced 2025-07-18 16:11:28 -05:00
feat(OTP): [PM-18612] Change email OTP to six digits
* Change email OTP to 6 digits * Added comment on base class * Added tests * Renamed tests. * Fixed tests * Renamed file to match class
This commit is contained in:
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Generates and validates tokens for email OTPs.
|
||||
/// </summary>
|
||||
public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
private const string CacheKeyFormat = "EmailToken_{0}_{1}_{2}";
|
||||
@ -16,16 +20,25 @@ public class EmailTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
|
||||
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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
@ -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";
|
||||
|
@ -7,7 +7,7 @@ using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Identity;
|
||||
|
||||
public class AuthenticationTokenProviderTests : BaseTokenProviderTests<AuthenticatorTokenProvider>
|
||||
public class AuthenticationTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests<AuthenticatorTokenProvider>
|
||||
{
|
||||
public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Authenticator;
|
||||
|
@ -14,7 +14,7 @@ using Xunit;
|
||||
namespace Bit.Core.Test.Auth.Identity;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public abstract class BaseTokenProviderTests<T>
|
||||
public abstract class BaseTwoFactorTokenProviderTests<T>
|
||||
where T : IUserTwoFactorTokenProvider<User>
|
||||
{
|
||||
public abstract TwoFactorProviderType TwoFactorProviderType { get; }
|
@ -12,7 +12,7 @@ using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Test.Auth.Identity;
|
||||
|
||||
public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests<DuoUniversalTokenProvider>
|
||||
public class DuoUniversalTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests<DuoUniversalTokenProvider>
|
||||
{
|
||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService = Substitute.For<IDuoUniversalTokenService>();
|
||||
public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Duo;
|
||||
|
68
test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs
Normal file
68
test/Core.Test/Auth/Identity/EmailTokenProviderTests.cs
Normal file
@ -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<IDistributedCache>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GenerateAsync_GeneratesSixDigitToken_WhenFeatureFlagIsEnabled(User user)
|
||||
{
|
||||
// Arrange
|
||||
var purpose = "test-purpose";
|
||||
var featureService = Substitute.For<IFeatureService>();
|
||||
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<IFeatureService>();
|
||||
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<User> SubstituteUserManager()
|
||||
{
|
||||
return new UserManager<User>(Substitute.For<IUserStore<User>>(),
|
||||
Substitute.For<IOptions<IdentityOptions>>(),
|
||||
Substitute.For<IPasswordHasher<User>>(),
|
||||
Enumerable.Empty<IUserValidator<User>>(),
|
||||
Enumerable.Empty<IPasswordValidator<User>>(),
|
||||
Substitute.For<ILookupNormalizer>(),
|
||||
Substitute.For<IdentityErrorDescriber>(),
|
||||
Substitute.For<IServiceProvider>(),
|
||||
Substitute.For<ILogger<UserManager<User>>>());
|
||||
}
|
||||
}
|
@ -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<EmailTwoFactorTokenProvider>
|
||||
public class EmailTwoFactorTokenProviderTests : BaseTwoFactorTokenProviderTests<EmailTwoFactorTokenProvider>
|
||||
{
|
||||
public override TwoFactorProviderType TwoFactorProviderType => TwoFactorProviderType.Email;
|
||||
|
||||
@ -42,4 +44,48 @@ public class EmailTwoFactorTokenProviderTests : BaseTokenProviderTests<EmailTwoF
|
||||
{
|
||||
await base.RunCanGenerateTwoFactorTokenAsync(metaData, expectedResponse, user, sutProvider);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GenerateAsync_ShouldReturnSixDigitToken_WithFeatureFlagEnabled(
|
||||
User user, SutProvider<EmailTwoFactorTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.TwoFactorProviders = GetTwoFactorEmailProvidersJson();
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.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<EmailTwoFactorTokenProvider> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
user.TwoFactorProviders = GetTwoFactorEmailProvidersJson();
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.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\"}}}";
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user