bitwarden/server · error · BadRequestException
NewMasterPasswordHash and Key are required.
Error message
NewMasterPasswordHash and Key are required.
What it means
Thrown as a 400 BadRequestException(ModelState) from the legacy path of POST accounts/email when model.NewMasterPasswordHash or model.Key is null/empty. These fields are optional on the EmailRequestModel, but the legacy email-change flow requires both a new master-password hash and a wrapped user key, so the controller enforces them before calling ChangeEmailAsync. The ModelState error description is 'NewMasterPasswordHash and Key are required.'
Source
Thrown at src/Api/Auth/Controllers/AccountsController.cs:181
// check and keep only the SelfServiceChangeEmailCommand call.
if (_featureService.IsEnabled(FeatureFlagKeys.PM30806_SelfServiceChangeEmailCommand))
{
await _selfServiceChangeEmailCommand.ChangeEmailAsync(
user, model.MasterPasswordHash, model.NewEmail, model.Token);
return;
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("You cannot change your email when using Key Connector.");
}
// Legacy path still rotates the master password and wrapped user key alongside the
// email change; those fields are optional on the model so we have to enforce them here.
if (string.IsNullOrEmpty(model.NewMasterPasswordHash) || string.IsNullOrEmpty(model.Key))
{
ModelState.AddModelError(string.Empty, "NewMasterPasswordHash and Key are required.");
throw new BadRequestException(ModelState);
}
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
model.NewMasterPasswordHash, model.Token, model.Key);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
View on GitHub (pinned to e93b962371)
Solutions
- Send both NewMasterPasswordHash (the new master-password hash) and Key (the user key wrapped for the new master password) in the request body on the legacy path.
- Enable the PM30806_SelfServiceChangeEmailCommand flag so the client can use the flow that does not require these legacy fields.
- Verify the content-type is JSON and field names/casing match the EmailRequestModel so binding populates both fields.
Example fix
// before: legacy fields omitted
await api.post('accounts/email', { newEmail, masterPasswordHash, token });
// after: include the legacy required fields
await api.post('accounts/email', {
newEmail, masterPasswordHash, token,
newMasterPasswordHash: newHash,
key: wrappedKey,
}); Defensive patterns
Strategy: validation
Validate before calling
function legacyEmailBodyComplete(b) {
return !!b && !!b.newMasterPasswordHash && !!b.key;
} Type guard
function isLegacyEmailRequest(v): v is { newMasterPasswordHash: string; key: string; token: string; newEmail: string; masterPasswordHash: string } {
return !!v && typeof v.newMasterPasswordHash === 'string' && typeof v.key === 'string';
} Prevention
- Send NewMasterPasswordHash and Key on the legacy email-change path.
- Verify JSON content-type and field casing so binding populates both fields.
- Enable the self-service flag to avoid the legacy required fields.
When it happens
Trigger: A non-Key Connector user (flag OFF) calls POST /accounts/email with UsesKeyConnector false but omits NewMasterPasswordHash or Key from the legacy request body. The missing required field triggers the precondition throw.
Common situations: Client built for the newer self-service flow omits the legacy-only fields while hitting a server with the flag still off; partial form submission; model binding dropped the fields due to casing/content-type mismatch.
Related errors
- Resource not found.
- Organization Connection configuration malformed
- Unauthorized.
- You cannot change your email when using Key Connector.
- Invalid password.
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/86cc30fbfd9bc192.
Report an issue: GitHub.