spring-projects/spring-security · error · OAuth2AuthenticationException
invalid_dpop_proof
invalid_dpop_proof
Error message
jwk header is missing or invalid.
What it means
When verifying a DPoP-bound refresh token grant, the provider parses the 'jwk' header of the DPoP proof JWT to recover the proof's public key. If the header is absent, unparseable, or not a valid JWK, verifyDPoPProofPublicKey throws an OAuth2AuthenticationException with error code invalid_dpop_proof and description 'jwk header is missing or invalid.'
Source
Thrown at oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java:314
@Override
public boolean supports(Class<?> authentication) {
return OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
private static void verifyDPoPProofPublicKey(Jwt dPoPProof, ClaimAccessor accessTokenClaims) {
JWK jwk = null;
@SuppressWarnings("unchecked")
Map<String, Object> jwkJson = (Map<String, Object>) dPoPProof.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);
}
String jwkThumbprint;
try {
jwkThumbprint = jwk.computeThumbprint().toString();
}
catch (Exception ex) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,
"Failed to compute SHA-256 Thumbprint for jwk.", null);
throw new OAuth2AuthenticationException(error);
}
String jwkThumbprintClaim = null;
Map<String, Object> confirmationMethodClaim = accessTokenClaims.getClaimAsMap("cnf");
if (!CollectionUtils.isEmpty(confirmationMethodClaim) && confirmationMethodClaim.containsKey("jkt")) {
jwkThumbprintClaim = (String) confirmationMethodClaim.get("jkt");
}
if (jwkThumbprintClaim == null) {View on GitHub (pinned to 96852e8860)
Solutions
- Fix the client so every DPoP proof JWT includes a valid 'jwk' header (public key in JWK format, e.g. via nimbus-jose-jwt: header.setJWK(publicJWK)).
- Regenerate the DPoP proof so the jwk matches the key that signed the proof (EC/RSA types must correspond).
- Verify the header parses as a JWK (no truncation or re-encoding damage in transit).
- Catch OAuth2AuthenticationException and return the invalid_dpop_proof error so the client can re-attest with a fresh proof.
Example fix
// before
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256).build(); // no jwk
// after
JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)
.jwk(publicJWK.toPublicJWK())
.type(JOSEObjectType.JWT)
.build(); Defensive patterns
Strategy: try-catch
Validate before calling
// client-side: ensure the DPoP proof carries the jwk header before sending
SignedJWT proof = new SignedJWT(
new JWSHeader.Builder(alg).jwk(publicKey.toPublicJWK()).type(JOSEObjectType.JWT).build(),
claimsSet);
Objects.requireNonNull(proof.getHeader().getJWK(), "DPoP proof must embed jwk header"); Type guard
boolean hasValidJwkHeader(JWSHeader header) {
try {
return header != null && header.getJWK() != null
&& header.getJWK().toJSONObject() != null;
} catch (Exception e) {
return false;
}
} Try / catch
try {
return provider.authenticate(refreshRequest);
} catch (OAuth2AuthenticationException e) {
if (OAuth2ErrorCodes.INVALID_DPOP_PROOF.equals(e.getError().getErrorCode())) {
// regenerate the DPoP proof with a valid jwk header and retry once
return regenerateProofAndRetry();
}
throw e;
} Prevention
- Always build DPoP proofs with nimbus-jose-jwt's header.setJWK(publicJWK.toPublicJWK()).
- Keep client and server JOSE library versions compatible to avoid JWK serialization drift.
- Log (client-side) the proof header keys before sending so missing jwk is caught immediately.
When it happens
Trigger: OAuth2RefreshTokenAuthenticationProvider.authenticate() with a DPoP proof JWT whose header lacks 'jwk', contains a malformed JWK, or whose JSON cannot be parsed (the catch block swallows the parse exception and jwk remains null).
Common situations: Client DPoP library not embedding the public key in the proof header (per RFC 9449); key serialization bugs producing invalid JWK JSON; proofs forwarded through proxies stripping headers; mismatched JOSE libraries on client and server.
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
- invalid_dpop_proof
- Unsupported alg parameter in JWS Header: ${algorithm.getName
- Missing jwk parameter in JWS Header.
- missing_signature_verifier
- INVALID_CLIENT
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/ffd5770440a85c2e.
Report an issue: GitHub.