{"record":{"id":"a50084d50293475c","repo":"quarkusio/quarkus","slug":"dpop-access-token-jwk-thumbprint-does-not-match-th","errorCode":null,"errorMessage":"DPoP access token JWK thumbprint does not match the DPoP proof JWK thumbprint","messagePattern":"DPoP access token JWK thumbprint does not match the DPoP proof JWK thumbprint","errorType":"exception","errorClass":"AuthenticationFailedException","httpStatus":401,"severity":"error","filePath":"extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java","lineNumber":288,"sourceCode":"                            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()));\n                                }\n                            } catch (JoseException ex) {\n                                LOG.warn(\"DPoP proof token signature can not be verified\");\n                                throw new AuthenticationFailedException(ex, invalidDPoPProofMap(request.getToken()));\n                            }\n\n                            JsonObject proofClaims = (JsonObject) requestData.get(OidcUtils.DPOP_PROOF_JWT_CLAIMS);\n","sourceCodeStart":270,"sourceCodeEnd":306,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java#L270-L306","documentation":"Thrown when the RFC 7638 SHA-256 thumbprint of the public JWK in the DPoP proof does not equal the 'jkt' value in the access token's 'cnf' claim. This proves the proof was signed with a different key than the one the AS bound to the token, so the proof-of-possession fails and the request is rejected.","triggerScenarios":"The client signs the DPoP proof with key A but presents an access token bound (cnf.jkt) to key B — e.g. the client restarted and regenerated its keypair while reusing a cached token, multiple app instances share a token but each has its own DPoP key, or the key store entry changed.","commonSituations":"Horizontal scaling where DPoP keys are generated per-instance instead of shared/sticky; ephemeral in-memory keys with long-lived tokens; rotating signing keys without invalidating cached tokens.","solutions":["Use a persistent DPoP key (stable keystore) or re-obtain the token whenever the DPoP key changes so cnf.jkt matches.","Ensure all instances behind a load balancer share the same DPoP private key, or pin requests carrying a given token to the instance holding its key.","Verify offline: compute the proof JWK's RFC 7638 thumbprint and compare to token cnf.jkt before sending.","Catch AuthenticationFailedException and trigger token refresh to mint a token bound to the current key."],"exampleFix":"// before: new key per process, cached token reused\nKeyPair kp = generateKeyPair(); // random each startup\n\n// after: load stable key from keystore\nKeyPair kp = keyStore.loadKeyPair(\"dpop-key\"); // same key across restarts/instances","handlingStrategy":"try-catch","validationCode":"// Compare thumbprints client-side before sending\nString proofJkt = Base64.getUrlEncoder().withoutPadding()\n    .encodeToString(publicJwk.calculateThumbprint(\"SHA-256\"));\nString tokenJkt = cnfClaim.getString(\"jkt\");\nif (!proofJkt.equals(tokenJkt)) token = reauthenticate(); // rebind token to current key","typeGuard":"static boolean keyMatchesTokenBinding(PublicJsonWebKey k, JsonObject cnf) {\n    try {\n        String jkt = OidcCommonUtils.base64UrlEncode(k.calculateThumbprint(\"SHA-256\"));\n        return jkt.equals(cnf.getString(\"jkt\"));\n    } catch (JoseException e) { return false; }\n}","tryCatchPattern":"try {\n    return callWithDpop(token, keyPair);\n} catch (AuthenticationFailedException e) {\n    // key changed: obtain a token bound to the current key\n    return callWithDpop(reauthenticate(keyPair), keyPair);\n}","preventionTips":["Persist DPoP keys in a keystore shared by all app instances","Re-obtain tokens whenever the DPoP key changes","Use sticky sessions or shared keys when scaling horizontally","Compare local RFC 7638 thumbprint to cnf.jkt in pre-flight checks"],"tags":["oidc","dpop","thumbprint-mismatch","key-binding"],"backgroundTag":"dpop-jwk-thumbprint-mismatch","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"}