{"record":{"id":"c20dc7110878c2fe","repo":"spring-projects/spring-security","slug":"missing-jwk-parameter-in-jws-header","errorCode":null,"errorMessage":"Missing jwk parameter in JWS Header.","messagePattern":"Missing jwk parameter in JWS Header\\.","errorType":"exception","errorClass":"BadJwtException","httpStatus":null,"severity":"error","filePath":"oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofJwtDecoderFactory.java","lineNumber":188,"sourceCode":"\t\tjwtProcessor.setJWSTypeVerifier(DPOP_TYPE_VERIFIER);\n\t\tjwtProcessor.setJWSKeySelector(jwsKeySelector());\n\t\t// Override the default Nimbus claims set verifier and use jwtValidatorFactory for\n\t\t// claims validation\n\t\tjwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {\n\t\t});\n\t\treturn new NimbusJwtDecoder(jwtProcessor);\n\t}\n\n\tprivate static JWSKeySelector<SecurityContext> jwsKeySelector() {\n\t\treturn (header, context) -> {\n\t\t\tJWSAlgorithm algorithm = header.getAlgorithm();\n\t\t\tif (!JWSAlgorithm.Family.RSA.contains(algorithm) && !JWSAlgorithm.Family.EC.contains(algorithm)) {\n\t\t\t\tthrow new BadJwtException(\"Unsupported alg parameter in JWS Header: \" + algorithm.getName());\n\t\t\t}\n\n\t\t\tJWK jwk = header.getJWK();\n\t\t\tif (jwk == null) {\n\t\t\t\tthrow new BadJwtException(\"Missing jwk parameter in JWS Header.\");\n\t\t\t}\n\t\t\tif (jwk.isPrivate()) {\n\t\t\t\tthrow new BadJwtException(\"Invalid jwk parameter in JWS Header.\");\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tif (JWSAlgorithm.Family.RSA.contains(algorithm) && jwk instanceof RSAKey rsaKey) {\n\t\t\t\t\treturn Collections.singletonList(rsaKey.toRSAPublicKey());\n\t\t\t\t}\n\t\t\t\telse if (JWSAlgorithm.Family.EC.contains(algorithm) && jwk instanceof ECKey ecKey) {\n\t\t\t\t\treturn Collections.singletonList(ecKey.toECPublicKey());\n\t\t\t\t}\n\t\t\t}\n\t\t\tcatch (JOSEException ex) {\n\t\t\t\tthrow new BadJwtException(\"Invalid jwk parameter in JWS Header.\");\n\t\t\t}\n\n\t\t\tthrow new BadJwtException(\"Invalid alg / jwk parameter in JWS Header: alg=\" + algorithm.getName()","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/spring-projects/spring-security/blob/96852e8860138a482cb13d1479573f24ff6443c6/oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/DPoPProofJwtDecoderFactory.java#L170-L206","documentation":"This BadJwtException is thrown by DPoPProofJwtDecoderFactory's jwsKeySelector when the DPoP proof JWT's JWS header does not embed the public JSON Web Key (jwk header parameter). RFC 9449 requires DPoP proofs to carry the signing public key in the header so the resource server can verify the signature without external key material.","triggerScenarios":"Decoding a DPoP proof JWT that was signed without the \"jwk\" header parameter set — e.g. a hand-built JWT via JWSHeader.Builder without jwk(jwk), or a client library that doesn't embed the key.","commonSituations":"Custom JWT code that omits the jwk header (normal JWTs embed keys elsewhere), a client migration where the DPoP library changed, or a proof generated by another tool that references the key by kid instead of embedding it.","solutions":["Set the jwk header when signing the proof: new JWSHeader.Builder(alg).jwk(publicJwk.toPublicJWK()).build().","Ensure the embedded JWK is the public key (private keys are also rejected separately).","Catch BadJwtException on decode and respond with an invalid_dpop_proof OAuth error.","If using a DPoP client library, update/configure it so the public key is embedded per RFC 9449."],"exampleFix":"// before\nJWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build(); // no jwk\n// after\nJWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT)\n        .jwk(rsaPublicJwk.toPublicJWK()).build();","handlingStrategy":"validation","validationCode":"if (header.getJWK() == null) {\n    throw new IllegalArgumentException(\"DPoP proof header must embed the public jwk\");\n}","typeGuard":"boolean embedsPublicJwk(JWSHeader header) {\n    return header.getJWK() != null && !header.getJWK().isPrivate();\n}","tryCatchPattern":"try {\n    Jwt jwt = decoder.decode(proof);\n} catch (BadJwtException ex) {\n    if (ex.getMessage().contains(\"Missing jwk\")) {\n        logger.error(\"Client omitted jwk header in DPoP proof\");\n    }\n}","preventionTips":["Always call .jwk(publicKeyJwk) on the JWSHeader.Builder when generating proofs.","Embed only the PUBLIC key — private keys are rejected too.","Use an RFC 9449-compliant DPoP library instead of hand-rolled JWT signing.","Contract-test that generated proofs include the jwk header."],"tags":["dpop","jwt","jwk","oauth2","spring-security"],"backgroundTag":"missing-jwk-header","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"}