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

  1. Re-derive MasterPasswordHash using the user's current KDF configuration and resubmit.
  2. Re-authenticate if the master password was changed since the session started.
  3. Confirm the hash sent is the derived hash (not plaintext) and maps to the authenticated principal.
  4. 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

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


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