{"record":{"id":"ffd5770440a85c2e","repo":"spring-projects/spring-security","slug":"invalid-dpop-proof-ffd577","errorCode":"invalid_dpop_proof","errorMessage":"jwk header is missing or invalid.","messagePattern":"jwk header is missing or invalid\\.","errorType":"error_code","errorClass":"OAuth2AuthenticationException","httpStatus":400,"severity":"error","filePath":"oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java","lineNumber":314,"sourceCode":"\n\t@Override\n\tpublic boolean supports(Class<?> authentication) {\n\t\treturn OAuth2RefreshTokenAuthenticationToken.class.isAssignableFrom(authentication);\n\t}\n\n\tprivate static void verifyDPoPProofPublicKey(Jwt dPoPProof, ClaimAccessor accessTokenClaims) {\n\t\tJWK jwk = null;\n\t\t@SuppressWarnings(\"unchecked\")\n\t\tMap<String, Object> jwkJson = (Map<String, Object>) dPoPProof.getHeaders().get(\"jwk\");\n\t\ttry {\n\t\t\tjwk = JWK.parse(jwkJson);\n\t\t}\n\t\tcatch (Exception ignored) {\n\t\t}\n\t\tif (jwk == null) {\n\t\t\tOAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,\n\t\t\t\t\t\"jwk header is missing or invalid.\", null);\n\t\t\tthrow new OAuth2AuthenticationException(error);\n\t\t}\n\n\t\tString jwkThumbprint;\n\t\ttry {\n\t\t\tjwkThumbprint = jwk.computeThumbprint().toString();\n\t\t}\n\t\tcatch (Exception ex) {\n\t\t\tOAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,\n\t\t\t\t\t\"Failed to compute SHA-256 Thumbprint for jwk.\", null);\n\t\t\tthrow new OAuth2AuthenticationException(error);\n\t\t}\n\n\t\tString jwkThumbprintClaim = null;\n\t\tMap<String, Object> confirmationMethodClaim = accessTokenClaims.getClaimAsMap(\"cnf\");\n\t\tif (!CollectionUtils.isEmpty(confirmationMethodClaim) && confirmationMethodClaim.containsKey(\"jkt\")) {\n\t\t\tjwkThumbprintClaim = (String) confirmationMethodClaim.get(\"jkt\");\n\t\t}\n\t\tif (jwkThumbprintClaim == null) {","sourceCodeStart":296,"sourceCodeEnd":332,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2RefreshTokenAuthenticationProvider.java#L296-L332","documentation":"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.'","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nJWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256).build(); // no jwk\n// after\nJWSHeader header = new JWSHeader.Builder(JWSAlgorithm.ES256)\n    .jwk(publicJWK.toPublicJWK())\n    .type(JOSEObjectType.JWT)\n    .build();","handlingStrategy":"try-catch","validationCode":"// client-side: ensure the DPoP proof carries the jwk header before sending\nSignedJWT proof = new SignedJWT(\n    new JWSHeader.Builder(alg).jwk(publicKey.toPublicJWK()).type(JOSEObjectType.JWT).build(),\n    claimsSet);\nObjects.requireNonNull(proof.getHeader().getJWK(), \"DPoP proof must embed jwk header\");","typeGuard":"boolean hasValidJwkHeader(JWSHeader header) {\n    try {\n        return header != null && header.getJWK() != null\n            && header.getJWK().toJSONObject() != null;\n    } catch (Exception e) {\n        return false;\n    }\n}","tryCatchPattern":"try {\n    return provider.authenticate(refreshRequest);\n} catch (OAuth2AuthenticationException e) {\n    if (OAuth2ErrorCodes.INVALID_DPOP_PROOF.equals(e.getError().getErrorCode())) {\n        // regenerate the DPoP proof with a valid jwk header and retry once\n        return regenerateProofAndRetry();\n    }\n    throw e;\n}","preventionTips":["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."],"tags":["oauth2","dpop","jwt","token-endpoint"],"backgroundTag":"invalid-argument-format","analyzedSha":"96852e8860138a482cb13d1479573f24ff6443c6","analyzedAt":"2026-09-10T23:25:23.477Z","contentChangedAt":"2026-09-10T23:25:23.477Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}