{"record":{"id":"726c894298a35c12","repo":"spring-projects/spring-security","slug":"invalid-dpop-proof","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":"config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/DefaultOAuth2TokenCustomizers.java","lineNumber":107,"sourceCode":"\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add 'cnf' claim for OAuth 2.0 Demonstrating Proof of Possession (DPoP)\n\t\tJwt dPoPProofJwt = tokenContext.get(OAuth2TokenContext.DPOP_PROOF_KEY);\n\t\tif (OAuth2TokenType.ACCESS_TOKEN.equals(tokenContext.getTokenType()) && dPoPProofJwt != null) {\n\t\t\tJWK jwk = null;\n\t\t\t@SuppressWarnings(\"unchecked\")\n\t\t\tMap<String, Object> jwkJson = (Map<String, Object>) dPoPProofJwt.getHeaders().get(\"jwk\");\n\t\t\ttry {\n\t\t\t\tjwk = JWK.parse(jwkJson);\n\t\t\t}\n\t\t\tcatch (Exception ignored) {\n\t\t\t}\n\t\t\tif (jwk == null) {\n\t\t\t\tOAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_DPOP_PROOF,\n\t\t\t\t\t\t\"jwk header is missing or invalid.\", null);\n\t\t\t\tthrow new OAuth2AuthenticationException(error);\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tString sha256Thumbprint = jwk.computeThumbprint().toString();\n\t\t\t\tif (cnfClaims == null) {\n\t\t\t\t\tcnfClaims = new HashMap<>();\n\t\t\t\t}\n\t\t\t\tcnfClaims.put(\"jkt\", sha256Thumbprint);\n\t\t\t}\n\t\t\tcatch (Exception ex) {\n\t\t\t\tOAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,\n\t\t\t\t\t\t\"Failed to compute SHA-256 Thumbprint for DPoP Proof PublicKey.\", null);\n\t\t\t\tthrow new OAuth2AuthenticationException(error, ex);\n\t\t\t}\n\t\t}\n\n\t\tif (!CollectionUtils.isEmpty(cnfClaims)) {\n\t\t\tclaims.put(\"cnf\", cnfClaims);","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/DefaultOAuth2TokenCustomizers.java#L89-L125","documentation":"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).","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before: DPoP proof without jwk header\n{\"typ\":\"dpop+jwt\",\"alg\":\"ES256\"}\n// after: include the public key in the jwk header\n{\"typ\":\"dpop+jwt\",\"alg\":\"ES256\",\"jwk\":{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"...\",\"y\":\"...\"}}","handlingStrategy":"validation","validationCode":"boolean dpopProofHasJwk(String proofJwt) {\n    String[] parts = proofJwt.split(\"\\\\.\");\n    if (parts.length != 3) return false;\n    try {\n        String header = new String(Base64.getUrlDecoder().decode(parts[0]), StandardCharsets.UTF_8);\n        return header.contains(\"\\\"jwk\\\"\") && header.contains(\"\\\"kty\\\"\");\n    } catch (IllegalArgumentException e) {\n        return false;\n    }\n}","typeGuard":"boolean isPublicJwk(Map<String, Object> jwk) {\n    return jwk != null && jwk.get(\"kty\") instanceof String kty\n        && (kty.equals(\"EC\") || kty.equals(\"RSA\") || kty.equals(\"OKP\"))\n        && !jwk.containsKey(\"d\");\n}","tryCatchPattern":"try {\n    tokenRequest.submit();\n} catch (OAuth2AuthenticationException e) {\n    if (\"invalid_dpop_proof\".equals(e.getError().getErrorCode())) {\n        // regenerate DPoP proof with valid public jwk header and retry once\n    } else {\n        throw e;\n    }\n}","preventionTips":["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."],"tags":["oauth2","dpop","spring-security","jwt"],"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"}