From 06bf7b82cc5fb8617e30bcdf9bbb5e2398e7bcf0 Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Fri, 2 Jan 2026 11:54:14 -0500 Subject: [PATCH] fix(register): [PM-27084] Account Register Uses New Data Types - Converted throws to validation results and updated tests. --- .../Accounts/RegisterFinishRequestModel.cs | 93 ++++++++++++------- .../Controllers/AccountsController.cs | 9 +- .../Controllers/AccountsControllerTests.cs | 15 ++- 3 files changed, 76 insertions(+), 41 deletions(-) diff --git a/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs b/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs index 30dc8a141a..430ef04307 100644 --- a/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs @@ -119,83 +119,110 @@ public class RegisterFinishRequestModel : IValidatableObject public IEnumerable Validate(ValidationContext validationContext) { - // PM-28143 - Remove this check - ThrowIfExistsAndHashIsNotEqual(MasterPasswordAuthentication, MasterPasswordHash); + // 1. Authentication data containing hash and hash at root level check + if (MasterPasswordAuthentication != null && MasterPasswordHash != null) + { + if (MasterPasswordAuthentication.MasterPasswordAuthenticationHash != MasterPasswordHash) + { + yield return new ValidationResult( + $"{nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash)} and root level {nameof(MasterPasswordHash)} provided and are not equal. Only provide one.", + [nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash), nameof(MasterPasswordHash)]); + } + } - // 1. Access Token Presence Verification Check + + // 1. Access token presence verification check switch (GetTokenType()) { case RegisterFinishTokenType.EmailVerification: if (string.IsNullOrEmpty(EmailVerificationToken)) { - throw new BadRequestException("Email verification token absent when processing register/finish."); + yield return new ValidationResult( + $"{nameof(EmailVerificationToken)} absent when processing register/finish.", + [nameof(EmailVerificationToken)]); } break; case RegisterFinishTokenType.OrganizationInvite: if (string.IsNullOrEmpty(OrgInviteToken)) { - throw new BadRequestException("Organization invite token absent when processing register/finish."); + yield return new ValidationResult( + $"{nameof(OrgInviteToken)} absent when processing register/finish.", + [nameof(OrgInviteToken)]); } break; case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan: if (string.IsNullOrEmpty(OrgSponsoredFreeFamilyPlanToken)) { - throw new BadRequestException("Organization sponsored free family plan token absent when processing register/finish."); + yield return new ValidationResult( + $"{nameof(OrgSponsoredFreeFamilyPlanToken)} absent when processing register/finish.", + [nameof(OrgSponsoredFreeFamilyPlanToken)]); } break; case RegisterFinishTokenType.EmergencyAccessInvite: if (string.IsNullOrEmpty(AcceptEmergencyAccessInviteToken)) { - throw new BadRequestException("Accept emergency access invite token absent when processing register/finish."); + yield return new ValidationResult( + $"{nameof(AcceptEmergencyAccessInviteToken)} absent when processing register/finish.", + [nameof(AcceptEmergencyAccessInviteToken)]); } if (!AcceptEmergencyAccessId.HasValue || AcceptEmergencyAccessId.Value == Guid.Empty) { - throw new BadRequestException("Accept emergency access id absent when processing register/finish."); + yield return new ValidationResult( + $"{nameof(AcceptEmergencyAccessId)} absent when processing register/finish.", + [nameof(AcceptEmergencyAccessId)]); } break; case RegisterFinishTokenType.ProviderInvite: if (string.IsNullOrEmpty(ProviderInviteToken)) { - throw new BadRequestException("Provider invite token absent when processing register/finish."); + yield return new ValidationResult( + $"{nameof(ProviderInviteToken)} absent when processing register/finish.", + [nameof(ProviderInviteToken)]); } if (!ProviderUserId.HasValue || ProviderUserId.Value == Guid.Empty) { - throw new BadRequestException("Provider user id absent when processing register/finish."); + yield return new ValidationResult( + $"{nameof(ProviderUserId)} absent when processing register/finish.", + [nameof(ProviderUserId)]); } break; default: - throw new BadRequestException("Invalid registration finish request"); + yield return new ValidationResult("Invalid registration finish request"); + break; } // 2. Validate kdf settings. - - IEnumerable kdfValidationResults; if (MasterPasswordUnlock != null && MasterPasswordAuthentication != null) { - kdfValidationResults = KdfSettingsValidator.Validate(MasterPasswordUnlock.ToData().Kdf); + foreach (var validationResult in KdfSettingsValidator.Validate(MasterPasswordUnlock.ToData().Kdf)) + { + yield return validationResult; + } } else { - kdfValidationResults = KdfSettingsValidator.Validate( - Kdf ?? throw new BadRequestException($"{nameof(Kdf)} not found on RequestModel"), - KdfIterations ?? throw new BadRequestException($"{nameof(KdfIterations)} not found on RequestModel"), - KdfMemory, - KdfParallelism); - } - - return kdfValidationResults; - } - - // PM-28143 - Remove function - private static void ThrowIfExistsAndHashIsNotEqual( - MasterPasswordAuthenticationDataRequestModel? authenticationData, - string? hash) - { - if (authenticationData != null && hash != null) - { - if (authenticationData.MasterPasswordAuthenticationHash != hash) + var hasMissingRequiredKdfInputs = false; + if (Kdf == null) { - throw new BadRequestException("AuthenticationData MasterPasswordHash and root level MasterPasswordHash provided and are not equal. Only provide one."); + yield return new ValidationResult($"{nameof(Kdf)} not found on RequestModel", [nameof(Kdf)]); + hasMissingRequiredKdfInputs = true; + } + if (KdfIterations == null) + { + yield return new ValidationResult($"{nameof(KdfIterations)} not found on RequestModel", [nameof(KdfIterations)]); + hasMissingRequiredKdfInputs = true; + } + + if (!hasMissingRequiredKdfInputs) + { + foreach (var validationResult in KdfSettingsValidator.Validate( + Kdf!.Value, + KdfIterations!.Value, + KdfMemory, + KdfParallelism)) + { + yield return validationResult; + } } } } diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index f80274d65d..2e42b690dd 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -202,11 +202,10 @@ public class AccountsController : Controller return new RegisterFinishResponseModel(); } - if (result.Errors != null) - foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName")) - { - ModelState.AddModelError(string.Empty, error.Description); - } + foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName")) + { + ModelState.AddModelError(string.Empty, error.Description); + } throw new BadRequestException(ModelState); } diff --git a/test/Identity.Test/Controllers/AccountsControllerTests.cs b/test/Identity.Test/Controllers/AccountsControllerTests.cs index 90ef917cf6..6a619842d1 100644 --- a/test/Identity.Test/Controllers/AccountsControllerTests.cs +++ b/test/Identity.Test/Controllers/AccountsControllerTests.cs @@ -1187,11 +1187,20 @@ public class AccountsControllerTests : IDisposable } }; + // Provide a minimal valid token type to satisfy model-level token validation + model.EmailVerificationToken = "test-token"; + var ctx = new ValidationContext(model); - // Act & Assert - var ex = Assert.Throws(() => model.Validate(ctx).ToList()); - Assert.Equal("AuthenticationData MasterPasswordHash and root level MasterPasswordHash provided and are not equal. Only provide one.", ex.Message); + // Act + var results = model.Validate(ctx).ToList(); + + // Assert: validation result exists with expected message and member names + var mismatchResult = Assert.Single(results.Where(r => + r.ErrorMessage == + "MasterPasswordAuthenticationHash and root level MasterPasswordHash provided and are not equal. Only provide one.")); + Assert.Contains("MasterPasswordAuthenticationHash", mismatchResult.MemberNames); + Assert.Contains("MasterPasswordHash", mismatchResult.MemberNames); } private void SetDefaultKdfHmacKey(byte[]? newKey)