bitwarden/server · error · BadRequestException

All existing webauthn prf keys must be included in the rotat

Error message

All existing webauthn prf keys must be included in the rotation.

What it means

Thrown by WebAuthnLoginKeyRotationValidator during key rotation. It loads every WebAuthn credential the user has with PRF enabled (WebAuthnPrfStatus.Enabled) and requires the rotation request to include each one matched by Id, because each such credential stores an encrypted copy of the user key. Omitting one would leave it under the old key, so the rotation is rejected.

Source

Thrown at src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs:41

    }

    public async Task<IEnumerable<WebAuthnLoginRotateKeyData>> ValidateAsync(User user,
        IEnumerable<WebAuthnLoginRotateKeyRequestModel> keysToRotate)
    {
        var result = new List<WebAuthnLoginRotateKeyData>();
        var validCredentials = (await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id))
            .Where(credential => credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled).ToList();
        if (validCredentials.Count == 0)
        {
            return result;
        }

        foreach (var webAuthnCredential in validCredentials)
        {
            var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == webAuthnCredential.Id);
            if (keyToRotate == null)
            {
                throw new BadRequestException("All existing webauthn prf keys must be included in the rotation.");
            }

            if (keyToRotate.EncryptedUserKey == null)
            {
                throw new BadRequestException("WebAuthn prf keys must have user-key during rotation.");
            }

            if (keyToRotate.EncryptedPublicKey == null)
            {
                throw new BadRequestException("WebAuthn prf keys must have public-key during rotation.");
            }

            result.Add(keyToRotate.ToWebAuthnRotateKeyData());
        }

        return result;
    }
}

View on GitHub (pinned to e93b962371)

Solutions

  1. Fetch the user's current WebAuthn credentials (with PRF status) immediately before building the rotation payload.
  2. Include every credential Id whose PRF status is Enabled, with re-encrypted user and public keys.
  3. Disable PRF on a credential (or remove the passkey) before rotating if you do not want to rotate it.
  4. Validate the submitted Id set covers every PRF-enabled credential before sending.

Example fix

// before
const keys = localPasskeys.filter(p => p.favorite).map(p => ({ id: p.id, ...reencrypt(p) }));

// after
const creds = await api.getWebAuthnCredentials();
const keys = creds.filter(c => c.prfStatus === 'Enabled').map(c => ({ id: c.id, encryptedUserKey: reencrypt(c.userKey), encryptedPublicKey: reencrypt(c.publicKey) }));
Defensive patterns

Strategy: validation

Validate before calling

const prfCreds = (await api.getWebAuthnCredentials()).filter(c => c.prfStatus === 'Enabled');
const submitted = new Set(payload.keys.map(k => k.id));
const missing = prfCreds.filter(c => !submitted.has(c.id));
if (missing.length) {
  throw new Error(`Rotation is missing PRF credentials: ${missing.map(c => c.id).join(', ')}`);
}

Type guard

function isCompleteWebAuthnRotation(existing: { id: string; prfStatus: string }[], submitted: { id: string }[]): boolean {
  const have = new Set(submitted.map(s => s.id));
  return existing.filter(c => c.prfStatus === 'Enabled').every(c => have.has(c.id));
}

Try / catch

try {
  await api.rotateKey(payload);
} catch (e) {
  if (e.status === 400 && /webauthn prf keys must be included/i.test(e.message)) {
    await refreshWebAuthnCredentials();
    payload.keys = prfCreds.map(c => ({ id: c.id, encryptedUserKey: reencrypt(c.userKey), encryptedPublicKey: reencrypt(c.publicKey) }));
    return api.rotateKey(payload);
  }
  throw e;
}

Prevention

When it happens

Trigger: Key-rotation request whose keys array omits a WebAuthn credential Id that has PRF enabled on the server. A PRF credential was added/enabled in another session after the client cached credentials; the client sent a partial list; an Id was malformed.

Common situations: User enabled PRF on a passkey in another client and then rotated keys here; client built the list from local state; the client only rotated 'active' passkeys and excluded some PRF-enabled ones.

Related errors


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