quarkusio/quarkus · error · AuthenticationFailedException
DPoP access token JWK thumbprint does not match the DPoP pro
Error message
DPoP access token JWK thumbprint does not match the DPoP proof JWK thumbprint
What it means
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.
Source
Thrown at extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java:288
PublicJsonWebKey publicJsonWebKey = null;
try {
publicJsonWebKey = PublicJsonWebKey.Factory.newPublicJwk(jwkProof.getMap());
} catch (JoseException ex) {
LOG.warn("DPoP proof jwk header does not represent a valid JWK key");
throw new AuthenticationFailedException(ex, invalidDPoPProofMap(request.getToken()));
}
if (publicJsonWebKey.getPrivateKey() != null) {
LOG.warn("DPoP proof JWK key is a private key but it must be a public key");
throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));
}
byte[] jwkProofDigest = publicJsonWebKey.calculateThumbprint("SHA-256");
String jwkProofThumbprint = OidcCommonUtils.base64UrlEncode(jwkProofDigest);
if (!dpopJwkThumbprint.equals(jwkProofThumbprint)) {
LOG.warn("DPoP access token JWK thumbprint does not match the DPoP proof JWK thumbprint");
throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));
}
try {
JsonWebSignature jws = new JsonWebSignature();
jws.setAlgorithmConstraints(OidcProvider.ASYMMETRIC_ALGORITHM_CONSTRAINTS);
jws.setCompactSerialization((String) requestData.get(OidcUtils.DPOP_PROOF));
jws.setKey(publicJsonWebKey.getPublicKey());
if (!jws.verifySignature()) {
LOG.warn("DPoP proof token signature is invalid");
throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));
}
} catch (JoseException ex) {
LOG.warn("DPoP proof token signature can not be verified");
throw new AuthenticationFailedException(ex, invalidDPoPProofMap(request.getToken()));
}
JsonObject proofClaims = (JsonObject) requestData.get(OidcUtils.DPOP_PROOF_JWT_CLAIMS);
View on GitHub (pinned to e1c734241f)
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.
Example fix
// before: new key per process, cached token reused
KeyPair kp = generateKeyPair(); // random each startup
// after: load stable key from keystore
KeyPair kp = keyStore.loadKeyPair("dpop-key"); // same key across restarts/instances Defensive patterns
Strategy: try-catch
Validate before calling
// Compare thumbprints client-side before sending
String proofJkt = Base64.getUrlEncoder().withoutPadding()
.encodeToString(publicJwk.calculateThumbprint("SHA-256"));
String tokenJkt = cnfClaim.getString("jkt");
if (!proofJkt.equals(tokenJkt)) token = reauthenticate(); // rebind token to current key Type guard
static boolean keyMatchesTokenBinding(PublicJsonWebKey k, JsonObject cnf) {
try {
String jkt = OidcCommonUtils.base64UrlEncode(k.calculateThumbprint("SHA-256"));
return jkt.equals(cnf.getString("jkt"));
} catch (JoseException e) { return false; }
} Try / catch
try {
return callWithDpop(token, keyPair);
} catch (AuthenticationFailedException e) {
// key changed: obtain a token bound to the current key
return callWithDpop(reauthenticate(keyPair), keyPair);
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Client certificate thumbprint does not match the token certi
- DPoP access token does not contain a confirmation 'cnf' clai
- DPoP proof jwk header is missing
- DPoP proof jwk header does not represent a valid JWK key
- DPoP proof JWK key is a private key but it must be a public
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/a50084d50293475c.
Report an issue: GitHub.