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

  1. 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)).
  2. Regenerate the DPoP proof so the jwk matches the key that signed the proof (EC/RSA types must correspond).
  3. Verify the header parses as a JWK (no truncation or re-encoding damage in transit).
  4. 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

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


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