From c21d7b43df211501ac9a39100bd00b737384adbe Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Tue, 16 Dec 2025 10:38:15 -0500 Subject: [PATCH] test(register): [PM-27084] Account Register Uses New Data Types - Test additions to make sure properties that were once required are still required. --- .../Accounts/RegisterFinishRequestModel.cs | 10 +- .../Controllers/AccountsControllerTests.cs | 219 ++++++++++++++++++ 2 files changed, 226 insertions(+), 3 deletions(-) diff --git a/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs b/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs index b185f8db3c..1fa78390fd 100644 --- a/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs @@ -24,14 +24,18 @@ public class RegisterFinishRequestModel : IValidatableObject public MasterPasswordAuthenticationData? MasterPasswordAuthenticationData { get; set; } public MasterPasswordUnlockData? MasterPasswordUnlockData { get; set; } - // PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData) + // PM-28143 - Remove property below (made optional during migration to MasterPasswordUnlockData) [StringLength(1000)] + // Made optional but there will still be a thrown error if it does not exist either here or + // in the MasterPasswordAuthenticationData. public string? MasterPasswordHash { get; set; } [StringLength(50)] public string? MasterPasswordHint { get; set; } - // PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData) + // PM-28143 - Remove property below (made optional during migration to MasterPasswordUnlockData) + // Made optional but there will still be a thrown error if it does not exist either here or + // in the MasterPasswordAuthenticationData. public string? UserSymmetricKey { get; set; } public required KeysRequestModel UserAsymmetricKeys { get; set; } @@ -70,7 +74,7 @@ public class RegisterFinishRequestModel : IValidatableObject KdfParallelism = MasterPasswordUnlockData?.Kdf.Parallelism ?? KdfParallelism, // PM-28827 To be added when MasterPasswordSalt is added to the user column // MasterPasswordSalt = MasterPasswordUnlockData?.Salt ?? Email.ToLower().Trim(), - Key = MasterPasswordUnlockData?.MasterKeyWrappedUserKey ?? UserSymmetricKey, + Key = MasterPasswordUnlockData?.MasterKeyWrappedUserKey ?? UserSymmetricKey ?? throw new Exception("MasterKeyWrappedUserKey couldn't be found on either the MasterPasswordUnlockData or the UserSymmetricKey property passed in."), }; UserAsymmetricKeys.ToUser(user); diff --git a/test/Identity.Test/Controllers/AccountsControllerTests.cs b/test/Identity.Test/Controllers/AccountsControllerTests.cs index 8d37ee10d7..468fed55cd 100644 --- a/test/Identity.Test/Controllers/AccountsControllerTests.cs +++ b/test/Identity.Test/Controllers/AccountsControllerTests.cs @@ -821,6 +821,225 @@ public class AccountsControllerTests : IDisposable organizationUserId); } + [Theory, BitAutoData] + public async Task PostRegisterFinish_NewForm_UsesUnlockDataForKdfAndKey_WhenRootFieldsNull( + string email, + string emailVerificationToken, + string masterPasswordHash, + string masterKeyWrappedUserKey, + int iterations, + string publicKey, + string encryptedPrivateKey) + { + // Arrange: Provide only unlock-data KDF + key; leave root KDF fields null + var unlockKdf = new KdfSettings + { + KdfType = KdfType.PBKDF2_SHA256, + Iterations = iterations + }; + + var model = new RegisterFinishRequestModel + { + Email = email, + EmailVerificationToken = emailVerificationToken, + MasterPasswordAuthenticationData = new MasterPasswordAuthenticationData + { + // present but not used by ToUser for KDF/Key + Kdf = new KdfSettings { KdfType = KdfType.Argon2id, Iterations = iterations }, + MasterPasswordAuthenticationHash = masterPasswordHash, + Salt = email + }, + MasterPasswordUnlockData = new MasterPasswordUnlockData + { + Kdf = unlockKdf, + MasterKeyWrappedUserKey = masterKeyWrappedUserKey, + Salt = email + }, + // root KDF fields intentionally null + Kdf = null, + KdfIterations = null, + UserAsymmetricKeys = new KeysRequestModel + { + PublicKey = publicKey, + EncryptedPrivateKey = encryptedPrivateKey + } + }; + + _registerUserCommand + .RegisterUserViaEmailVerificationToken(Arg.Any(), masterPasswordHash, emailVerificationToken) + .Returns(Task.FromResult(IdentityResult.Success)); + + // Act + var _ = await _sut.PostRegisterFinish(model); + + // Assert: The user passed to command uses unlock-data values + await _registerUserCommand.Received(1).RegisterUserViaEmailVerificationToken( + Arg.Is(u => + u.Email == email && + u.Kdf == unlockKdf.KdfType && + u.KdfIterations == unlockKdf.Iterations && + u.Key == masterKeyWrappedUserKey), + masterPasswordHash, + emailVerificationToken); + } + + [Theory, BitAutoData] + public async Task PostRegisterFinish_LegacyForm_UsesRootFields_WhenUnlockDataNull( + string email, + string emailVerificationToken, + string masterPasswordHash, + string legacyKey, + string publicKey, + string encryptedPrivateKey) + { + // Arrange: Provide only legacy root KDF + key; no unlock-data provided + var model = new RegisterFinishRequestModel + { + Email = email, + EmailVerificationToken = emailVerificationToken, + MasterPasswordHash = masterPasswordHash, + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default, + UserSymmetricKey = legacyKey, + MasterPasswordUnlockData = null, + UserAsymmetricKeys = new KeysRequestModel + { + PublicKey = publicKey, + EncryptedPrivateKey = encryptedPrivateKey + } + }; + + _registerUserCommand + .RegisterUserViaEmailVerificationToken(Arg.Any(), masterPasswordHash, emailVerificationToken) + .Returns(Task.FromResult(IdentityResult.Success)); + + // Act + var _ = await _sut.PostRegisterFinish(model); + + // Assert: The user passed to command uses root values + await _registerUserCommand.Received(1).RegisterUserViaEmailVerificationToken( + Arg.Is(u => + u.Email == email && + u.Kdf == KdfType.PBKDF2_SHA256 && + u.KdfIterations == AuthConstants.PBKDF2_ITERATIONS.Default && + u.Key == legacyKey), + masterPasswordHash, + emailVerificationToken); + } + + [Theory, BitAutoData] + public async Task PostRegisterFinish_WhenKdfMissingInAllSources_ShouldReturnBadRequest( + string email, + string emailVerificationToken, + string masterPasswordHash, + string masterKeyWrappedUserKey, + int iterations, + string publicKey, + string encryptedPrivateKey) + { + // Arrange: No KDF at root, and no unlock-data present + var model = new RegisterFinishRequestModel + { + Email = email, + EmailVerificationToken = emailVerificationToken, + MasterPasswordAuthenticationData = new MasterPasswordAuthenticationData + { + // present but ToUser does not source KDF from here + Kdf = new KdfSettings { KdfType = KdfType.Argon2id, Iterations = iterations }, + MasterPasswordAuthenticationHash = masterPasswordHash, + Salt = email + }, + MasterPasswordUnlockData = null, + Kdf = null, + KdfIterations = iterations, + UserSymmetricKey = masterKeyWrappedUserKey, + UserAsymmetricKeys = new KeysRequestModel + { + PublicKey = publicKey, + EncryptedPrivateKey = encryptedPrivateKey + } + }; + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _sut.PostRegisterFinish(model)); + Assert.Equal("KdfType couldn't be found on either the MasterPasswordUnlockData or the Kdf property passed in.", ex.Message); + } + + [Theory, BitAutoData] + public async Task PostRegisterFinish_WhenKdfIterationsMissingInAllSources_ShouldReturnBadRequest( + string email, + string emailVerificationToken, + string masterPasswordHash, + string masterKeyWrappedUserKey, + KdfType kdfType, + string publicKey, + string encryptedPrivateKey) + { + // Arrange: No KdfIterations at root, and no unlock-data present + var model = new RegisterFinishRequestModel + { + Email = email, + EmailVerificationToken = emailVerificationToken, + MasterPasswordAuthenticationData = new MasterPasswordAuthenticationData + { + // present but ToUser does not source iterations from here + Kdf = new KdfSettings { KdfType = kdfType, Iterations = AuthConstants.PBKDF2_ITERATIONS.Default }, + MasterPasswordAuthenticationHash = masterPasswordHash, + Salt = email + }, + MasterPasswordUnlockData = null, + Kdf = kdfType, + KdfIterations = null, + UserSymmetricKey = masterKeyWrappedUserKey, + UserAsymmetricKeys = new KeysRequestModel + { + PublicKey = publicKey, + EncryptedPrivateKey = encryptedPrivateKey + } + }; + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _sut.PostRegisterFinish(model)); + Assert.Equal("KdfIterations couldn't be found on either the MasterPasswordUnlockData or the KdfIterations property passed in.", ex.Message); + } + + [Theory, BitAutoData] + public async Task PostRegisterFinish_WhenKeyMissingInAllSources_ShouldReturnBadRequest( + string email, + string emailVerificationToken, + string masterPasswordHash, + int iterations, + KdfType kdfType, + string publicKey, + string encryptedPrivateKey) + { + // Arrange: No key at root, and no unlock-data present + var model = new RegisterFinishRequestModel + { + Email = email, + EmailVerificationToken = emailVerificationToken, + MasterPasswordAuthenticationData = new MasterPasswordAuthenticationData + { + Kdf = new KdfSettings { KdfType = kdfType, Iterations = iterations }, + MasterPasswordAuthenticationHash = masterPasswordHash, + Salt = email + }, + MasterPasswordUnlockData = null, + Kdf = kdfType, + KdfIterations = iterations, + UserSymmetricKey = null, + UserAsymmetricKeys = new KeysRequestModel + { + PublicKey = publicKey, + EncryptedPrivateKey = encryptedPrivateKey + } + }; + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _sut.PostRegisterFinish(model)); + Assert.Equal("MasterKeyWrappedUserKey couldn't be found on either the MasterPasswordUnlockData or the UserSymmetricKey property passed in.", ex.Message); + } + private void SetDefaultKdfHmacKey(byte[]? newKey) { var fieldInfo = typeof(AccountsController).GetField("_defaultKdfHmacKey", BindingFlags.NonPublic | BindingFlags.Instance);