bitwarden/server · error · BadRequestException
Invalid password.
Error message
Invalid password.
What it means
Thrown as a 400 BadRequestException("MasterPasswordHash", "Invalid password.") from the legacy path of POST accounts/email-token when _userService.CheckPasswordAsync(user, model.MasterPasswordHash) returns false. The supplied master-password hash does not match the user's current verifier; a 2-second delay precedes the throw to mitigate timing attacks.
Source
Thrown at src/Api/Auth/Controllers/AccountsController.cs:140
// TODO: PM-39120 - PM30806_SelfServiceChangeEmailCommand flag cleanup, remove the flag
// check and keep only the SelfServiceChangeEmailCommand call.
if (_featureService.IsEnabled(FeatureFlagKeys.PM30806_SelfServiceChangeEmailCommand))
{
await _selfServiceChangeEmailCommand.InitiateChangeEmailAsync(
user, model.MasterPasswordHash, model.NewEmail);
return;
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("You cannot change your email when using Key Connector.");
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
{
await Task.Delay(2000);
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
}
var claimedUserValidationResult = await _userService.ValidateClaimedUserDomainAsync(user, model.NewEmail);
if (!claimedUserValidationResult.Succeeded)
{
throw new BadRequestException(claimedUserValidationResult.Errors);
}
await _userService.InitiateEmailChangeAsync(user, model.NewEmail);
}
[HttpPost("email")]
public async Task PostEmail([FromBody] EmailRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{View on GitHub (pinned to e93b962371)
Solutions
- Re-derive MasterPasswordHash using the user's current KDF configuration and resubmit.
- Re-authenticate if the master password was changed since the session started.
- Confirm the hash sent is the derived hash (not plaintext) and maps to the authenticated principal.
- Prompt the user to re-enter the master password and retry.
Example fix
// before
body.masterPasswordHash = staleHash;
// after
body.masterPasswordHash = await crypto.hashPassword(masterPassword, user.kdf);
await api.post('accounts/email-token', body); Defensive patterns
Strategy: try-catch
Validate before calling
function validHash(h) { return typeof h === 'string' && h.length > 0 && h.length < 1024; } Type guard
function isEmailTokenModel(v): v is { masterPasswordHash: string; newEmail: string } {
return !!v && typeof v.masterPasswordHash === 'string' && typeof v.newEmail === 'string';
} Try / catch
try { await api.post('accounts/email-token', body); }
catch (e) {
if (e?.response?.status === 400 && e.response.data?.ValidationErrors?.['MasterPasswordHash']?.some(m => /Invalid password/i.test(m))) promptForMasterPasswordAgain();
else throw e;
} Prevention
- Derive MasterPasswordHash with the current KDF before submitting.
- Re-authenticate after a password or KDF change.
- Re-prompt for the master password before sensitive account changes.
When it happens
Trigger: An authenticated, non-Key-Connector user (flag OFF path) calls POST accounts/email-token with a master-password hash that fails verification. The user exists and is not a Key Connector user, but the hash is wrong.
Common situations: User mistyped the master password; client derived the hash with outdated KDF settings after a server-side KDF rotation; password was changed elsewhere and the local session holds a stale derived hash.
Related errors
- User verification failed.
- Invalid password.
- Unauthorized.
- You cannot change your email when using Key Connector.
- NewMasterPasswordHash and Key are required.
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/7aa5cd417c0d1e5d.
Report an issue: GitHub.