1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-14 22:27:32 -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

@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
@ -34,6 +35,8 @@ public class AccountsController : Controller
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly ITwoFactorEmailService _twoFactorEmailService;
public AccountsController(
IOrganizationService organizationService,
@ -44,7 +47,8 @@ public class AccountsController : Controller
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService
IFeatureService featureService,
ITwoFactorEmailService twoFactorEmailService
)
{
_organizationService = organizationService;
@ -56,6 +60,8 @@ public class AccountsController : Controller
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_twoFactorEmailService = twoFactorEmailService;
}
@ -619,7 +625,14 @@ public class AccountsController : Controller
[HttpPost("resend-new-device-otp")]
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
{
await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret);
var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();
if (!await _userService.VerifySecretAsync(user, request.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user);
}
[HttpPost("verify-devices")]

View File

@ -7,6 +7,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
@ -34,6 +35,7 @@ public class TwoFactorController : Controller
private readonly IDuoUniversalTokenService _duoUniversalTokenService;
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
private readonly ITwoFactorEmailService _twoFactorEmailService;
public TwoFactorController(
IUserService userService,
@ -44,7 +46,8 @@ public class TwoFactorController : Controller
IVerifyAuthRequestCommand verifyAuthRequestCommand,
IDuoUniversalTokenService duoUniversalConfigService,
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector)
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector,
ITwoFactorEmailService twoFactorEmailService)
{
_userService = userService;
_organizationRepository = organizationRepository;
@ -55,6 +58,7 @@ public class TwoFactorController : Controller
_duoUniversalTokenService = duoUniversalConfigService;
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
_twoFactorEmailService = twoFactorEmailService;
}
[HttpGet("")]
@ -297,8 +301,9 @@ public class TwoFactorController : Controller
public async Task SendEmail([FromBody] TwoFactorEmailRequestModel model)
{
var user = await CheckAsync(model, false, true);
// Add email to the user's 2FA providers, with the email address they've provided.
model.ToUser(user);
await _userService.SendTwoFactorEmailAsync(user, false);
await _twoFactorEmailService.SendTwoFactorSetupEmailAsync(user);
}
[AllowAnonymous]
@ -316,15 +321,14 @@ public class TwoFactorController : Controller
.VerifyAuthRequestAsync(new Guid(requestModel.AuthRequestId),
requestModel.AuthRequestAccessCode))
{
await _userService.SendTwoFactorEmailAsync(user);
return;
await _twoFactorEmailService.SendTwoFactorEmailAsync(user);
}
}
else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken))
{
if (ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user))
{
await _userService.SendTwoFactorEmailAsync(user);
await _twoFactorEmailService.SendTwoFactorEmailAsync(user);
return;
}
@ -333,7 +337,7 @@ public class TwoFactorController : Controller
}
else if (await _userService.VerifySecretAsync(user, requestModel.Secret))
{
await _userService.SendTwoFactorEmailAsync(user);
await _twoFactorEmailService.SendTwoFactorEmailAsync(user);
return;
}
}

View File

@ -0,0 +1,8 @@
namespace Core.Auth.Enums;
public enum TwoFactorEmailPurpose
{
Login,
Setup,
NewDeviceVerification,
}

View File

@ -0,0 +1,11 @@
using Bit.Core.Entities;
namespace Bit.Core.Auth.Services;
public interface ITwoFactorEmailService
{
Task SendTwoFactorEmailAsync(User user);
Task SendTwoFactorSetupEmailAsync(User user);
Task SendNewDeviceVerificationEmailAsync(User user);
Task<bool> VerifyTwoFactorTokenAsync(User user, string token);
}

View File

