bitwarden/server · error · BadRequestException

WebAuthn prf keys must have user-key during rotation.

Error message

WebAuthn prf keys must have user-key during rotation.

What it means

Thrown by WebAuthnLoginKeyRotationValidator when a rotation entry matched a PRF-enabled credential (matched by Id) but its EncryptedUserKey is null. PRF credentials store an encrypted copy of the user key, so a matched entry must supply the re-encrypted user key; a null value leaves the credential unusable and is rejected.

Source

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

        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. Re-encrypt the user key for every PRF-enabled credential and populate EncryptedUserKey.
  2. Add a pre-send assertion that every key entry has a non-null EncryptedUserKey.
  3. If a credential cannot be re-encrypted, disable its PRF or remove the passkey before rotating.
  4. Log the failing credential Id to identify the under-populated entry.

Example fix

// before
{ id: c.id, encryptedPublicKey: reencrypt(c.publicKey) }

// after
{ id: c.id, encryptedUserKey: reencrypt(c.userKey), encryptedPublicKey: reencrypt(c.publicKey) }
Defensive patterns

Strategy: validation

Validate before calling

const missingUserKey = payload.keys.filter(k => k.encryptedUserKey == null);
if (missingUserKey.length) {
  throw new Error(`WebAuthn keys missing user key: ${missingUserKey.map(k => k.id).join(', ')}`);
}

Type guard

function hasWebAuthnUserKey(k: { encryptedUserKey?: string | null }): boolean {
  return k.encryptedUserKey != null && k.encryptedUserKey.length > 0;
}

Try / catch

try {
  await api.rotateKey(payload);
} catch (e) {
  if (e.status === 400 && /must have user-key/i.test(e.message)) {
    payload.keys = payload.keys.map(k => ({ ...k, encryptedUserKey: k.encryptedUserKey ?? reencrypt(userKeyFor(k.id)) }));
    return api.rotateKey(payload);
  }
  throw e;
}

Prevention

When it happens

Trigger: The keys array includes the credential Id but EncryptedUserKey is null or was omitted during request construction.

Common situations: Client reused a template and forgot to set EncryptedUserKey for a newly-added PRF credential; serialization dropped the field; the re-encryption of the user key for that credential failed silently.

Related errors


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