spring-projects/spring-security · error · OAuth2AuthenticationException

invalid_dpop_proof

invalid_dpop_proof

Error message

jwk header is missing or invalid.

What it means

When a DPoP-bound access token is being customized, the token customizer reads the 'jwk' header of the DPoP proof JWT and reconstructs the public key to compute a confirmation ('cnf') claim. If the header is absent, malformed, or does not yield a valid JWK, the customizer cannot bind the token to the proof's key and throws OAuth2AuthenticationException with code 'invalid_dpop_proof' (per RFC 9449).

Source

Thrown at config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/DefaultOAuth2TokenCustomizers.java:107

				}
			}
		}

		// Add 'cnf' claim for OAuth 2.0 Demonstrating Proof of Possession (DPoP)
		Jwt dPoPProofJwt = tokenContext.get(OAuth2TokenContext.DPOP_PROOF_KEY);
		if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType()) && dPoPProofJwt != null) {
			JWK jwk = null;
			@SuppressWarnings("unchecked")
			Map<String, Object> jwkJson = (Map<String, Object>) dPoPProofJwt.getHeaders().get("jwk");
			try {
				jwk = JWK.parse(jwkJson);
			}
			catch (Exception ignored) {
			}
			if (jwk == null) {
				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
						"jwk header is missing or invalid.", null);
				throw new OAuth2AuthenticationException(error);
			}

			try {
				String sha256Thumbprint = jwk.computeThumbprint().toString();
				if (cnfClaims == null) {
					cnfClaims = new HashMap<>();
				}
				cnfClaims.put("jkt", sha256Thumbprint);
			}
			catch (Exception ex) {
				OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
						"Failed to compute SHA-256 Thumbprint for DPoP Proof PublicKey.", null);
				throw new OAuth2AuthenticationException(error, ex);
			}
		}

		if (!CollectionUtils.isEmpty(cnfClaims)) {
			claims.put("cnf", cnfClaims);

View on GitHub (pinned to 96852e8860)

Solutions

  1. Fix the client so every DPoP proof JWT includes a valid public 'jwk' header matching the key used to sign the proof.
  2. Verify the 'jwk' header contains only public parameters (kty, crv/n/e or kty/n/e) and a supported key type (EC, RSA, OKP).
  3. Use a maintained DPoP library (e.g. com.nimbusds:oauth2-dpop) to generate proofs rather than constructing JWTs manually.
  4. If DPoP is not intended, send the token request without the DPoP header and ensure the client is not registered for DPoP-bound tokens.

Example fix

// before: DPoP proof without jwk header
{"typ":"dpop+jwt","alg":"ES256"}
// after: include the public key in the jwk header
{"typ":"dpop+jwt","alg":"ES256","jwk":{"kty":"EC","crv":"P-256","x":"...","y":"..."}}
Defensive patterns

Strategy: validation

Validate before calling

boolean dpopProofHasJwk(String proofJwt) {
    String[] parts = proofJwt.split("\\.");
    if (parts.length != 3) return false;
    try {
        String header = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);
        return header.contains("\"jwk\"") && header.contains("\"kty\"");
    } catch (IllegalArgumentException e) {
        return false;
    }
}

Type guard

boolean isPublicJwk(Map<String, Object> jwk) {
    return jwk != null && jwk.get("kty") instanceof String kty
        && (kty.equals("EC") || kty.equals("RSA") || kty.equals("OKP"))
        && !jwk.containsKey("d");
}

Try / catch

try {
    tokenRequest.submit();
} catch (OAuth2AuthenticationException e) {
    if ("invalid_dpop_proof".equals(e.getError().getErrorCode())) {
        // regenerate DPoP proof with valid public jwk header and retry once
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: An access token request carries a DPoP proof whose JWT header lacks a valid 'jwk' (public key) member, or the 'jwk' cannot be parsed into a JWK object during DefaultOAuth2TokenCustomizers.customize (invoked via jwtCustomizer/accessTokenCustomizer).

Common situations: Clients using DPoP libraries that omit the 'jwk' header from the proof; hand-rolled DPoP proof generation with malformed or non-EC/RSA 'jwk' values; 'jwk' header containing a private key or unsupported key type; middleware stripping JWT headers.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/726c894298a35c12. Report an issue: GitHub.