bitwarden/server · error · BadRequestException

The model state is invalid.

Error message

The model state is invalid.

What it means

Thrown by BadRequestException(ModelState) after an IdentityResult (from the password-change call on the grantee account) returned Succeeded == false. Each identity error is folded into ASP.NET ModelState, and serializing ModelState produces the generic message 'The model state is invalid.' The real cause lives in the per-field errors inside the ModelState, not this top-level string.

Source

Thrown at src/Api/Auth/Controllers/EmergencyAccessController.cs:195

        if (model.RequestHasNewDataTypes())
        {
            var result = await _emergencyAccessService.FinishRecoveryTakeoverAsync(
                id,
                user,
                model.UnlockData!.ToData(),
                model.AuthenticationData!.ToData());

            if (result.Succeeded)
            {
                return;
            }

            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }

            throw new BadRequestException(ModelState);
        }

        await _emergencyAccessService.PasswordAsync(id, user, model.NewMasterPasswordHash, model.Key);
    }

    [HttpPost("{id}/view")]
    public async Task<EmergencyAccessViewResponseModel> ViewCiphers(Guid id)
    {
        var user = await _userService.GetUserByPrincipalAsync(User);
        var viewResult = await _emergencyAccessService.ViewAsync(id, user);
        return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers, user);
    }

    [HttpGet("{id}/{cipherId}/attachment/{attachmentId}")]
    public async Task<AttachmentResponseModel> GetAttachmentData(Guid id, Guid cipherId, string attachmentId)
    {
        var user = await _userService.GetUserByPrincipalAsync(User);
        var result =

View on GitHub (pinned to e93b962371)

Solutions

  1. Inspect the 'error' object array in the response body (the serialized ModelState) rather than the top-level message; it lists each failing field and description.
  2. Verify the AuthenticationData payload (current password hash) is correct for the emergency-access grantee before resubmitting.
  3. Confirm NewMasterPasswordHash satisfies the org/user password policy and has not been used recently (history enforcement).
  4. If no field errors appear in ModelState, validate the request body against the request model schema client-side before sending.

Example fix

// before: only logging top-level message
console.log(err.response.data.message); // 'The model state is invalid.'
// after: surface the per-field ModelState errors
const errors = err.response.data?.error?.errors ?? [];
errors.forEach(e => console.log(e.field, e.description));
Defensive patterns

Strategy: try-catch

Validate before calling

const required = ['newMasterPasswordHash','key','authenticationData'];
const missing = required.filter(k => !(k in model));
if (missing.length) throw new Error('Missing fields: ' + missing.join(','));

Type guard

function isValidPasswordChangeModel(m): m is PasswordChangeModel {
  return typeof m?.newMasterPasswordHash === 'string' && m.newMasterPasswordHash.length > 0
    && typeof m?.key === 'string'
    && !!m?.authenticationData;
}

Try / catch

try { await api.post(`/emergency-access/${id}/password`, model); }
catch (e) {
  if (e.response?.status === 400 && e.response.data?.error?.errors) {
    const detail = e.response.data.error.errors.map(x => `${x.field}: ${x.description}`).join('; ');
    throw new Error(`Password change failed: ${detail}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: POST/PUT to the emergency-access change-password endpoint (src/Api/Auth/Controllers/EmergencyAccessController.cs around line 195) where model.AuthenticationData is supplied but the underlying identity password change fails (e.g. new password rejected by policy, wrong current authentication data). result.Succeeded is false, errors are added to ModelState, then thrown.

Common situations: The supplied AuthenticationData does not match the grantee's stored credential, or NewMasterPasswordHash violates a server-side password policy / history rule. Also seen when the request model itself fails validation before reaching the identity call, leaving stale ModelState entries.

Related errors


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