quarkusio/quarkus · error · AuthenticationFailedException

DPoP proof JWK key is a private key but it must be a public

Error message

DPoP proof JWK key is a private key but it must be a public key

What it means

Thrown when the JWK embedded in the DPoP proof header contains (or resolves to) private key material. Per RFC 9449 the proof must include only the public key; shipping a private key would leak secret material and is rejected outright.

Source

Thrown at extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcIdentityProvider.java:280

                            JsonObject proofHeaders = (JsonObject) requestData.get(OidcUtils.DPOP_PROOF_JWT_HEADERS);

                            JsonObject jwkProof = proofHeaders.getJsonObject(OidcConstants.DPOP_JWK_HEADER);
                            if (jwkProof == null) {
                                LOG.warn("DPoP proof jwk header is missing");
                                throw new AuthenticationFailedException(invalidDPoPProofMap(request.getToken()));
                            }

                            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()));

View on GitHub (pinned to e1c734241f)

Solutions

  1. Embed only the public part: use privateJwk.toPublicJwk().toJson() (jose4j) or equivalent before placing it in the jwk header.
  2. Audit the proof generator to ensure only kty/crv/x/y (EC) or kty/n/e (RSA) members are emitted.
  3. If a private key may have leaked in a proof, rotate the DPoP signing key.
  4. Catch AuthenticationFailedException and regenerate proofs with the public JWK.

Example fix

// before
String jwkJson = privateJsonWebKey.toJson(); // includes private 'd'

// after
String jwkJson = privateJsonWebKey.toPublicJwk().toJson();
Defensive patterns

Strategy: validation

Validate before calling

// Assert no private members leak into the proof header
Set<String> priv = Set.of("d", "p", "q", "dp", "dq", "qi", "oth");
if (jwkMap.keySet().stream().anyMatch(priv::contains)) {
    throw new IllegalStateException("Proof jwk header must contain only PUBLIC key material");
}

Type guard

static boolean isPublicOnlyJwk(Map<String,Object> jwk) {
    return !jwk.containsKey("d") && !jwk.containsKey("p") && !jwk.containsKey("q");
}

Prevention

When it happens

Trigger: Serializing a KeyPair/EC5PrivateKey into the jwk header (e.g. calling toJson on a PrivateJsonWebKey, which includes 'd' and other private parameters), so jose4j's parsed PublicJsonWebKey has a non-null privateKey.

Common situations: Using PrivateJsonWebKey.toJson() instead of toJson() on its public counterpart; storing a full JWK set from the keystore in the proof; libraries misconfigured to embed key pairs.

Related errors


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