@ -0,0 +1,116 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Bit.Core.Auth.Enums;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Core.Auth.Enums;
using Microsoft.AspNetCore.Identity;
namespace Bit.Core.Auth.Services;
public class TwoFactorEmailService : ITwoFactorEmailService
{
private readonly ICurrentContext _currentContext;
private readonly UserManager<User> _userManager;
private readonly IMailService _mailService;
public TwoFactorEmailService(
ICurrentContext currentContext,
IMailService mailService,
UserManager<User> userManager
)
{
_currentContext = currentContext;
_userManager = userManager;
_mailService = mailService;
}
/// <summary>
/// Sends a two-factor email to the user with an OTP token for login
/// </summary>
/// <param name="user">The user to whom the email should be sent</param>
/// <exception cref="ArgumentNullException">Thrown if the user does not have an email for email 2FA</exception>
public async Task SendTwoFactorEmailAsync(User user)
{
await VerifyAndSendTwoFactorEmailAsync(user, TwoFactorEmailPurpose.Login);
}
/// <summary>
/// Sends a two-factor email to the user with an OTP for setting up 2FA
/// </summary>
/// <param name="user">The user to whom the email should be sent</param>
/// <exception cref="ArgumentNullException">Thrown if the user does not have an email for email 2FA</exception>
public async Task SendTwoFactorSetupEmailAsync(User user)
{
await VerifyAndSendTwoFactorEmailAsync(user, TwoFactorEmailPurpose.Setup);
}
/// <summary>
/// Sends a new device verification email to the user with an OTP token
/// </summary>
/// <param name="user">The user to whom the email should be sent</param>
/// <exception cref="ArgumentNullException">Thrown if the user is not provided</exception>
public async Task SendNewDeviceVerificationEmailAsync(User user)
{
ArgumentNullException.ThrowIfNull(user);
var token = await _userManager.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email);
var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? "Unknown Browser";
await _mailService.SendTwoFactorEmailAsync(
user.Email, user.Email, token, _currentContext.IpAddress, deviceType, TwoFactorEmailPurpose.NewDeviceVerification);
}
/// <summary>
/// Verifies the two-factor token for the specified user
/// </summary>
/// <param name="user">The user for whom the token should be verified</param>
/// <param name="token">The token to verify</param>
/// <exception cref="ArgumentNullException">Thrown if the user does not have an email for email 2FA</exception>
public async Task<bool> VerifyTwoFactorTokenAsync(User user, string token)
{
var email = GetUserTwoFactorEmail(user);
return await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token);
}
/// <summary>
/// Sends a two-factor email with the specified purpose to the user only if they have 2FA email set up
/// </summary>
/// <param name="user">The user to whom the email should be sent</param>
/// <param name="purpose">The purpose of the email</param>
/// <exception cref="ArgumentNullException">Thrown if the user does not have an email set up for 2FA</exception>
private async Task VerifyAndSendTwoFactorEmailAsync(User user, TwoFactorEmailPurpose purpose)
{
var email = GetUserTwoFactorEmail(user);
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email));
var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? "Unknown Browser";
await _mailService.SendTwoFactorEmailAsync(
email, user.Email, token, _currentContext.IpAddress, deviceType, purpose);
}
/// <summary>
/// Verifies the user has email 2FA and will return the email if present and throw otherwise.
/// </summary>
/// <param name="user">The user to check</param>
/// <returns>The user's 2FA email address</returns>
/// <exception cref="ArgumentNullException"></exception>
private string GetUserTwoFactorEmail(User user)
{
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue))
{
throw new ArgumentNullException("No email.");
}
return ((string)emailValue).ToLowerInvariant();
}
}

View File

@ -12,7 +12,9 @@
<ul>
<li>Deauthorize unrecognized devices</li>
<li>Change your master password</li>
<li>Turn on two-step login</li>
{{#if DisplayTwoFactorReminder}}
<li style="margin-bottom: 5px;">Turn on two-step login</li>
{{/if}}
</ul>
</td>
</tr>

View File

@ -22,4 +22,9 @@ public class TwoFactorEmailTokenViewModel : BaseMailModel
public string TimeZone { get; set; }
public string DeviceIp { get; set; }
public string DeviceType { get; set; }
/// <summary>
/// Depending on the context, we may want to show a reminder to the user that they should enable two factor authentication.
/// This is not relevant when the user is using the email to verify setting up 2FA, so we hide it in that case.
/// </summary>
public bool DisplayTwoFactorReminder { get; set; }
}

View File

@ -8,6 +8,7 @@ using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Mail;
using Bit.Core.Vault.Models.Data;
using Core.Auth.Enums;
namespace Bit.Core.Services;
@ -27,7 +28,7 @@ public interface IMailService
Task SendCannotDeleteClaimedAccountEmailAsync(string email);
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true);
Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose);
Task SendNoMasterPasswordHintEmailAsync(string email);
Task SendMasterPasswordHintEmailAsync(string email, string hint);

View File

