{"record":{"id":"c3c56d84a8754345","repo":"quarkusio/quarkus","slug":"dpop-proof-jwk-key-is-a-private-key-but-it-must-be","errorCode":null,"errorMessage":"DPoP proof JWK key is a private key but it must be a public key","messagePattern":"DPoP proof JWK key is a private key but it must be a public key","errorType":"exception","errorClass":"AuthenticationFailedException","httpStatus":401,"severity":"error","filePath":"extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java","lineNumber":280,"sourceCode":"                            JsonObject proofHeaders = (JsonObject) requestData.get(OidcUtils.DPOP_PROOF_JWT_HEADERS);\n\n                            JsonObject jwkProof = proofHeaders.getJsonObject(OidcConstants.DPOP_JWK_HEADER);\n                            if (jwkProof == null) {\n                                LOG.warn(\"DPoP proof jwk header is missing\");\n                                throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));\n                            }\n\n                            PublicJsonWebKey publicJsonWebKey = null;\n                            try {\n                                publicJsonWebKey = PublicJsonWebKey.Factory.newPublicJwk(jwkProof.getMap());\n                            } catch (JoseException ex) {\n                                LOG.warn(\"DPoP proof jwk header does not represent a valid JWK key\");\n                                throw new AuthenticationFailedException(ex, invalidDPoPProofMap(request.getToken()));\n                            }\n\n                            if (publicJsonWebKey.getPrivateKey() != null) {\n                                LOG.warn(\"DPoP proof JWK key is a private key but it must be a public key\");\n                                throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));\n                            }\n\n                            byte[] jwkProofDigest = publicJsonWebKey.calculateThumbprint(\"SHA-256\");\n                            String jwkProofThumbprint = OidcCommonUtils.base64UrlEncode(jwkProofDigest);\n\n                            if (!dpopJwkThumbprint.equals(jwkProofThumbprint)) {\n                                LOG.warn(\"DPoP access token JWK thumbprint does not match the DPoP proof JWK thumbprint\");\n                                throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));\n                            }\n\n                            try {\n                                JsonWebSignature jws = new JsonWebSignature();\n                                jws.setAlgorithmConstraints(OidcProvider.ASYMMETRIC_ALGORITHM_CONSTRAINTS);\n                                jws.setCompactSerialization((String) requestData.get(OidcUtils.DPOP_PROOF));\n                                jws.setKey(publicJsonWebKey.getPublicKey());\n                                if (!jws.verifySignature()) {\n                                    LOG.warn(\"DPoP proof token signature is invalid\");\n                                    throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java#L262-L298","documentation":"Thrown when the JWK embedded in the DPoP proof header contains (or resolves to) private key material. Per RFC 9449 the proof must include only the public key; shipping a private key would leak secret material and is rejected outright.","triggerScenarios":"Serializing a KeyPair/EC5PrivateKey into the jwk header (e.g. calling toJson on a PrivateJsonWebKey, which includes 'd' and other private parameters), so jose4j's parsed PublicJsonWebKey has a non-null privateKey.","commonSituations":"Using PrivateJsonWebKey.toJson() instead of toJson() on its public counterpart; storing a full JWK set from the keystore in the proof; libraries misconfigured to embed key pairs.","solutions":["Embed only the public part: use privateJwk.toPublicJwk().toJson() (jose4j) or equivalent before placing it in the jwk header.","Audit the proof generator to ensure only kty/crv/x/y (EC) or kty/n/e (RSA) members are emitted.","If a private key may have leaked in a proof, rotate the DPoP signing key.","Catch AuthenticationFailedException and regenerate proofs with the public JWK."],"exampleFix":"// before\nString jwkJson = privateJsonWebKey.toJson(); // includes private 'd'\n\n// after\nString jwkJson = privateJsonWebKey.toPublicJwk().toJson();","handlingStrategy":"validation","validationCode":"// Assert no private members leak into the proof header\nSet<String> priv = Set.of(\"d\", \"p\", \"q\", \"dp\", \"dq\", \"qi\", \"oth\");\nif (jwkMap.keySet().stream().anyMatch(priv::contains)) {\n    throw new IllegalStateException(\"Proof jwk header must contain only PUBLIC key material\");\n}","typeGuard":"static boolean isPublicOnlyJwk(Map<String,Object> jwk) {\n    return !jwk.containsKey(\"d\") && !jwk.containsKey(\"p\") && !jwk.containsKey(\"q\");\n}","tryCatchPattern":null,"preventionTips":["Use toPublicJwk().toJson() (jose4j) rather than toJson() on a PrivateJsonWebKey","Code-review any code that serializes key pairs","Rotate the DPoP key if a proof with private material was transmitted","Treat private-key-in-header incidents as security events, not just auth errors"],"tags":["oidc","dpop","jwk","private-key-exposure"],"backgroundTag":"dpop-private-key-in-proof","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}