bitwarden/server · error · BadRequestException

Unable to update credential.

Error message

Unable to update credential.

What it means

Thrown (HTTP 400) after a WebAuthn assertion succeeds but either no credential was resolved (credential == null) or the resolved authenticator does not report the PRF extension as supported (SupportsPrf != true). The key-rotation flow relies on PRF to derive the new user encryption keys, so a non-PRf passkey is ineligible to update the key set.

Source

Thrown at src/Api/Auth/Controllers/WebAuthnController.cs:146

        {
            throw new BadRequestException("Passkeys cannot be created for your account. SSO login is required.");
        }
    }

    [Authorize(Policies.Application)]
    [HttpPut()]
    public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)
    {
        var tokenable = _assertionOptionsDataProtector.Unprotect(model.Token);
        if (!tokenable.TokenIsValid(WebAuthnLoginAssertionOptionsScope.UpdateKeySet) || tokenable.Options == null)
        {
            throw new BadRequestException("The token associated with your request is invalid or has expired. A valid token is required to continue.");
        }

        var (_, credential) = await _assertWebAuthnLoginCredentialCommand.AssertWebAuthnLoginCredential(tokenable.Options, model.DeviceResponse);
        if (credential == null || credential.SupportsPrf != true)
        {
            throw new BadRequestException("Unable to update credential.");
        }

        // assign new keys to credential
        credential.EncryptedUserKey = model.EncryptedUserKey;
        credential.EncryptedPrivateKey = model.EncryptedPrivateKey;
        credential.EncryptedPublicKey = model.EncryptedPublicKey;

        await _credentialRepository.UpdateAsync(credential);
    }

    [Authorize(Policies.Web)]
    [HttpPost("{id}/delete")]
    public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
    {
        var user = await VerifyUserAsync(model);
        var credential = await _credentialRepository.GetByIdAsync(id, user.Id);
        if (credential == null)
        {

View on GitHub (pinned to e93b962371)

Solutions

  1. Enroll a new passkey on a PRf-capable authenticator (recent platform authenticator + up-to-date browser) and retry key rotation.
  2. Confirm the deviceResponse corresponds to a credential id still present for the user (refresh the credential list).
  3. Fall back to a non-passkey key-rotation path for users without a PRf-capable authenticator.
Defensive patterns

Strategy: type-guard

Validate before calling

// Client-side: before attempting key rotation, confirm the chosen authenticator advertised PRF support during registration/assertion.

Type guard

// Server-side guard you can apply before reaching the throw site:
// bool CanRotateViaPrf(WebAuthnCredential c) => c is not null && c.SupportsPrf == true;
// Then: if (!CanRotateViaPrf(credential)) return GuidedFallback();

Try / catch

try {
  await put('/webauthn', payload);
} catch (e) {
  if (e.isBadRequest && /unable to update credential/i.test(e.message)) {
    await enrollPrfCapablePasskey(); // re-enroll on a PRf-capable device, then retry
  } else { throw e; }
}

Prevention

When it happens

Trigger: User's passkey was created on an authenticator that does not expose the PRF extension (older security keys, some platform authenticators); the submitted deviceResponse signs over a credential id that matches no stored record; the credential was deleted between options-generation and assertion.

Common situations: Trying to rotate keys with a passkey enrolled on legacy hardware; browser/authenticator combination that does not surface PRF; mixed authenticator fleet where only some users have PRf-capable devices.

Related errors


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