-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(register): [PM-27084] Account Register Uses New Data Types #6715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat(register): [PM-27084] Account Register Uses New Data Types #6715
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #6715 +/- ##
==========================================
+ Coverage 54.94% 58.85% +3.91%
==========================================
Files 1927 1927
Lines 85480 85627 +147
Branches 7653 7677 +24
==========================================
+ Hits 46963 50396 +3433
+ Misses 36729 33346 -3383
- Partials 1788 1885 +97 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
New Issues (7)Checkmarx found the following issues in this Pull Request
|
…ed up reference to master password hash
…d more comments and fixed up some long lines.
…unts controller no longer nullish allowed.
…e thrown error messages more appropriate
|
Adding @eligrubb to review because he has work he is branching off of this to do! |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
|
|
||
| // 1. Access token presence verification check | ||
| switch (GetTokenType()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GetTokenType() method (called at line 135) throws InvalidOperationException when no valid token is found (line 117). This violates the IValidatableObject.Validate() contract, which should return ValidationResult objects, not throw exceptions.
Problem:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
switch (GetTokenType()) // Line 135 - can throw InvalidOperationException
{
// ...
}
}If GetTokenType() throws, the exception bypasses ASP.NET Core's validation error handling and gets caught by the exception filter instead, resulting in a generic 500 error rather than a proper 400 validation error.
Fix: Wrap the call or refactor to return validation results:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
RegisterFinishTokenType tokenType;
try
{
tokenType = GetTokenType();
}
catch (InvalidOperationException)
{
yield return new ValidationResult("No valid registration token provided");
yield break;
}
// Check authentication/hash consistency before token validation
if (MasterPasswordAuthentication != null && MasterPasswordHash != null)
{
// ... existing check
}
switch (tokenType)
{
// ... rest of validation
}
}| // PM-28143 - Just use the MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash | ||
| string masterPasswordHash = model.MasterPasswordAuthentication?.MasterPasswordAuthenticationHash | ||
| ?? model.MasterPasswordHash ?? throw new BadRequestException("MasterPasswordHash couldn't be found on either the MasterPasswordAuthenticationData or the MasterPasswordHash property passed in."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The master password hash is extracted here with a null-coalescing throw pattern, but this check should happen during model validation, not in the controller action.
Problem:
model.Validate()passes (ASP.NET Core doesn't report errors)- Controller calls
model.ToUser()(passes if fields present) - Controller extracts
masterPasswordHash(line 150) - throws here if both are null - Exception caught and converted to 400 response
Why this is problematic:
- Validation timing inconsistency: Some validation in
Validate(), some in controller, some inToUser() - The error message "MasterPasswordHash couldn't be found on either..." reveals internal API structure
- Attackers can probe which combinations of fields are accepted
Security concern - Information disclosure:
The error message reveals:
- Property names:
MasterPasswordAuthenticationData,MasterPasswordHash - API structure during migration period
- Which fields are checked and in what order
Fix: Move this validation into RegisterFinishRequestModel.Validate():
// In RegisterFinishRequestModel.Validate(), before token checks:
if (MasterPasswordAuthentication == null && MasterPasswordHash == null)
{
yield return new ValidationResult(
"Master password hash is required.",
[nameof(MasterPasswordAuthentication), nameof(MasterPasswordHash)]);
}Then in controller, this becomes a simple assertion:
string masterPasswordHash = model.MasterPasswordAuthentication?.MasterPasswordAuthenticationHash
?? model.MasterPasswordHash!; // Safe: validated above| identityResult = await _registerUserCommand.RegisterUserViaEmailVerificationToken( | ||
| user, | ||
| masterPasswordHash, | ||
| model.EmailVerificationToken!); | ||
| return ProcessRegistrationResult(identityResult, user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎨 SUGGESTED: Force unwraps rely on validation that can be bypassed
The null-forgiving operators (!) on lines 159, 167, 174, 181, and 190 assume that validation has ensured these values are non-null. However, validation can be incomplete or bypassed in several scenarios:
Current risk:
- If validation has gaps (see other comments about incomplete validation)
- If [ApiController] attribute is not present (automatic validation disabled)
- If validation is explicitly bypassed in tests or other contexts
Evidence of risk:
The validation for tokens only checks if they're null/empty in the switch statement (lines 137-192 in RegisterFinishRequestModel), but these checks only run if GetTokenType() succeeds. If token detection fails, validation may not run.
Suggestion: Use explicit null checks with appropriate error handling:
case RegisterFinishTokenType.EmailVerification:
if (string.IsNullOrEmpty(model.EmailVerificationToken))
{
throw new BadRequestException("Email verification token is required");
}
identityResult = await _registerUserCommand.RegisterUserViaEmailVerificationToken(
user,
masterPasswordHash,
model.EmailVerificationToken);
return ProcessRegistrationResult(identityResult, user);This provides defense in depth and makes the code's assumptions explicit, reducing risk of NullReferenceException if validation is bypassed.
…d more validation around the master password kdf.
|
|
||
|
|
||
| // 1. Access token presence verification check | ||
| switch (GetTokenType()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ CRITICAL: Validate() violates IValidatableObject contract by allowing GetTokenType() to throw exceptions
Details and fix
Problem:
GetTokenType() can throw InvalidOperationException (line 117) when no valid token is present. The ASP.NET validation framework does not catch exceptions from IValidatableObject.Validate() - they propagate as unhandled exceptions, bypassing validation entirely and returning 500 errors instead of 400 validation errors.
Execution path:
- Client submits request with invalid token combination
- Model binding triggers
Validate() - Line 135 calls
GetTokenType() - No valid token type matches → line 117 throws
InvalidOperationException - Exception bypasses validation framework → 500 Internal Server Error
Fix:
Wrap the GetTokenType() call in try-catch and convert exceptions to ValidationResults:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Validate token type without throwing
RegisterFinishTokenType tokenType;
try
{
tokenType = GetTokenType();
}
catch (InvalidOperationException)
{
yield return new ValidationResult("Invalid registration request: no valid token provided");
yield break;
}
// Rest of validation using tokenType...Impact: Without this fix, invalid registration requests crash with 500 errors instead of returning proper 400 validation errors, breaking client error handling and making debugging difficult.
Reference: IValidatableObject documentation
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
…fled around validation a little. In a great place now.
…ved unused import.
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs
Outdated
Show resolved
Hide resolved
…ved unused import.
| IdentityResult? identityResult = null; | ||
|
|
||
| // PM-28143 - Just use the MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash | ||
| string masterPasswordHash = model.MasterPasswordAuthentication?.MasterPasswordAuthenticationHash |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename to "masterPasswordAuthenticationHash", or just use the MasterPasswordAuthentication struct all the way through. Even if it is not on the request model, you can still construct it from the email and kdf that are on the request model.
| // Always force a valid encrypted string for tests to avoid model validation failures. | ||
| var masterKeyWrappedUserKey = DefaultEncryptedString; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This requires fixing the tests that use this function.
…ed validation tests and ToUser no longer throws bad request.


🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-27084
📔 Objective
📸 Screenshots
Screen.Recording.2025-12-11.at.5.21.28.PM.mov
⏰ Reminders before review
🦮 Reviewer guidelines
:+1:) or similar for great changes:memo:) or ℹ️ (:information_source:) for notes or general info:question:) for questions:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion:art:) for suggestions / improvements:x:) or:warning:) for more significant problems or concerns needing attention:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt:pick:) for minor or nitpick changes