spring-projects/spring-security · error · JwtEncodingException

Failed to encode the JWT due to signing error: Unable to…

Error message

Failed to encode the JWT due to signing error: Unable to convert 'jwk' JOSE header

What it means

NimbusJwtEncoder's header converter tries to parse the user-supplied 'jwk' JOSE header map into a nimbus-jose-jwt JWK via JWK.parse() and wraps any failure in a JwtEncodingException. It means the Map<String,Object> set as the jwk header (e.g. to embed a public key via 'jwk' or trigger key thumbprint embedding) is not a valid JWK JSON structure. The library rejects it early so no malformed JWS is produced.

Solutions

  1. Ensure the map contains at least the mandatory 'kty' member ('RSA', 'EC', 'oct') with string values, per RFC 7517.
  2. Instead of a hand-built map, serialize a real nimbus JWK: jwk.toJSONObject() and pass that map, guaranteeing structural validity.
  3. Inspect the cause via getCause() — JWK.parse reports the missing/invalid member.
  4. If you only need kid/thumbprint, use .keyId(...) or .jwk(URI/ thumbprint) options rather than embedding a raw jwk map.

Example fix

// before
Map<String, Object> jwk = Map.of("key_type", "RSA", "n", n, "e", e);
// after
Map<String, Object> jwk = rsaKey.toJSONObject(); // has "kty":"RSA", "n", "e"
Defensive patterns

Strategy: validation

Validate before calling

// validate a jwk header map before passing to JwsHeader.with(...).jwk(map)
if (!(jwk.get("kty") instanceof String kty) || kty.isBlank()) {
    throw new IllegalArgumentException("jwk header map missing required 'kty'");
}
JWK.parse(jwk); // dry-run parse; throws if structure invalid

Try / catch

try {
    token = jwtEncoder.encode(params);
} catch (JwtEncodingException ex) {
    if (ex.getMessage().contains("'jwk' JOSE header")) {
        throw new IllegalArgumentException("Embedded jwk header is not a valid JWK", ex);
    }
    throw ex;
}

Prevention

When it happens

Trigger: Calling JwsHeader.with(...).jwk(Map<String,Object>) with a map that lacks required JWK fields ('kty'), contains values of wrong types (e.g. numbers where strings are required), or nested structures JWK.parse() cannot read; passing a full private-key map with unsupported extra members.

Common situations: Copying a PEM or X.509 structure into the jwk header instead of a proper JWK; building the map from a properties file where 'kty' was dropped; using snake_case or wrong parameter names (e.g. 'key_type' instead of 'kty'); serializing a JWK to JSON but double-decoding it so values arrive as nested JSON strings.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtEncoder.java:303

		}
	}

	private static JWSHeader convert(JwsHeader headers) {
		JwsAlgorithm algorithm = headers.getAlgorithm();
		Assert.notNull(algorithm, "JWS header algorithm must not be null");
		JWSHeader.Builder builder = new JWSHeader.Builder(JWSAlgorithm.parse(algorithm.getName()));

		if (headers.getJwkSetUrl() != null) {
			builder.jwkURL(convertAsURI(JoseHeaderNames.JKU, headers.getJwkSetUrl()));
		}

		Map<String, Object> jwk = headers.getJwk();
		if (!CollectionUtils.isEmpty(jwk)) {
			try {
				builder.jwk(JWK.parse(jwk));
			}
			catch (Exception ex) {
				throw new JwtEncodingException(String.format(ENCODING_ERROR_MESSAGE_TEMPLATE,
						"Unable to convert '" + JoseHeaderNames.JWK + "' JOSE header"), ex);
			}
		}

		String keyId = headers.getKeyId();
		if (StringUtils.hasText(keyId)) {
			builder.keyID(keyId);
		}

		if (headers.getX509Url() != null) {
			builder.x509CertURL(convertAsURI(JoseHeaderNames.X5U, headers.getX509Url()));
		}

		List<String> x509CertificateChain = headers.getX509CertificateChain();
		if (!CollectionUtils.isEmpty(x509CertificateChain)) {
			List<Base64> x5cList = new ArrayList<>();
			x509CertificateChain.forEach((x5c) -> x5cList.add(new Base64(x5c)));
			if (!x5cList.isEmpty()) {

View on GitHub (pinned to 96852e8860)