1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

Auth/PM-11969 - Registration with Email Verification - Accept Emergency Access Invite Flow (#4773)

* PM-11969 - Add new logic for registering a user via an AcceptEmergencyAccessInviteToken

* PM-11969 - Unit test new RegisterUserViaAcceptEmergencyAccessInviteToken method.

* PM-11969 - Integration test new method
This commit is contained in:
Jared Snider
2024-09-12 19:39:10 -04:00
committed by GitHub
parent 7d8df767cd
commit fd07de736d
6 changed files with 232 additions and 4 deletions

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
@ -449,7 +450,90 @@ public class RegisterUserCommandTests
var result = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, masterPasswordHash, orgSponsoredFreeFamilyPlanInviteToken));
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
}
// RegisterUserViaAcceptEmergencyAccessInviteToken
[Theory]
[BitAutoData]
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_Succeeds(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
emergencyAccess.Email = user.Email;
emergencyAccess.Id = acceptEmergencyAccessId;
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
.TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())
.Returns(callInfo =>
{
callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 10);
return true;
});
sutProvider.GetDependency<IUserService>()
.CreateUserAsync(user, masterPasswordHash)
.Returns(IdentityResult.Success);
// Act
var result = await sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId);
// Assert
Assert.True(result.Succeeded);
await sutProvider.GetDependency<IUserService>()
.Received(1)
.CreateUserAsync(Arg.Is<User>(u => u.Name == user.Name && u.EmailVerified == true && u.ApiKey != null), masterPasswordHash);
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendWelcomeEmailAsync(user);
await sutProvider.GetDependency<IReferenceEventService>()
.Received(1)
.RaiseEventAsync(Arg.Is<ReferenceEvent>(refEvent => refEvent.Type == ReferenceEventType.Signup));
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,
string masterPasswordHash, EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
emergencyAccess.Email = "wrong@email.com";
emergencyAccess.Id = acceptEmergencyAccessId;
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
.TryUnprotect(acceptEmergencyAccessInviteToken, out Arg.Any<EmergencyAccessInviteTokenable>())
.Returns(callInfo =>
{
callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 10);
return true;
});
// Act & Assert
var result = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId));
Assert.Equal("Invalid accept emergency access invite token.", result.Message);
}
[Theory]
[BitAutoData]
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_DisabledOpenRegistration_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,
string masterPasswordHash, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
sutProvider.GetDependency<IGlobalSettings>()
.DisableUserRegistration = true;
// Act & Assert
var result = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.RegisterUserViaAcceptEmergencyAccessInviteToken(user, masterPasswordHash, acceptEmergencyAccessInviteToken, acceptEmergencyAccessId));
Assert.Equal("Open registration has been disabled by the system administrator.", result.Message);
}

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Entities;
@ -400,6 +401,75 @@ public class AccountsControllerTests : IClassFixture<IdentityApplicationFactory>
Assert.Equal(kdfParallelism, user.KdfParallelism);
}
[Theory, BitAutoData]
public async Task RegistrationWithEmailVerification_WithAcceptEmergencyAccessInviteToken_Succeeds(
[StringLength(1000)] string masterPasswordHash, [StringLength(50)] string masterPasswordHint, string userSymmetricKey,
KeysRequestModel userAsymmetricKeys, int kdfMemory, int kdfParallelism, EmergencyAccess emergencyAccess)
{
// Localize factory to just this test.
var localFactory = new IdentityApplicationFactory();
// Hardcoded, valid data
var email = "jsnider+local79813655659549@bitwarden.com";
var acceptEmergencyAccessInviteToken = "CfDJ8HFsgwUNr89EtnCal5H72cwjvdjWmBp3J0ry7KoG6zDFub-EeoA3cfLBXONq7thKq7QTBh6KJ--jU0Det7t3P9EXqxmEacxIlgFlBgtywIUho9N8nVQeNcltkQO9g0vj_ASshnn6fWK3zpqS6Z8JueVZ2TMtdks5uc7DjZurWFLX27Dpii-UusFD78Z5tCY-D79bkjHy43g1ULk2F2ZtwiJvp3C9QvXW1-12IEsyHHSxU-9RELe-_joo2iDIR-cvMmEfbEXK7uvuzNT2V0r22jalaAKFvd84Gza9Q0YSFn8z_nAJxVqEXsAVKdG8SRN5Wa3K2mdNoBMt20RrzNuuJhe6vzX0yP35HtC4e1YXXzWB";
var acceptEmergencyAccessId = new Guid("8bc5e574-cef6-4ee7-b9ed-b1e90158c016");
emergencyAccess.Id = acceptEmergencyAccessId;
emergencyAccess.Email = email;
var emergencyAccessInviteTokenable = new EmergencyAccessInviteTokenable(emergencyAccess, 10) { };
localFactory.SubstituteService<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>(dataProtectorTokenFactory =>
{
dataProtectorTokenFactory.TryUnprotect(Arg.Is(acceptEmergencyAccessInviteToken), out Arg.Any<EmergencyAccessInviteTokenable>())
.Returns(callInfo =>
{
callInfo[1] = emergencyAccessInviteTokenable;
return true;
});
});
var registerFinishReqModel = new RegisterFinishRequestModel
{
Email = email,
MasterPasswordHash = masterPasswordHash,
MasterPasswordHint = masterPasswordHint,
AcceptEmergencyAccessInviteToken = acceptEmergencyAccessInviteToken,
AcceptEmergencyAccessId = acceptEmergencyAccessId,
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
UserSymmetricKey = userSymmetricKey,
UserAsymmetricKeys = userAsymmetricKeys,
KdfMemory = kdfMemory,
KdfParallelism = kdfParallelism
};
var postRegisterFinishHttpContext = await localFactory.PostRegisterFinishAsync(registerFinishReqModel);
Assert.Equal(StatusCodes.Status200OK, postRegisterFinishHttpContext.Response.StatusCode);
var database = localFactory.GetDatabaseContext();
var user = await database.Users
.SingleAsync(u => u.Email == email);
Assert.NotNull(user);
// Assert user properties match the request model
Assert.Equal(email, user.Email);
Assert.NotEqual(masterPasswordHash, user.MasterPassword); // We execute server side hashing
Assert.NotNull(user.MasterPassword);
Assert.Equal(masterPasswordHint, user.MasterPasswordHint);
Assert.Equal(userSymmetricKey, user.Key);
Assert.Equal(userAsymmetricKeys.EncryptedPrivateKey, user.PrivateKey);
Assert.Equal(userAsymmetricKeys.PublicKey, user.PublicKey);
Assert.Equal(KdfType.PBKDF2_SHA256, user.Kdf);
Assert.Equal(AuthConstants.PBKDF2_ITERATIONS.Default, user.KdfIterations);
Assert.Equal(kdfMemory, user.KdfMemory);
Assert.Equal(kdfParallelism, user.KdfParallelism);
}
[Theory, BitAutoData]
public async Task PostRegisterVerificationEmailClicked_Success(