diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 2499b269f5..a7a8dd78b8 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -15,6 +15,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; +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; @@ -59,7 +60,7 @@ public class AccountsController : Controller _organizationUserValidator; private readonly IRotationValidator, IEnumerable> _webauthnKeyValidator; - + private readonly ITwoFactorEmailService _twoFactorEmailService; public AccountsController( IOrganizationService organizationService, @@ -79,7 +80,8 @@ public class AccountsController : Controller emergencyAccessValidator, IRotationValidator, IReadOnlyList> organizationUserValidator, - IRotationValidator, IEnumerable> webAuthnKeyValidator + IRotationValidator, IEnumerable> webAuthnKeyValidator, + ITwoFactorEmailService twoFactorEmailService ) { _organizationService = organizationService; @@ -98,6 +100,7 @@ public class AccountsController : Controller _emergencyAccessValidator = emergencyAccessValidator; _organizationUserValidator = organizationUserValidator; _webauthnKeyValidator = webAuthnKeyValidator; + _twoFactorEmailService = twoFactorEmailService; } @@ -700,7 +703,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")] diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs index 83490f1c2f..c60893ef6e 100644 --- a/src/Api/Auth/Controllers/TwoFactorController.cs +++ b/src/Api/Auth/Controllers/TwoFactorController.cs @@ -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; @@ -14,6 +15,7 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Tokens; using Bit.Core.Utilities; +using Core.Auth.Enums; using Fido2NetLib; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -34,6 +36,7 @@ public class TwoFactorController : Controller private readonly IDuoUniversalTokenService _duoUniversalTokenService; private readonly IDataProtectorTokenFactory _twoFactorAuthenticatorDataProtector; private readonly IDataProtectorTokenFactory _ssoEmailTwoFactorSessionDataProtector; + private readonly ITwoFactorEmailService _twoFactorEmailService; public TwoFactorController( IUserService userService, @@ -44,7 +47,8 @@ public class TwoFactorController : Controller IVerifyAuthRequestCommand verifyAuthRequestCommand, IDuoUniversalTokenService duoUniversalConfigService, IDataProtectorTokenFactory twoFactorAuthenticatorDataProtector, - IDataProtectorTokenFactory ssoEmailTwoFactorSessionDataProtector) + IDataProtectorTokenFactory ssoEmailTwoFactorSessionDataProtector, + ITwoFactorEmailService twoFactorEmailService) { _userService = userService; _organizationRepository = organizationRepository; @@ -55,6 +59,7 @@ public class TwoFactorController : Controller _duoUniversalTokenService = duoUniversalConfigService; _twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector; _ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector; + _twoFactorEmailService = twoFactorEmailService; } [HttpGet("")] @@ -298,7 +303,7 @@ public class TwoFactorController : Controller { var user = await CheckAsync(model, false, true); 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; } } diff --git a/src/Core/Auth/Services/ITwoFactorEmailService.cs b/src/Core/Auth/Services/ITwoFactorEmailService.cs new file mode 100644 index 0000000000..e22ef81ef2 --- /dev/null +++ b/src/Core/Auth/Services/ITwoFactorEmailService.cs @@ -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 VerifyTwoFactorEmailAsync(User user, string token); +} \ No newline at end of file diff --git a/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs b/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs new file mode 100644 index 0000000000..ce1f139411 --- /dev/null +++ b/src/Core/Auth/Services/Implementations/TwoFactorEmailService.cs @@ -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 _userManager; + private IMailService _mailService; + + public TwoFactorEmailService( + ICurrentContext currentContext, + IMailService mailService, + UserManager 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()?.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()?.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()?.GetName() ?? "Unknown Browser"; + + await _mailService.SendTwoFactorEmailAsync( + user.Email, user.Email, token, _currentContext.IpAddress, deviceType, TwoFactorEmailPurpose.NewDeviceVerification); + } + + public async Task 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); + } +} \ No newline at end of file diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index aa1c0c8c25..e5a7577770 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -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); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index e63b4e3b87..2ac9345ebf 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -21,21 +21,6 @@ public interface IUserService Task CreateUserAsync(User user); Task CreateUserAsync(User user, string masterPasswordHash); Task SendMasterPasswordHintAsync(string email); - /// - /// Used for both email two factor and email two factor setup. - /// - /// user requesting the action - /// this controls if what verbiage is shown in the email - /// void - Task SendTwoFactorEmailAsync(User user, bool authentication = true); - /// - /// 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. - /// - /// user attepting to login with a new device - /// void - Task SendNewDeviceVerificationEmailAsync(User user); - Task VerifyTwoFactorEmailAsync(User user, string token); Task StartWebAuthnRegistrationAsync(User user); Task DeleteWebAuthnKeyAsync(User user, int id); Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); @@ -87,7 +72,6 @@ public interface IUserService Task SendOTPAsync(User user); Task VerifyOTPAsync(User user, string token); Task VerifySecretAsync(User user, string secret, bool isSettingMFA = false); - Task ResendNewDeviceVerificationEmail(string email, string secret); /// /// We use this method to check if the user has an active new device verification bypass /// diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index fe5a064c44..1e8acf0a15 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -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, 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()?.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()?.GetName() ?? "Unknown Browser"; - - await _mailService.SendTwoFactorEmailAsync( - user.Email, user.Email, token, _currentContext.IpAddress, deviceType); - } - - public async Task 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 StartWebAuthnRegistrationAsync(User user) { var providers = user.GetTwoFactorProviders(); @@ -1454,20 +1406,6 @@ public class UserService : UserManager, 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 ActiveNewDeviceVerificationException(Guid userId) { var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, userId.ToString()); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 26858911a8..d8f2488088 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -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); } diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 4dc77c4449..ce5189703e 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -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 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 _logger = logger; + private readonly ITwoFactorEmailService _twoFactorEmailService = twoFactorEmailService; public async Task 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; } diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 581a7e8f04..3d21c9095b 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -12,6 +12,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; +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; @@ -52,6 +53,7 @@ public class AccountsControllerTests : IDisposable private readonly IRotationValidator, IReadOnlyList> _resetPasswordValidator; + private readonly ITwoFactorEmailService _twoFactorEmailService; private readonly IRotationValidator, IEnumerable> _webauthnKeyRotationValidator; @@ -79,6 +81,7 @@ public class AccountsControllerTests : IDisposable _resetPasswordValidator = Substitute .For, IReadOnlyList>>(); + _twoFactorEmailService = Substitute.For(); _sut = new AccountsController( _organizationService, @@ -96,7 +99,8 @@ public class AccountsControllerTests : IDisposable _sendValidator, _emergencyAccessValidator, _resetPasswordValidator, - _webauthnKeyRotationValidator + _webauthnKeyRotationValidator, + _twoFactorEmailService ); } diff --git a/test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs b/test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs new file mode 100644 index 0000000000..d59e57c94a --- /dev/null +++ b/test/Core.Test/Auth/Services/TwoFactorEmailServiceTests.cs @@ -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 sutProvider, User user) + { + var email = user.Email.ToLowerInvariant(); + var token = "thisisatokentocompare"; + var IpAddress = "1.1.1.1"; + var deviceType = "Android"; + + var userTwoFactorTokenProvider = Substitute.For>(); + userTwoFactorTokenProvider + .CanGenerateTwoFactorTokenAsync(Arg.Any>(), user) + .Returns(Task.FromResult(true)); + userTwoFactorTokenProvider + .GenerateAsync("TwoFactor", Arg.Any>(), user) + .Returns(Task.FromResult(token)); + + var context = sutProvider.GetDependency(); + context.DeviceType = DeviceType.Android; + context.IpAddress = IpAddress; + + var userManager = sutProvider.GetDependency>(); + userManager.RegisterTokenProvider(CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), userTwoFactorTokenProvider); + + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new TwoFactorProvider + { + MetaData = new Dictionary { ["Email"] = email }, + Enabled = true + } + }); + await sutProvider.Sut.SendTwoFactorEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType, + TwoFactorEmailPurpose.Login); + } + + [Theory, BitAutoData] + public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderOnUser(SutProvider sutProvider, User user) + { + user.TwoFactorProviders = null; + + await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); + } + + [Theory, BitAutoData] + public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderMetadataOnUser(SutProvider sutProvider, User user) + { + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new TwoFactorProvider + { + MetaData = null, + Enabled = true + } + }); + + await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); + } + + [Theory, BitAutoData] + public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderEmailMetadataOnUser(SutProvider sutProvider, User user) + { + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new TwoFactorProvider + { + MetaData = new Dictionary { ["qweqwe"] = user.Email.ToLowerInvariant() }, + Enabled = true + } + }); + + await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); + } + + [Theory, BitAutoData] + public async Task SendNewDeviceVerificationEmailAsync_ExceptionBecauseUserNull(SutProvider sutProvider) + { + await Assert.ThrowsAsync(() => 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(); + + var context = sutProvider.GetDependency(); + context.DeviceType = deviceType; + context.IpAddress = "1.1.1.1"; + + await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), deviceTypeName, Arg.Any()); + } + + [Theory, BitAutoData] + public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(User user) + { + var sutProvider = new SutProvider(); + + var context = sutProvider.GetDependency(); + context.DeviceType = null; + context.IpAddress = "1.1.1.1"; + + await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user); + + await sutProvider.GetDependency() + .Received(1) + .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), "Unknown Browser", Arg.Any()); + } + + + // [Theory, BitAutoData] + // public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled( + // SutProvider sutProvider, string email, string secret) + // { + // sutProvider.GetDependency() + // .GetByEmailAsync(email) + // .Returns(null as User); + + // await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret); + + // await sutProvider.GetDependency() + // .DidNotReceive() + // .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + // } + + // [Theory, BitAutoData] + // public async Task ResendNewDeviceVerificationEmail_SecretNotValid_SendTwoFactorEmailAsyncNotCalled( + // SutProvider sutProvider, string email, string secret) + // { + // sutProvider.GetDependency() + // .GetByEmailAsync(email) + // .Returns(null as User); + + // await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret); + + // await sutProvider.GetDependency() + // .DidNotReceive() + // .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + // } + + // [Theory, BitAutoData] + // public async Task ResendNewDeviceVerificationEmail_SendsToken_Success(User user) + // { + // // Arrange + // var testPassword = "test_password"; + // SetupUserAndDevice(user, true); + + // var sutProvider = new SutProvider(); + + // // Setup the fake password verification + // sutProvider + // .GetDependency>() + // .GetPasswordHashAsync(user, Arg.Any()) + // .Returns((ci) => + // { + // return Task.FromResult("hashed_test_password"); + // }); + + // sutProvider.GetDependency>() + // .VerifyHashedPassword(user, "hashed_test_password", testPassword) + // .Returns((ci) => + // { + // return PasswordVerificationResult.Success; + // }); + + // sutProvider.GetDependency() + // .GetByEmailAsync(user.Email) + // .Returns(user); + + // var context = sutProvider.GetDependency(); + // context.DeviceType = DeviceType.Android; + // context.IpAddress = "1.1.1.1"; + + // await sutProvider.Sut.ResendNewDeviceVerificationEmail(user.Email, testPassword); + + // await sutProvider.GetDependency() + // .Received(1) + // .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + + // } +} \ No newline at end of file diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 0589753dd7..5332ae21de 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -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 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>(); - userTwoFactorTokenProvider - .CanGenerateTwoFactorTokenAsync(Arg.Any>(), user) - .Returns(Task.FromResult(true)); - userTwoFactorTokenProvider - .GenerateAsync("TwoFactor", Arg.Any>(), user) - .Returns(Task.FromResult(token)); - - var context = sutProvider.GetDependency(); - context.DeviceType = DeviceType.Android; - context.IpAddress = IpAddress; - - sutProvider.Sut.RegisterTokenProvider("Custom_Email", userTwoFactorTokenProvider); - - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new TwoFactorProvider - { - MetaData = new Dictionary { ["Email"] = email }, - Enabled = true - } - }); - await sutProvider.Sut.SendTwoFactorEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .SendTwoFactorEmailAsync(email, user.Email, token, IpAddress, deviceType, authentication); - } - - [Theory, BitAutoData] - public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderOnUser(SutProvider sutProvider, User user) - { - user.TwoFactorProviders = null; - - await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); - } - - [Theory, BitAutoData] - public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderMetadataOnUser(SutProvider sutProvider, User user) - { - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new TwoFactorProvider - { - MetaData = null, - Enabled = true - } - }); - - await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); - } - - [Theory, BitAutoData] - public async Task SendTwoFactorEmailAsync_ExceptionBecauseNoProviderEmailMetadataOnUser(SutProvider sutProvider, User user) - { - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new TwoFactorProvider - { - MetaData = new Dictionary { ["qweqwe"] = user.Email.ToLowerInvariant() }, - Enabled = true - } - }); - - await Assert.ThrowsAsync("No email.", () => sutProvider.Sut.SendTwoFactorEmailAsync(user)); - } - - [Theory, BitAutoData] - public async Task SendNewDeviceVerificationEmailAsync_ExceptionBecauseUserNull(SutProvider sutProvider) - { - await Assert.ThrowsAsync(() => 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() - .CreateWithUserServiceCustomizations(user); - - var context = sutProvider.GetDependency(); - context.DeviceType = deviceType; - context.IpAddress = "1.1.1.1"; - - await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), deviceTypeName, Arg.Any()); - } - - [Theory, BitAutoData] - public async Task SendNewDeviceVerificationEmailAsync_NullDeviceTypeShouldSendUnkownBrowserType(User user) - { - var sutProvider = new SutProvider() - .CreateWithUserServiceCustomizations(user); - - var context = sutProvider.GetDependency(); - context.DeviceType = null; - context.IpAddress = "1.1.1.1"; - - await sutProvider.Sut.SendNewDeviceVerificationEmailAsync(user); - - await sutProvider.GetDependency() - .Received(1) - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), "Unknown Browser", Arg.Any()); - } - [Theory, BitAutoData] public async Task HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider sutProvider, User user) { @@ -577,78 +457,6 @@ public class UserServiceTests .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default); } - [Theory, BitAutoData] - public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled( - SutProvider sutProvider, string email, string secret) - { - sutProvider.GetDependency() - .GetByEmailAsync(email) - .Returns(null as User); - - await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret); - - await sutProvider.GetDependency() - .DidNotReceive() - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task ResendNewDeviceVerificationEmail_SecretNotValid_SendTwoFactorEmailAsyncNotCalled( - SutProvider sutProvider, string email, string secret) - { - sutProvider.GetDependency() - .GetByEmailAsync(email) - .Returns(null as User); - - await sutProvider.Sut.ResendNewDeviceVerificationEmail(email, secret); - - await sutProvider.GetDependency() - .DidNotReceive() - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - } - - [Theory, BitAutoData] - public async Task ResendNewDeviceVerificationEmail_SendsToken_Success(User user) - { - // Arrange - var testPassword = "test_password"; - SetupUserAndDevice(user, true); - - var sutProvider = new SutProvider() - .CreateWithUserServiceCustomizations(user); - - // Setup the fake password verification - sutProvider - .GetDependency>() - .GetPasswordHashAsync(user, Arg.Any()) - .Returns((ci) => - { - return Task.FromResult("hashed_test_password"); - }); - - sutProvider.GetDependency>() - .VerifyHashedPassword(user, "hashed_test_password", testPassword) - .Returns((ci) => - { - return PasswordVerificationResult.Success; - }); - - sutProvider.GetDependency() - .GetByEmailAsync(user.Email) - .Returns(user); - - var context = sutProvider.GetDependency(); - context.DeviceType = DeviceType.Android; - context.IpAddress = "1.1.1.1"; - - await sutProvider.Sut.ResendNewDeviceVerificationEmail(user.Email, testPassword); - - await sutProvider.GetDependency() - .Received(1) - .SendTwoFactorEmailAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); - - } - [Theory] [BitAutoData("")] [BitAutoData("null")] diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index 9e20e630cd..9058d26cf1 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -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 _logger; private readonly DeviceValidator _sut; @@ -39,6 +41,7 @@ public class DeviceValidatorTests _currentContext = Substitute.For(); _userService = Substitute.For(); _distributedCache = Substitute.For(); + _twoFactorEmailService = Substitute.For(); _logger = new Logger(Substitute.For()); _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()); Assert.False(result);