1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-19 00:21:35 -05:00

fix(2fa): [PM-22323] Do not show 2FA warning for 2FA setup and login emails

* Added configuration to not display 2FA setup instruction

* Refactored to new service.

* Linting.

* Dependency injection

* Changed to scoped to have access to ICurrentContext.

* Inverted logic for EmailTotpAction

* Fixed tests.

* Fixed tests.

* More tests.

* Fixed tests.

* Linting.

* Added tests at controller level.

* Linting

* Fixed error in test.

* Review updates.

* Accidentally deleted imports.
This commit is contained in:
Todd Martin
2025-07-07 10:56:59 -04:00
committed by GitHub
parent 240968ef4c
commit 79ad1dbda0
18 changed files with 491 additions and 288 deletions

View File

@ -0,0 +1,254 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Core.Auth.Enums;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Auth.Services;
[SutProviderCustomize]
public class TwoFactorEmailServiceTests
{
[Theory, BitAutoData]
public async Task SendTwoFactorEmailAsync_Success(SutProvider<TwoFactorEmailService> sutProvider, User user)
{
var email = user.Email.ToLowerInvariant();
var token = "thisisatokentocompare";
var IpAddress = "1.1.1.1";
var deviceType = DeviceType.Android;
var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = deviceType;
context.IpAddress = IpAddress;
var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();
userTwoFactorTokenProvider
.CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(true));
userTwoFactorTokenProvider
.GenerateAsync("TwoFactor", Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(token));
var userManager = sutProvider.GetDependency<UserManager<User>>();
userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider);
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = new Dictionary<string, object> { ["Email"] = email },
Enabled = true
}
});
await sutProvider.Sut.SendTwoFactorEmailAsync(user);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType.ToString(),
TwoFactorEmailPurpose.Login);
}
[Theory, BitAutoData]
public async Task SendTwoFactorSetupEmailAsync_Success(SutProvider<TwoFactorEmailService> sutProvider, User user)
{
var email = user.Email.ToLowerInvariant();
var token = "thisisatokentocompare";
var IpAddress = "1.1.1.1";
var deviceType = DeviceType.Android;
var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = deviceType;
context.IpAddress = IpAddress;
var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();
userTwoFactorTokenProvider
.CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(true));
userTwoFactorTokenProvider
.GenerateAsync("TwoFactor", Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(token));
var userManager = sutProvider.GetDependency<UserManager<User>>();
userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider);
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = new Dictionary<string, object> { ["Email"] = email },
Enabled = true
}
});
await sutProvider.Sut.SendTwoFactorSetupEmailAsync(user);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType.ToString(),
TwoFactorEmailPurpose.Setup);
}
[Theory, BitAutoData]
public async Task SendNewDeviceVerificationEmailAsync_Success(SutProvider<TwoFactorEmailService> sutProvider, User user)
{
var email = user.Email.ToLowerInvariant();
var token = "thisisatokentocompare";
var IpAddress = "1.1.1.1";
var deviceType = DeviceType.Android;
var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = deviceType;
context.IpAddress = IpAddress;
var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();
userTwoFactorTokenProvider
.CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(true));
userTwoFactorTokenProvider
.GenerateAsync("otp:" + user.Email, Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(token));
var userManager = sutProvider.GetDependency<UserManager<User>>();
userManager.RegisterTokenProvider(TokenOptions.DefaultEmailProvider, userTwoFactorTokenProvider);
await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType.ToString(),
TwoFactorEmailPurpose.NewDeviceVerification);
}
[Theory, BitAutoData]
public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderOnUser(SutProvider<TwoFactorEmailService> sutProvider, User user)
{
user.TwoFactorProviders = null;
await Assert.ThrowsAsync<ArgumentNullException>("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));
}
[Theory, BitAutoData]
public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderMetadataOnUser(SutProvider<TwoFactorEmailService> sutProvider, User user)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = null,
Enabled = true
}
});
await Assert.ThrowsAsync<ArgumentNullException>("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));
}
[Theory, BitAutoData]
public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderEmailMetadataOnUser(SutProvider<TwoFactorEmailService> sutProvider, User user)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = new Dictionary<string, object> { ["qweqwe"] = user.Email.ToLowerInvariant() },
Enabled = true
}
});
await Assert.ThrowsAsync<ArgumentNullException>("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));
}
[Theory, BitAutoData]
public async Task SendNewDeviceVerificationEmailAsync_ExceptionBecauseUserNull(SutProvider<TwoFactorEmailService> sutProvider)
{
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.SendNewDeviceVerificationEmailAsync(null));
}
[Theory]
[BitAutoData(DeviceType.UnknownBrowser, "Unknown Browser")]
[BitAutoData(DeviceType.Android, "Android")]
public async Task SendTwoFactorEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName,
SutProvider<TwoFactorEmailService> sutProvider,
User user)
{
var email = user.Email.ToLowerInvariant();
var token = "thisisatokentocompare";
var IpAddress = "1.1.1.1";
var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = deviceType;
context.IpAddress = IpAddress;
var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();
userTwoFactorTokenProvider
.CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(true));
userTwoFactorTokenProvider
.GenerateAsync("TwoFactor", Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(token));
var userManager = sutProvider.GetDependency<UserManager<User>>();
userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider);
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = new Dictionary<string, object> { ["Email"] = email },
Enabled = true
}
});
await sutProvider.Sut.SendTwoFactorEmailAsync(user);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendTwoFactorEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), deviceTypeName, TwoFactorEmailPurpose.Login);
}
[Theory, BitAutoData]
public async Task SendTwoFactorEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(SutProvider<TwoFactorEmailService> sutProvider, User user)
{
var email = user.Email.ToLowerInvariant();
var token = "thisisatokentocompare";
var IpAddress = "1.1.1.1";
var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();
userTwoFactorTokenProvider
.CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(true));
userTwoFactorTokenProvider
.GenerateAsync("TwoFactor", Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(token));
var context = Substitute.For<ICurrentContext>();
context.DeviceType = null;
context.IpAddress = IpAddress;
var userManager = sutProvider.GetDependency<UserManager<User>>();
userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider);
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = new Dictionary<string, object> { ["Email"] = email },
Enabled = true
}
});
await sutProvider.Sut.SendTwoFactorEmailAsync(user);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendTwoFactorEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), "Unknown Browser", Arg.Any<TwoFactorEmailPurpose>());
}
}

