{"record":{"id":"ce6a08599954c6f3","repo":"quarkusio/quarkus","slug":"dpop-proof-access-token-hash-is-missing","errorCode":null,"errorMessage":"DPoP proof access token hash is missing","messagePattern":"DPoP proof access token hash is missing","errorType":"exception","errorClass":"AuthenticationFailedException","httpStatus":401,"severity":"error","filePath":"extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java","lineNumber":312,"sourceCode":"                                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\n                            // 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","sourceCodeStart":294,"sourceCodeEnd":330,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java#L294-L330","documentation":"Thrown when the verified DPoP proof lacks the 'ath' (access token hash) claim, required by RFC 9449 whenever the proof accompanies a resource request carrying an access token. Without 'ath', the proof cannot be cryptographically tied to this specific access token, so Quarkus rejects the request.","triggerScenarios":"The client builds the DPoP proof for a protected-resource request without computing 'ath' = base64url(SHA-256(accessToken)) and adding it to the proof claims; proof-generation code intended only for the token endpoint is reused for API calls.","commonSituations":"Older/naive DPoP implementations that only set htm/htu/iat/jti; frameworks where the proof builder has no access to the access token at proof time; tokens added to requests after the proof was generated.","solutions":["Add 'ath' to the proof claims: base64urlEncode(SHA-256 bytes of the ASCII access token) on every protected-resource DPoP proof.","Make the proof factory accept the access token so 'ath' is always computed at request time (unlike jti/iat, ath must reflect the current token).","Regenerate a new proof per request — never reuse proofs, which also fails replay checks.","Catch AuthenticationFailedException and rebuild the proof including 'ath', then retry."],"exampleFix":"// before\nclaims.put(\"htm\", \"GET\"); claims.put(\"htu\", url); // no ath\n\n// after\nclaims.put(\"htm\", \"GET\"); claims.put(\"htu\", url);\nclaims.put(\"ath\", Base64.getUrlEncoder().withoutPadding()\n    .encodeToString(MessageDigest.getInstance(\"SHA-256\").digest(accessToken.getBytes(UTF_8))));","handlingStrategy":"validation","validationCode":"// Ensure every resource-request proof carries ath\nif (!proofClaims.containsKey(\"ath\")) {\n    byte[] digest = MessageDigest.getInstance(\"SHA-256\")\n        .digest(accessToken.getBytes(StandardCharsets.US_ASCII));\n    proofClaims.put(\"ath\", Base64.getUrlEncoder().withoutPadding().encodeToString(digest));\n}","typeGuard":"static boolean proofHasAth(Map<String,Object> claims) {\n    return claims.containsKey(\"ath\") && claims.get(\"ath\") instanceof String s && !s.isBlank();\n}","tryCatchPattern":"try {\n    return callWithProof(proof);\n} catch (AuthenticationFailedException e) {\n    return callWithProof(rebuildProofWithAth(accessToken));\n}","preventionTips":["Design proof builders to always accept the access token and compute ath","Remember ath is required for resource requests (not for the token endpoint call)","Regenerate the proof whenever the access token changes","Unit-test that ath equals base64url(SHA-256(token)) exactly, with no padding"],"tags":["oidc","dpop","ath-claim","proof-validation"],"backgroundTag":"dpop-proof-missing-ath-claim","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}