{"record":{"id":"8039757dd0fa0eb7","repo":"quarkusio/quarkus","slug":"dpop-access-token-hash-does-not-match-the-dpop-pro","errorCode":null,"errorMessage":"DPoP access token hash does not match the DPoP proof access token hash","messagePattern":"DPoP access token hash does not match the DPoP proof access token hash","errorType":"exception","errorClass":"AuthenticationFailedException","httpStatus":401,"severity":"error","filePath":"extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java","lineNumber":325,"sourceCode":"                            // Calculate the access token thumprint and compare with the `ath` claim\n\n                            String accessTokenProof = proofClaims.getString(OidcConstants.DPOP_ACCESS_TOKEN_THUMBPRINT);\n                            if (accessTokenProof == null) {\n                                LOG.warn(\"DPoP proof access token hash is missing\");\n                                throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));\n                            }\n\n                            String accessTokenHash = null;\n                            try {\n                                accessTokenHash = OidcCommonUtils.base64UrlEncode(\n                                        OidcUtils.getSha256Digest(request.getToken().getToken()));\n                            } catch (NoSuchAlgorithmException ex) {\n                                // SHA256 is always supported\n                            }\n\n                            if (!accessTokenProof.equals(accessTokenHash)) {\n                                LOG.warn(\"DPoP access token hash does not match the DPoP proof access token hash\");\n                                throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));\n                            }\n\n                            return t;\n                        }\n\n                    });\n                }\n            }\n\n            if (stepUpAuthPolicy != null) {\n                result = result.invoke(stepUpAuthPolicy);\n            }\n\n            return result;\n        }\n    }\n\n    private static String getTokenCertThumbprint(Map<String, Object> requestData, TokenVerificationResult t) {","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java#L307-L343","documentation":"When a DPoP-bound access token is validated, Quarkus recomputes the SHA-256 hash of the access token and compares it to the 'ath' claim in the DPoP proof JWT. This AuthenticationFailedException is thrown when the two hashes differ, meaning the DPoP proof was not bound to the access token being presented. This prevents replay of a DPoP proof with a different token.","triggerScenarios":"Sending a Bearer or DPoP header whose DPoP proof 'ath' claim was computed over a different access token (e.g. a stale proof generated for a previous token), or hand-crafting DPoP proofs with a wrong/missing 'ath' value.","commonSituations":"Client caches DPoP proofs across token refreshes; proxy or middleware re-signs proofs incorrectly; client library computes 'ath' only when the token is a JWT but the server requires it for opaque tokens too.","solutions":["Regenerate the DPoP proof for the current access token, computing 'ath' = base64url(SHA-256(accessToken)).","Ensure the client does not reuse DPoP proofs after the access token is refreshed.","If DPoP is not intended, configure the client not to use DPoP (no 'dpop_jkt' / DPoP header) and use plain Bearer tokens.","Check server-side quarkus-oidc DPoP settings and token clock skew if hashes should match."],"exampleFix":"// before: proof generated for old token\nath = b64url(sha256(oldAccessToken))\n// after: proof bound to the token actually sent\nath = b64url(sha256(currentAccessToken));\nproof = makeDPoPProof(htm, htu, nonce, jwk, ath);","handlingStrategy":"validation","validationCode":"String ath = base64Url(sha256(accessToken));\nif (!ath.equals(dpopProof.getClaim(\"ath\"))) {\n    throw new IllegalArgumentException(\"DPoP proof not bound to this access token\");\n}","typeGuard":"boolean isDpopBound(String accessToken, JwtClaims proof) {\n    String ath = proof.getStringClaimValue(\"ath\");\n    return ath != null && ath.equals(base64Url(sha256(accessToken)));\n}","tryCatchPattern":"try {\n    return identityProvider.authenticate(request);\n} catch (AuthenticationFailedException e) {\n    // regenerate DPoP proof for the current token and retry once\n}","preventionTips":["Always compute 'ath' from the exact access token sent in the request","Never cache DPoP proofs across token refreshes","Use a maintained client library (e.g. oauth2-client with DPoP support) instead of hand-rolling proofs"],"tags":["oidc","dpop","token-validation","authentication"],"backgroundTag":"dpop-proof-token-binding-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"}