@ -21,21 +21,6 @@ public interface IUserService
Task<IdentityResult> CreateUserAsync(User user);
Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash);
Task SendMasterPasswordHintAsync(string email);
/// <summary>
/// Used for both email two factor and email two factor setup.
/// </summary>
/// <param name="user">user requesting the action</param>
/// <param name="authentication">this controls if what verbiage is shown in the email</param>
/// <returns>void</returns>
Task SendTwoFactorEmailAsync(User user, bool authentication = true);
/// <summary>
/// Calls the same email implementation but instead it sends the token to the account email not the
/// email set up for two-factor, since in practice they can be different.
/// </summary>
/// <param name="user">user attepting to login with a new device</param>
/// <returns>void</returns>
Task SendNewDeviceVerificationEmailAsync(User user);
Task<bool> VerifyTwoFactorEmailAsync(User user, string token);
Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user);
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
@ -87,7 +72,6 @@ public interface IUserService
Task SendOTPAsync(User user);
Task<bool> VerifyOTPAsync(User user, string token);
Task<bool> VerifySecretAsync(User user, string secret, bool isSettingMFA = false);
Task ResendNewDeviceVerificationEmail(string email, string secret);
/// <summary>
/// We use this method to check if the user has an active new device verification bypass
/// </summary>

View File

@ -21,6 +21,7 @@ using Bit.Core.SecretsManager.Models.Mail;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Core.Vault.Models.Data;
using Core.Auth.Enums;
using HandlebarsDotNet;
namespace Bit.Core.Services;
@ -166,14 +167,14 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true)
public async Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose)
{
var message = CreateDefaultMessage("Your Bitwarden Verification Code", email);
var requestDateTime = DateTime.UtcNow;
var model = new TwoFactorEmailTokenViewModel
{
Token = token,
EmailTotpAction = authentication ? "logging in" : "setting up two-step login",
EmailTotpAction = (purpose == TwoFactorEmailPurpose.Setup) ? "setting up two-step login" : "logging in",
AccountEmail = accountEmail,
TheDate = requestDateTime.ToLongDateString(),
TheTime = requestDateTime.ToShortTimeString(),
@ -182,6 +183,9 @@ public class HandlebarsMailService : IMailService
DeviceType = deviceType,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
// We only want to remind users to set up 2FA if they're getting a new device verification email.
// For login with 2FA, and setup of 2FA, we do not want to show the reminder because users are already doing so.
DisplayTwoFactorReminder = purpose == TwoFactorEmailPurpose.NewDeviceVerification
};
await AddMessageContentAsync(message, "Auth.TwoFactorEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);

View File

@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Security.Claims;
using System.Security.Claims;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
@ -337,52 +335,6 @@ public class UserService : UserManager<User>, IUserService
await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint);
}
public async Task SendTwoFactorEmailAsync(User user, bool authentication = true)
{
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue))
{
throw new ArgumentNullException("No email.");
}
var email = ((string)emailValue).ToLowerInvariant();
var token = await base.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email));
var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? "Unknown Browser";
await _mailService.SendTwoFactorEmailAsync(
email, user.Email, token, _currentContext.IpAddress, deviceType, authentication);
}
public async Task SendNewDeviceVerificationEmailAsync(User user)
{
ArgumentNullException.ThrowIfNull(user);
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email);
var deviceType = _currentContext.DeviceType?.GetType().GetMember(_currentContext.DeviceType?.ToString())
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName() ?? "Unknown Browser";
await _mailService.SendTwoFactorEmailAsync(
user.Email, user.Email, token, _currentContext.IpAddress, deviceType);
}
public async Task<bool> VerifyTwoFactorEmailAsync(User user, string token)
{
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email);
if (provider == null || provider.MetaData == null || !provider.MetaData.TryGetValue("Email", out var emailValue))
{
throw new ArgumentNullException("No email.");
}
var email = ((string)emailValue).ToLowerInvariant();
return await base.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token);
}
public async Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user)
{
var providers = user.GetTwoFactorProviders();
@ -1454,20 +1406,6 @@ public class UserService : UserManager<User>, IUserService
return isVerified;
}
public async Task ResendNewDeviceVerificationEmail(string email, string secret)
{
var user = await _userRepository.GetByEmailAsync(email);
if (user == null)
{
return;
}
if (await VerifySecretAsync(user, secret))
{
await SendNewDeviceVerificationEmailAsync(user);
}
}
public async Task<bool> ActiveNewDeviceVerificationException(Guid userId)
{
var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString());

View File

