1
0
mirror of https://github.com/bitwarden/server.git synced 2025-03-11 13:19:40 +01:00

[PM-12512] Add Endpoint to allow users to request a new device otp (#5146)

feat(NewDeviceVerification): Added a resend new device OTP endpoint and method for the IUserService as well as wrote test for new methods for the user service.
This commit is contained in:
Ike 2024-12-16 07:57:56 -08:00 committed by GitHub
parent 8994d1d7dd
commit c446ac86fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 38 deletions

View File

@ -961,6 +961,14 @@ public class AccountsController : Controller
}
}
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
[AllowAnonymous]
[HttpPost("resend-new-device-otp")]
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificatioRequestModel request)
{
await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret);
}
private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId)
{
var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId);

View File

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
namespace Bit.Api.Auth.Models.Request.Accounts;
public class UnauthenticatedSecretVerificatioRequestModel : SecretVerificationRequestModel
{
[Required]
[StrictEmailAddress]
[StringLength(256)]
public string Email { get; set; }
}

View File

@ -76,7 +76,7 @@ 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);
void SetTwoFactorProvider(User user, TwoFactorProviderType type, bool setEnabled = true);

View File

@ -1407,7 +1407,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
public async Task SendOTPAsync(User user)
{
if (user.Email == null)
if (string.IsNullOrEmpty(user.Email))
{
throw new BadRequestException("No user email.");
}
@ -1450,6 +1450,20 @@ public class UserService : UserManager<User>, IUserService, IDisposable
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 SendOTPAsync(user);
}
}
private async Task SendAppropriateWelcomeEmailAsync(User user, string initiationPath)
{
var isFromMarketingWebsite = initiationPath.Contains("Secrets Manager trial");

View File

@ -13,6 +13,7 @@ using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -240,42 +241,7 @@ public class UserServiceTests
});
// HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured
var sut = new UserService(
sutProvider.GetDependency<IUserRepository>(),
sutProvider.GetDependency<ICipherRepository>(),
sutProvider.GetDependency<IOrganizationUserRepository>(),
sutProvider.GetDependency<IOrganizationRepository>(),
sutProvider.GetDependency<IMailService>(),
sutProvider.GetDependency<IPushNotificationService>(),
sutProvider.GetDependency<IUserStore<User>>(),
sutProvider.GetDependency<IOptions<IdentityOptions>>(),
sutProvider.GetDependency<IPasswordHasher<User>>(),
sutProvider.GetDependency<IEnumerable<IUserValidator<User>>>(),
sutProvider.GetDependency<IEnumerable<IPasswordValidator<User>>>(),
sutProvider.GetDependency<ILookupNormalizer>(),
sutProvider.GetDependency<IdentityErrorDescriber>(),
sutProvider.GetDependency<IServiceProvider>(),
sutProvider.GetDependency<ILogger<UserManager<User>>>(),
sutProvider.GetDependency<ILicensingService>(),
sutProvider.GetDependency<IEventService>(),
sutProvider.GetDependency<IApplicationCacheService>(),
sutProvider.GetDependency<IDataProtectionProvider>(),
sutProvider.GetDependency<IPaymentService>(),
sutProvider.GetDependency<IPolicyRepository>(),
sutProvider.GetDependency<IPolicyService>(),
sutProvider.GetDependency<IReferenceEventService>(),
sutProvider.GetDependency<IFido2>(),
sutProvider.GetDependency<ICurrentContext>(),
sutProvider.GetDependency<IGlobalSettings>(),
sutProvider.GetDependency<IAcceptOrgUserCommand>(),
sutProvider.GetDependency<IProviderUserRepository>(),
sutProvider.GetDependency<IStripeSyncService>(),
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
sutProvider.GetDependency<IFeatureService>(),
sutProvider.GetDependency<IPremiumUserBillingService>(),
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
);
var sut = RebuildSut(sutProvider);
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
@ -522,6 +488,99 @@ public class UserServiceTests
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization2.DisplayName(), user.Email);
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_UserNull_SendOTPAsyncNotCalled(
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()
.SendOTPEmailAsync(Arg.Any<string>(), Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_SecretNotValid_SendOTPAsyncNotCalled(
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()
.SendOTPEmailAsync(Arg.Any<string>(), Arg.Any<string>());
}
[Theory, BitAutoData]
public async Task ResendNewDeviceVerificationEmail_SendsToken_Success(
SutProvider<UserService> sutProvider, User user)
{
// Arrange
var testPassword = "test_password";
var tokenProvider = SetupFakeTokenProvider(sutProvider, user);
SetupUserAndDevice(user, true);
// Setup the fake password verification
var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>();
substitutedUserPasswordStore
.GetPasswordHashAsync(user, Arg.Any<CancellationToken>())
.Returns((ci) =>
{
return Task.FromResult("hashed_test_password");
});
sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore, "store");
sutProvider.GetDependency<IPasswordHasher<User>>("passwordHasher")
.VerifyHashedPassword(user, "hashed_test_password", testPassword)
.Returns((ci) =>
{
return PasswordVerificationResult.Success;
});
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(user.Email)
.Returns(user);
// HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured
var sut = RebuildSut(sutProvider);
await sut.ResendNewDeviceVerificationEmail(user.Email, testPassword);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendOTPEmailAsync(user.Email, Arg.Any<string>());
}
[Theory]
[BitAutoData("")]
[BitAutoData("null")]
public async Task SendOTPAsync_UserEmailNull_ThrowsBadRequest(
string email,
SutProvider<UserService> sutProvider, User user)
{
user.Email = email == "null" ? null : "";
var expectedMessage = "No user email.";
try
{
await sutProvider.Sut.SendOTPAsync(user);
}
catch (BadRequestException ex)
{
Assert.Equal(ex.Message, expectedMessage);
await sutProvider.GetDependency<IMailService>()
.DidNotReceive()
.SendOTPEmailAsync(Arg.Any<string>(), Arg.Any<string>());
}
}
private static void SetupUserAndDevice(User user,
bool shouldHavePassword)
{
@ -573,4 +632,44 @@ public class UserServiceTests
return fakeUserTwoFactorProvider;
}
private IUserService RebuildSut(SutProvider<UserService> sutProvider)
{
return new UserService(
sutProvider.GetDependency<IUserRepository>(),
sutProvider.GetDependency<ICipherRepository>(),
sutProvider.GetDependency<IOrganizationUserRepository>(),
sutProvider.GetDependency<IOrganizationRepository>(),
sutProvider.GetDependency<IMailService>(),
sutProvider.GetDependency<IPushNotificationService>(),
sutProvider.GetDependency<IUserStore<User>>(),
sutProvider.GetDependency<IOptions<IdentityOptions>>(),
sutProvider.GetDependency<IPasswordHasher<User>>(),
sutProvider.GetDependency<IEnumerable<IUserValidator<User>>>(),
sutProvider.GetDependency<IEnumerable<IPasswordValidator<User>>>(),
sutProvider.GetDependency<ILookupNormalizer>(),
sutProvider.GetDependency<IdentityErrorDescriber>(),
sutProvider.GetDependency<IServiceProvider>(),
sutProvider.GetDependency<ILogger<UserManager<User>>>(),
sutProvider.GetDependency<ILicensingService>(),
sutProvider.GetDependency<IEventService>(),
sutProvider.GetDependency<IApplicationCacheService>(),
sutProvider.GetDependency<IDataProtectionProvider>(),
sutProvider.GetDependency<IPaymentService>(),
sutProvider.GetDependency<IPolicyRepository>(),
sutProvider.GetDependency<IPolicyService>(),
sutProvider.GetDependency<IReferenceEventService>(),
sutProvider.GetDependency<IFido2>(),
sutProvider.GetDependency<ICurrentContext>(),
sutProvider.GetDependency<IGlobalSettings>(),
sutProvider.GetDependency<IAcceptOrgUserCommand>(),
sutProvider.GetDependency<IProviderUserRepository>(),
sutProvider.GetDependency<IStripeSyncService>(),
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
sutProvider.GetDependency<IFeatureService>(),
sutProvider.GetDependency<IPremiumUserBillingService>(),
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>()
);
}
}