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
- Fix the client so every DPoP proof JWT includes a valid public 'jwk' header matching the key used to sign the proof.
- Verify the 'jwk' header contains only public parameters (kty, crv/n/e or kty/n/e) and a supported key type (EC, RSA, OKP).
- Use a maintained DPoP library (e.g. com.nimbusds:oauth2-dpop) to generate proofs rather than constructing JWTs manually.
- 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
- Always include a public 'jwk' header in DPoP proofs generated by your client.
- Use an established DPoP library instead of hand-building proof JWTs.
- Unit-test your DPoP proof generator against the authorization server before production.
- Never put private key material ('d') into the jwk header.
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
- Unsupported alg parameter in JWS Header: ${algorithm.getName
- Missing jwk parameter in JWS Header.
- invalid_dpop_proof
- invalid_key
- method is invalid
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/726c894298a35c12.
Report an issue: GitHub.