spring-projects/spring-security · error · IllegalStateException

Could not coerce + source + into a URI String

Error message

Could not coerce + source + into a URI String

What it means

MappedJwtClaimSetConverter's issuer conversion found an `iss` claim that looks like a URI (contains ':') but cannot be parsed by java.net.URI, so it throws this IllegalStateException. The converter normalizes the issuer claim to a URI string; an unparseable value means the token's issuer claim is malformed.

Solutions

  1. Fix the authorization server's issuer setting to a valid absolute URI (RFC 3986), e.g. https://issuer.example.com.
  2. If the token comes from a third party you can't fix, replace the default converter: MappedJwtClaimSetConverter.withDefaults(Map.of("iss", claim -> desiredValue)) to normalize or drop the claim.
  3. Sanitize/validate issuer claims at token creation time so malformed values never reach the decoder.

Example fix

// before
Jwt.withTokenValue(token).claim("iss", "my issuer: v1") // invalid URI
// after
Jwt.withTokenValue(token).claim("iss", "https://my-issuer.example.com")
// or override conversion:
// MappedJwtClaimSetConverter.withDefaults(Map.of("iss", claims -> "https://my-issuer.example.com"))
Defensive patterns

Strategy: validation

Validate before calling

Object iss = jwt.getClaims().get("iss");
if (iss instanceof String s && s.contains(":")) {
    try { new URI(s); } catch (URISyntaxException e) { /* reject/normalize before decoding */ }
}

Type guard

boolean isValidIssuerUri(Object iss) {
    if (!(iss instanceof String s) || !s.contains(":")) return false;
    try { new URI(s); return true; } catch (URISyntaxException e) { return false; }
}

Try / catch

try { jwtDecoder.decode(token); }
catch (IllegalStateException e) {
    if (e.getMessage().startsWith("Could not coerce")) { /* reject token: malformed iss claim */ }
}

Prevention

When it happens

Trigger: MappedJwtClaimSetConverter.convert invoked on a Jwt whose `iss` claim is a String containing ':' (e.g. 'http://:bad url', values with illegal characters/spaces) and new URI((String) source) throws URISyntaxException.

Common situations: Authorization server misconfigured with an issuer containing spaces or invalid characters; claims smuggled/overwritten by custom token enhancers; tests constructing Jwt with arbitrary iss strings like 'test: value'.

Understand the failure class

Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.

Related errors


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

Appendix: source

Thrown at oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/MappedJwtClaimSetConverter.java:145

		Instant result = (Instant) CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, INSTANT_TYPE_DESCRIPTOR);
		Assert.state(result != null, () -> "Could not coerce " + source + " into an Instant");
		return result;
	}

	private static @Nullable String convertIssuer(Object source) {
		if (source == null) {
			return null;
		}
		URL result = (URL) CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, URL_TYPE_DESCRIPTOR);
		if (result != null) {
			return result.toExternalForm();
		}
		if (source instanceof String && ((String) source).contains(":")) {
			try {
				return new URI((String) source).toString();
			}
			catch (Exception ex) {
				throw new IllegalStateException("Could not coerce " + source + " into a URI String", ex);
			}
		}
		return (String) CONVERSION_SERVICE.convert(source, OBJECT_TYPE_DESCRIPTOR, STRING_TYPE_DESCRIPTOR);
	}

	@Override
	public Map<String, Object> convert(Map<String, Object> claims) {
		Assert.notNull(claims, "claims cannot be null");
		Map<String, Object> mappedClaims = new HashMap<>(claims);
		for (Map.Entry<String, Converter<Object, ? extends @Nullable Object>> entry : this.claimTypeConverters
			.entrySet()) {
			String claimName = entry.getKey();
			Converter<Object, ? extends @Nullable Object> converter = entry.getValue();
			Object claim = claims.get(claimName);
			@SuppressWarnings("NullAway")
			Object mappedClaim = converter.convert(claim);
			mappedClaims.compute(claimName, (key, value) -> mappedClaim);
		}

View on GitHub (pinned to 96852e8860)