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

  1. 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.
  2. Enable the PM30806_SelfServiceChangeEmailCommand flag so the client can use the flow that does not require these legacy fields.
  3. 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

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


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/86cc30fbfd9bc192. Report an issue: GitHub.