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

  1. Use a persistent DPoP key (stable keystore) or re-obtain the token whenever the DPoP key changes so cnf.jkt matches.
  2. 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.
  3. Verify offline: compute the proof JWK's RFC 7638 thumbprint and compare to token cnf.jkt before sending.
  4. 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

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


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/a50084d50293475c. Report an issue: GitHub.