1
0
mirror of https://github.com/bitwarden/server synced 2026-01-26 14:23:21 +00:00

fix(register): [PM-27084] Account Register Uses New Data Types - Converted throws to validation results and updated tests.

This commit is contained in:
Patrick Pimentel
2026-01-02 11:54:14 -05:00
parent 260b289b64
commit 06bf7b82cc
3 changed files with 76 additions and 41 deletions

View File

@@ -119,83 +119,110 @@ public class RegisterFinishRequestModel : IValidatableObject
public IEnumerable<ValidationResult> 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<ValidationResult> 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;
}
}
}
}

View File

@@ -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);
}

View File

@@ -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<BadRequestException>(() => 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)