quarkusio/quarkus · error · AuthenticationFailedException
DPoP access token hash does not match the DPoP proof access
Error message
DPoP access token hash does not match the DPoP proof access token hash
What it means
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.
Source
Thrown at extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java:325
// Calculate the access token thumprint and compare with the `ath` claim
String accessTokenProof = proofClaims.getString(OidcConstants.DPOP_ACCESS_TOKEN_THUMBPRINT);
if (accessTokenProof == null) {
LOG.warn("DPoP proof access token hash is missing");
throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));
}
String accessTokenHash = null;
try {
accessTokenHash = OidcCommonUtils.base64UrlEncode(
OidcUtils.getSha256Digest(request.getToken().getToken()));
} catch (NoSuchAlgorithmException ex) {
// SHA256 is always supported
}
if (!accessTokenProof.equals(accessTokenHash)) {
LOG.warn("DPoP access token hash does not match the DPoP proof access token hash");
throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));
}
return t;
}
});
}
}
if (stepUpAuthPolicy != null) {
result = result.invoke(stepUpAuthPolicy);
}
return result;
}
}
private static String getTokenCertThumbprint(Map<String, Object> requestData, TokenVerificationResult t) {View on GitHub (pinned to e1c734241f)
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.
Example fix
// before: proof generated for old token ath = b64url(sha256(oldAccessToken)) // after: proof bound to the token actually sent ath = b64url(sha256(currentAccessToken)); proof = makeDPoPProof(htm, htu, nonce, jwk, ath);
Defensive patterns
Strategy: validation
Validate before calling
String ath = base64Url(sha256(accessToken));
if (!ath.equals(dpopProof.getClaim("ath"))) {
throw new IllegalArgumentException("DPoP proof not bound to this access token");
} Type guard
boolean isDpopBound(String accessToken, JwtClaims proof) {
String ath = proof.getStringClaimValue("ath");
return ath != null && ath.equals(base64Url(sha256(accessToken)));
} Try / catch
try {
return identityProvider.authenticate(request);
} catch (AuthenticationFailedException e) {
// regenerate DPoP proof for the current token and retry once
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- DPoP access token does not contain a confirmation 'cnf' clai
- The '%s' annotation is only supported when proactive authent
- Client certificate thumbprint is not available
- DPoP proof jwk header is missing
- DPoP proof jwk header does not represent a valid JWK key
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/8039757dd0fa0eb7.
Report an issue: GitHub.