mirror of
https://github.com/bitwarden/server.git
synced 2025-06-16 07:50:49 -05:00
Refactored to new service.
This commit is contained in:
parent
da5fa26054
commit
a08f7c5c7b
@ -15,6 +15,7 @@ using Bit.Core.AdminConsole.Services;
|
|||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||||
@ -59,7 +60,7 @@ public class AccountsController : Controller
|
|||||||
_organizationUserValidator;
|
_organizationUserValidator;
|
||||||
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
||||||
_webauthnKeyValidator;
|
_webauthnKeyValidator;
|
||||||
|
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||||
|
|
||||||
public AccountsController(
|
public AccountsController(
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
@ -79,7 +80,8 @@ public class AccountsController : Controller
|
|||||||
emergencyAccessValidator,
|
emergencyAccessValidator,
|
||||||
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
|
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
|
||||||
organizationUserValidator,
|
organizationUserValidator,
|
||||||
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator
|
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator,
|
||||||
|
ITwoFactorEmailService twoFactorEmailService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
@ -98,6 +100,7 @@ public class AccountsController : Controller
|
|||||||
_emergencyAccessValidator = emergencyAccessValidator;
|
_emergencyAccessValidator = emergencyAccessValidator;
|
||||||
_organizationUserValidator = organizationUserValidator;
|
_organizationUserValidator = organizationUserValidator;
|
||||||
_webauthnKeyValidator = webAuthnKeyValidator;
|
_webauthnKeyValidator = webAuthnKeyValidator;
|
||||||
|
_twoFactorEmailService = twoFactorEmailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -700,7 +703,14 @@ public class AccountsController : Controller
|
|||||||
[HttpPost("resend-new-device-otp")]
|
[HttpPost("resend-new-device-otp")]
|
||||||
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
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")]
|
[HttpPost("verify-devices")]
|
||||||
|
@ -7,6 +7,7 @@ using Bit.Core.Auth.Enums;
|
|||||||
using Bit.Core.Auth.Identity.TokenProviders;
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
|
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -14,6 +15,7 @@ using Bit.Core.Repositories;
|
|||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using Core.Auth.Enums;
|
||||||
using Fido2NetLib;
|
using Fido2NetLib;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
@ -34,6 +36,7 @@ public class TwoFactorController : Controller
|
|||||||
private readonly IDuoUniversalTokenService _duoUniversalTokenService;
|
private readonly IDuoUniversalTokenService _duoUniversalTokenService;
|
||||||
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
|
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
|
||||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
|
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
|
||||||
|
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||||
|
|
||||||
public TwoFactorController(
|
public TwoFactorController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
@ -44,7 +47,8 @@ public class TwoFactorController : Controller
|
|||||||
IVerifyAuthRequestCommand verifyAuthRequestCommand,
|
IVerifyAuthRequestCommand verifyAuthRequestCommand,
|
||||||
IDuoUniversalTokenService duoUniversalConfigService,
|
IDuoUniversalTokenService duoUniversalConfigService,
|
||||||
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
|
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
|
||||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector)
|
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector,
|
||||||
|
ITwoFactorEmailService twoFactorEmailService)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
@ -55,6 +59,7 @@ public class TwoFactorController : Controller
|
|||||||
_duoUniversalTokenService = duoUniversalConfigService;
|
_duoUniversalTokenService = duoUniversalConfigService;
|
||||||
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
|
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
|
||||||
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
|
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
|
||||||
|
_twoFactorEmailService = twoFactorEmailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@ -298,7 +303,7 @@ public class TwoFactorController : Controller
|
|||||||
{
|
{
|
||||||
var user = await CheckAsync(model, false, true);
|
var user = await CheckAsync(model, false, true);
|
||||||
model.ToUser(user);
|
model.ToUser(user);
|
||||||
await _userService.SendTwoFactorEmailAsync(user, false);
|
await _twoFactorEmailService.SendTwoFactorSetupEmailAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
@ -316,15 +321,14 @@ public class TwoFactorController : Controller
|
|||||||
.VerifyAuthRequestAsync(new Guid(requestModel.AuthRequestId),
|
.VerifyAuthRequestAsync(new Guid(requestModel.AuthRequestId),
|
||||||
requestModel.AuthRequestAccessCode))
|
requestModel.AuthRequestAccessCode))
|
||||||
{
|
{
|
||||||
await _userService.SendTwoFactorEmailAsync(user);
|
await _twoFactorEmailService.SendTwoFactorEmailAsync(user);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken))
|
else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken))
|
||||||
{
|
{
|
||||||
if (ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user))
|
if (ValidateSsoEmail2FaToken(requestModel.SsoEmail2FaSessionToken, user))
|
||||||
{
|
{
|
||||||
await _userService.SendTwoFactorEmailAsync(user);
|
await _twoFactorEmailService.SendTwoFactorEmailAsync(user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,7 +337,7 @@ public class TwoFactorController : Controller
|
|||||||
}
|
}
|
||||||
else if (await _userService.VerifySecretAsync(user, requestModel.Secret))
|
else if (await _userService.VerifySecretAsync(user, requestModel.Secret))
|
||||||
{
|
{
|
||||||
await _userService.SendTwoFactorEmailAsync(user);
|
await _twoFactorEmailService.SendTwoFactorEmailAsync(user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
src/Core/Auth/Services/ITwoFactorEmailService.cs
Normal file
11
src/Core/Auth/Services/ITwoFactorEmailService.cs
Normal 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> VerifyTwoFactorEmailAsync(User user, string token);
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
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 ICurrentContext _currentContext;
|
||||||
|
private UserManager<User> _userManager;
|
||||||
|
private IMailService _mailService;
|
||||||
|
|
||||||
|
public TwoFactorEmailService(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IMailService mailService,
|
||||||
|
UserManager<User> userManager
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_userManager = userManager;
|
||||||
|
_mailService = mailService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendTwoFactorEmailAsync(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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var email = ((string)emailValue).ToLowerInvariant();
|
||||||
|
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, TwoFactorEmailPurpose.Login);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendTwoFactorSetupEmailAsync(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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var email = ((string)emailValue).ToLowerInvariant();
|
||||||
|
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, TwoFactorEmailPurpose.Setup);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _userManager.VerifyTwoFactorTokenAsync(user,
|
||||||
|
CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), token);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Models.Mail;
|
using Bit.Core.Models.Mail;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
using Core.Auth.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ public interface IMailService
|
|||||||
Task SendCannotDeleteClaimedAccountEmailAsync(string email);
|
Task SendCannotDeleteClaimedAccountEmailAsync(string email);
|
||||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
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 SendNoMasterPasswordHintEmailAsync(string email);
|
||||||
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
Task SendMasterPasswordHintEmailAsync(string email, string hint);
|
||||||
|
|
||||||
|
@ -21,21 +21,6 @@ public interface IUserService
|
|||||||
Task<IdentityResult> CreateUserAsync(User user);
|
Task<IdentityResult> CreateUserAsync(User user);
|
||||||
Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash);
|
Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash);
|
||||||
Task SendMasterPasswordHintAsync(string email);
|
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<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user);
|
||||||
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);
|
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);
|
||||||
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
|
Task<bool> CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse);
|
||||||
@ -87,7 +72,6 @@ public interface IUserService
|
|||||||
Task SendOTPAsync(User user);
|
Task SendOTPAsync(User user);
|
||||||
Task<bool> VerifyOTPAsync(User user, string token);
|
Task<bool> VerifyOTPAsync(User user, string token);
|
||||||
Task<bool> VerifySecretAsync(User user, string secret, bool isSettingMFA = false);
|
Task<bool> VerifySecretAsync(User user, string secret, bool isSettingMFA = false);
|
||||||
Task ResendNewDeviceVerificationEmail(string email, string secret);
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// We use this method to check if the user has an active new device verification bypass
|
/// We use this method to check if the user has an active new device verification bypass
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.Security.Claims;
|
||||||
using System.Reflection;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Data;
|
using Bit.Core.AdminConsole.Models.Data;
|
||||||
@ -337,52 +335,6 @@ public class UserService : UserManager<User>, IUserService
|
|||||||
await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint);
|
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)
|
public async Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user)
|
||||||
{
|
{
|
||||||
var providers = user.GetTwoFactorProviders();
|
var providers = user.GetTwoFactorProviders();
|
||||||
@ -1454,20 +1406,6 @@ public class UserService : UserManager<User>, IUserService
|
|||||||
return isVerified;
|
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)
|
public async Task<bool> ActiveNewDeviceVerificationException(Guid userId)
|
||||||
{
|
{
|
||||||
var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString());
|
var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString());
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Models.Data.Organizations;
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Models.Mail;
|
using Bit.Core.Models.Mail;
|
||||||
using Bit.Core.Vault.Models.Data;
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
using Core.Auth.Enums;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
@ -86,7 +87,7 @@ public class NoopMailService : IMailService
|
|||||||
public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) =>
|
public Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) =>
|
||||||
Task.CompletedTask;
|
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);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -22,6 +23,7 @@ public class DeviceValidator(
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IDistributedCache distributedCache,
|
IDistributedCache distributedCache,
|
||||||
|
ITwoFactorEmailService twoFactorEmailService,
|
||||||
ILogger<DeviceValidator> logger) : IDeviceValidator
|
ILogger<DeviceValidator> logger) : IDeviceValidator
|
||||||
{
|
{
|
||||||
private readonly IDeviceService _deviceService = deviceService;
|
private readonly IDeviceService _deviceService = deviceService;
|
||||||
@ -32,6 +34,7 @@ public class DeviceValidator(
|
|||||||
private readonly IUserService _userService = userService;
|
private readonly IUserService _userService = userService;
|
||||||
private readonly IDistributedCache distributedCache = distributedCache;
|
private readonly IDistributedCache distributedCache = distributedCache;
|
||||||
private readonly ILogger<DeviceValidator> _logger = logger;
|
private readonly ILogger<DeviceValidator> _logger = logger;
|
||||||
|
private readonly ITwoFactorEmailService _twoFactorEmailService = twoFactorEmailService;
|
||||||
|
|
||||||
public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context)
|
public async Task<bool> ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context)
|
||||||
{
|
{
|
||||||
@ -75,7 +78,7 @@ public class DeviceValidator(
|
|||||||
BuildDeviceErrorResult(validationResult);
|
BuildDeviceErrorResult(validationResult);
|
||||||
if (validationResult == DeviceValidationResultType.NewDeviceVerificationRequired)
|
if (validationResult == DeviceValidationResultType.NewDeviceVerificationRequired)
|
||||||
{
|
{
|
||||||
await _userService.SendNewDeviceVerificationEmailAsync(context.User);
|
await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(context.User);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ using Bit.Core.AdminConsole.Services;
|
|||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
|
using Bit.Core.Auth.Services;
|
||||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||||
@ -52,6 +53,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
|
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
|
||||||
IReadOnlyList<OrganizationUser>>
|
IReadOnlyList<OrganizationUser>>
|
||||||
_resetPasswordValidator;
|
_resetPasswordValidator;
|
||||||
|
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||||
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
||||||
_webauthnKeyRotationValidator;
|
_webauthnKeyRotationValidator;
|
||||||
|
|
||||||
@ -79,6 +81,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
_resetPasswordValidator = Substitute
|
_resetPasswordValidator = Substitute
|
||||||
.For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
|
.For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
|
||||||
IReadOnlyList<OrganizationUser>>>();
|
IReadOnlyList<OrganizationUser>>>();
|
||||||
|
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
|
||||||
|
|
||||||
_sut = new AccountsController(
|
_sut = new AccountsController(
|
||||||
_organizationService,
|
_organizationService,
|
||||||
@ -96,7 +99,8 @@ public class AccountsControllerTests : IDisposable
|
|||||||
_sendValidator,
|
_sendValidator,
|
||||||
_emergencyAccessValidator,
|
_emergencyAccessValidator,
|
||||||
_resetPasswordValidator,
|
_resetPasswordValidator,
|
||||||
_webauthnKeyRotationValidator
|
_webauthnKeyRotationValidator,
|
||||||
|
_twoFactorEmailService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
210
test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs
Normal file
210
test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
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 = "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;
|
||||||
|
|
||||||
|
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,
|
||||||
|
TwoFactorEmailPurpose.Login);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 SendNewDeviceVerificationEmailAsync_DeviceMatches(DeviceType deviceType, string deviceTypeName,
|
||||||
|
User user)
|
||||||
|
{
|
||||||
|
var sutProvider = new SutProvider<TwoFactorEmailService>();
|
||||||
|
|
||||||
|
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<TwoFactorEmailPurpose>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(User user)
|
||||||
|
{
|
||||||
|
var sutProvider = new SutProvider<TwoFactorEmailService>();
|
||||||
|
|
||||||
|
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<TwoFactorEmailPurpose>());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// [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<TwoFactorEmailService>();
|
||||||
|
|
||||||
|
// // 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>());
|
||||||
|
|
||||||
|
// }
|
||||||
|
}
|
@ -11,7 +11,6 @@ using Bit.Core.AdminConsole.Services;
|
|||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -84,125 +83,6 @@ public class UserServiceTests
|
|||||||
Assert.Equal(1, versionProp.GetInt32());
|
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]
|
[Theory, BitAutoData]
|
||||||
public async Task HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider<UserService> sutProvider, User user)
|
public async Task HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider<UserService> sutProvider, User user)
|
||||||
{
|
{
|
||||||
@ -577,78 +457,6 @@ public class UserServiceTests
|
|||||||
.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default);
|
.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]
|
[Theory]
|
||||||
[BitAutoData("")]
|
[BitAutoData("")]
|
||||||
[BitAutoData("null")]
|
[BitAutoData("null")]
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Auth.Services;
|
||||||
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
@ -26,6 +27,7 @@ public class DeviceValidatorTests
|
|||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IDistributedCache _distributedCache;
|
private readonly IDistributedCache _distributedCache;
|
||||||
|
private readonly ITwoFactorEmailService _twoFactorEmailService;
|
||||||
private readonly Logger<DeviceValidator> _logger;
|
private readonly Logger<DeviceValidator> _logger;
|
||||||
|
|
||||||
private readonly DeviceValidator _sut;
|
private readonly DeviceValidator _sut;
|
||||||
@ -39,6 +41,7 @@ public class DeviceValidatorTests
|
|||||||
_currentContext = Substitute.For<ICurrentContext>();
|
_currentContext = Substitute.For<ICurrentContext>();
|
||||||
_userService = Substitute.For<IUserService>();
|
_userService = Substitute.For<IUserService>();
|
||||||
_distributedCache = Substitute.For<IDistributedCache>();
|
_distributedCache = Substitute.For<IDistributedCache>();
|
||||||
|
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
|
||||||
_logger = new Logger<DeviceValidator>(Substitute.For<ILoggerFactory>());
|
_logger = new Logger<DeviceValidator>(Substitute.For<ILoggerFactory>());
|
||||||
_sut = new DeviceValidator(
|
_sut = new DeviceValidator(
|
||||||
_deviceService,
|
_deviceService,
|
||||||
@ -48,6 +51,7 @@ public class DeviceValidatorTests
|
|||||||
_currentContext,
|
_currentContext,
|
||||||
_userService,
|
_userService,
|
||||||
_distributedCache,
|
_distributedCache,
|
||||||
|
_twoFactorEmailService,
|
||||||
_logger);
|
_logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,7 +584,7 @@ public class DeviceValidatorTests
|
|||||||
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
var result = await _sut.ValidateRequestDeviceAsync(request, context);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await _userService.Received(1).SendNewDeviceVerificationEmailAsync(context.User);
|
await _twoFactorEmailService.Received(1).SendNewDeviceVerificationEmailAsync(context.User);
|
||||||
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
|
await _deviceService.Received(0).SaveAsync(Arg.Any<Device>());
|
||||||
|
|
||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user