@ -8,6 +8,7 @@ using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Mail;
using Bit.Core.Vault.Models.Data;
using Core.Auth.Enums;
namespace Bit.Core.Services;
@ -86,7 +87,7 @@ public class NoopMailService : IMailService
public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) =>
Task.CompletedTask;
public Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, bool authentication = true)
public Task SendTwoFactorEmailAsync(string email, string accountEmail, string token, string deviceIp, string deviceType, TwoFactorEmailPurpose purpose)
{
return Task.FromResult(0);
}

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Bit.Core;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -22,6 +23,7 @@ public class DeviceValidator(
ICurrentContext currentContext,
IUserService userService,
IDistributedCache distributedCache,
ITwoFactorEmailService twoFactorEmailService,
ILogger<DeviceValidator> logger) : IDeviceValidator
{
private readonly IDeviceService _deviceService = deviceService;
@ -32,6 +34,7 @@ public class DeviceValidator(
private readonly IUserService _userService = userService;
private readonly IDistributedCache distributedCache = distributedCache;
private readonly ILogger<DeviceValidator> _logger = logger;
private readonly ITwoFactorEmailService _twoFactorEmailService = twoFactorEmailService;
public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context)
{
@ -75,7 +78,7 @@ public class DeviceValidator(
BuildDeviceErrorResult(validationResult);
if (validationResult == DeviceValidationResultType.NewDeviceVerificationRequired)
{
await _userService.SendNewDeviceVerificationEmailAsync(context.User);
await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(context.User);
}
return false;
}

View File

@ -234,6 +234,7 @@ public static class ServiceCollectionExtensions
});
services.AddScoped<IPaymentService, StripePaymentService>();
services.AddScoped<IPaymentHistoryService, PaymentHistoryService>();
services.AddScoped<ITwoFactorEmailService, TwoFactorEmailService>();
services.AddSingleton<IStripeSyncService, StripeSyncService>();
services.AddSingleton<IMailService, HandlebarsMailService>();
services.AddSingleton<ILicensingService, LicensingService>();

View File

@ -4,6 +4,7 @@ using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
@ -31,6 +32,8 @@ public class AccountsControllerTests : IDisposable
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IFeatureService _featureService;
private readonly ITwoFactorEmailService _twoFactorEmailService;
public AccountsControllerTests()
{
@ -43,6 +46,8 @@ public class AccountsControllerTests : IDisposable
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
_featureService = Substitute.For<IFeatureService>();
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
_sut = new AccountsController(
_organizationService,
@ -53,7 +58,8 @@ public class AccountsControllerTests : IDisposable
_setInitialMasterPasswordCommand,
_tdeOffboardingPasswordCommand,
_twoFactorIsEnabledQuery,
_featureService
_featureService,
_twoFactorEmailService
);
}
@ -547,6 +553,46 @@ public class AccountsControllerTests : IDisposable
Assert.Equal(model.VerifyDevices, user.VerifyDevices);
}
[Theory]
[BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenUserNotFound_ShouldFail(
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult((User)null));
// Act & Assert
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.ResendNewDeviceOtpAsync(model));
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenSecretNotValid_ShouldFail(
User user,
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(false));
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(() => _sut.ResendNewDeviceOtpAsync(model));
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_WhenTokenValid_SendsEmail(User user,
UnauthenticatedSecretVerificationRequestModel model)
{
// Arrange
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(Task.FromResult(user));
_userService.VerifySecretAsync(user, Arg.Any<string>()).Returns(Task.FromResult(true));
// Act
await _sut.ResendNewDeviceOtpAsync(model);
// Assert
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(user);
}
// Below are helper functions that currently belong to this
// test class, but ultimately may need to be split out into
// something greater in order to share common test steps with

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")]

View File

@ -1,4 +1,5 @@
using Bit.Core.Context;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
@ -26,6 +27,7 @@ public class DeviceValidatorTests
private readonly ICurrentContext _currentContext;
private readonly IUserService _userService;
private readonly IDistributedCache _distributedCache;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly Logger<DeviceValidator> _logger;
private readonly DeviceValidator _sut;
@ -39,6 +41,7 @@ public class DeviceValidatorTests
_currentContext = Substitute.For<ICurrentContext>();
_userService = Substitute.For<IUserService>();
_distributedCache = Substitute.For<IDistributedCache>();
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
_logger = new Logger<DeviceValidator>(Substitute.For<ILoggerFactory>());
_sut = new DeviceValidator(
_deviceService,
@ -48,6 +51,7 @@ public class DeviceValidatorTests
_currentContext,
_userService,
_distributedCache,
_twoFactorEmailService,
_logger);
}
@ -580,7 +584,7 @@ public class DeviceValidatorTests
var result = await _sut.ValidateRequestDeviceAsync(request, context);
// Assert
await _userService.Received(1).SendNewDeviceVerificationEmailAsync(context.User);
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(context.User);
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
Assert.False(result);