View File

@ -11,7 +11,6 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -84,125 +83,6 @@ public class UserServiceTests
Assert.Equal(1, versionProp.GetInt32());
}
[Theory, BitAutoData]
public async Task SendTwoFactorEmailAsync_Success(SutProvider<UserService> sutProvider, User user)
{
var email = user.Email.ToLowerInvariant();
var token = "thisisatokentocompare";
var authentication = true;
var IpAddress = "1.1.1.1";
var deviceType = "Android";
var userTwoFactorTokenProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();
userTwoFactorTokenProvider
.CanGenerateTwoFactorTokenAsync(Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(true));
userTwoFactorTokenProvider
.GenerateAsync("TwoFactor", Arg.Any<UserManager<User>>(), user)
.Returns(Task.FromResult(token));
var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = DeviceType.Android;
context.IpAddress = IpAddress;
sutProvider.Sut.RegisterTokenProvider("Custom_Email", userTwoFactorTokenProvider);
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = new Dictionary<string, object> { ["Email"] = email },
Enabled = true
}
});
await sutProvider.Sut.SendTwoFactorEmailAsync(user);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType, authentication);
}
[Theory, BitAutoData]
public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderOnUser(SutProvider<UserService> sutProvider, User user)
{
user.TwoFactorProviders = null;
await Assert.ThrowsAsync<ArgumentNullException>("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));
}
[Theory, BitAutoData]
public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderMetadataOnUser(SutProvider<UserService> sutProvider, User user)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = null,
Enabled = true
}
});
await Assert.ThrowsAsync<ArgumentNullException>("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));
}
[Theory, BitAutoData]
public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderEmailMetadataOnUser(SutProvider<UserService> sutProvider, User user)
{
user.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>
{
[TwoFactorProviderType.Email] = new TwoFactorProvider
{
MetaData = new Dictionary<string, object> { ["qweqwe"] = user.Email.ToLowerInvariant() },
Enabled = true
}
});
await Assert.ThrowsAsync<ArgumentNullException>("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user));
}
[Theory, BitAutoData]
public async Task SendNewDeviceVerificationEmailAsync_ExceptionBecauseUserNull(SutProvider<UserService> sutProvider)
{
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.SendNewDeviceVerificationEmailAsync(null));
}
[Theory]
[BitAutoData(DeviceType.UnknownBrowser, "Unknown Browser")]
[BitAutoData(DeviceType.Android, "Android")]
public async Task SendNewDeviceVerificationEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName,
User user)
{
var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);
var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = deviceType;
context.IpAddress = "1.1.1.1";
await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendTwoFactorEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), deviceTypeName, Arg.Any<bool>());
}
[Theory, BitAutoData]
public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(User user)
{
var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);
var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = null;
context.IpAddress = "1.1.1.1";
await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendTwoFactorEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), "Unknown Browser", Arg.Any<bool>());
}
[Theory, BitAutoData]
public async Task HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider<UserService> sutProvider, User user)
{
@ -577,78 +457,6 @@ public class UserServiceTests
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled(
SutProvider<UserService> sutProvider, string email, string secret)
{
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.Returns(null as User);
await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendTwoFactorEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_SecretNotValid_SendTwoFactorEmailAsyncNotCalled(
SutProvider<UserService> sutProvider, string email, string secret)
{
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(email)
.Returns(null as User);
await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendTwoFactorEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_SendsToken_Success(User user)
{
// Arrange
var testPassword = "test_password";
SetupUserAndDevice(user, true);
var sutProvider = new SutProvider<UserService>()
.CreateWithUserServiceCustomizations(user);
// Setup the fake password verification
sutProvider
.GetDependency<IUserPasswordStore<User>>()
.GetPasswordHashAsync(user, Arg.Any<CancellationToken>())
.Returns((ci) =>
{
return Task.FromResult("hashed_test_password");
});
sutProvider.GetDependency<IPasswordHasher<User>>()
.VerifyHashedPassword(user, "hashed_test_password", testPassword)
.Returns((ci) =>
{
return PasswordVerificationResult.Success;
});
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(user.Email)
.Returns(user);
var context = sutProvider.GetDependency<ICurrentContext>();
context.DeviceType = DeviceType.Android;
context.IpAddress = "1.1.1.1";
await sutProvider.Sut.ResendNewDeviceVerificationEmail(user.Email, testPassword);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendTwoFactorEmailAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
}
[Theory]
[BitAutoData("")]
[BitAutoData("null")]