{"record":{"id":"bc03963c82f25936","repo":"bitwarden/server","slug":"webauthn-prf-keys-must-have-user-key-during-rotati","errorCode":null,"errorMessage":"WebAuthn prf keys must have user-key during rotation.","messagePattern":"WebAuthn prf keys must have user-key during rotation\\.","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs","lineNumber":46,"sourceCode":"        var result = new List<WebAuthnLoginRotateKeyData>();\n        var validCredentials = (await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id))\n            .Where(credential => credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled).ToList();\n        if (validCredentials.Count == 0)\n        {\n            return result;\n        }\n\n        foreach (var webAuthnCredential in validCredentials)\n        {\n            var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == webAuthnCredential.Id);\n            if (keyToRotate == null)\n            {\n                throw new BadRequestException(\"All existing webauthn prf keys must be included in the rotation.\");\n            }\n\n            if (keyToRotate.EncryptedUserKey == null)\n            {\n                throw new BadRequestException(\"WebAuthn prf keys must have user-key during rotation.\");\n            }\n\n            if (keyToRotate.EncryptedPublicKey == null)\n            {\n                throw new BadRequestException(\"WebAuthn prf keys must have public-key during rotation.\");\n            }\n\n            result.Add(keyToRotate.ToWebAuthnRotateKeyData());\n        }\n\n        return result;\n    }\n}\n","sourceCodeStart":28,"sourceCodeEnd":60,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs#L28-L60","documentation":"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.","triggerScenarios":"The keys array includes the credential Id but EncryptedUserKey is null or was omitted during request construction.","commonSituations":"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.","solutions":["Re-encrypt the user key for every PRF-enabled credential and populate EncryptedUserKey.","Add a pre-send assertion that every key entry has a non-null EncryptedUserKey.","If a credential cannot be re-encrypted, disable its PRF or remove the passkey before rotating.","Log the failing credential Id to identify the under-populated entry."],"exampleFix":"// before\n{ id: c.id, encryptedPublicKey: reencrypt(c.publicKey) }\n\n// after\n{ id: c.id, encryptedUserKey: reencrypt(c.userKey), encryptedPublicKey: reencrypt(c.publicKey) }","handlingStrategy":"validation","validationCode":"const missingUserKey = payload.keys.filter(k => k.encryptedUserKey == null);\nif (missingUserKey.length) {\n  throw new Error(`WebAuthn keys missing user key: ${missingUserKey.map(k => k.id).join(', ')}`);\n}","typeGuard":"function hasWebAuthnUserKey(k: { encryptedUserKey?: string | null }): boolean {\n  return k.encryptedUserKey != null && k.encryptedUserKey.length > 0;\n}","tryCatchPattern":"try {\n  await api.rotateKey(payload);\n} catch (e) {\n  if (e.status === 400 && /must have user-key/i.test(e.message)) {\n    payload.keys = payload.keys.map(k => ({ ...k, encryptedUserKey: k.encryptedUserKey ?? reencrypt(userKeyFor(k.id)) }));\n    return api.rotateKey(payload);\n  }\n  throw e;\n}","preventionTips":["Assert every PRF key entry has a non-null EncryptedUserKey before submitting.","Disable PRF on a credential if its user key cannot be re-encrypted.","Centralize the re-encryption mapper so the user-key field is always populated."],"tags":["key-rotation","webauthn","prf","null-check","encryption","bad-request"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}