apereo/cas · error · IllegalArgumentException
Unable to validate JWT signature
Error message
Unable to validate JWT signature
What it means
JwtBuilder.unpack() validates incoming JWT tokens. When the token is not parseable with the configured default global signing/encryption keys (defaultTokenCipherExecutor), decode() fails and the code deliberately throws IllegalArgumentException instead of returning a parsed JWT. It signals that the JWT could not be verified against the globally configured CAS token signing keys, i.e. the signature/decryption check failed.
Solutions
- Ensure the JWT is signed/encrypted with the same keys configured for the default token cipher executor (cas.authn.token.crypto.* signing/encryption key settings) on the validating CAS server
- If keys differ by design, use the JwtBuilder overload that accepts explicit cipher executor / service keys instead of the default-global-keys path
- Regenerate the JWT from the original source rather than copying/transcoding the token string; verify the compact JWT has its three dot-separated segments intact
- Enable TRACE logging ('Decoding JWT based on default global keys') and confirm defaultTokenCipherExecutor.isEnabled() is true and keys are non-empty; fix configuration if the executor is disabled or misconfigured
Example fix
// before
String json = jwtBuilder.unpack(untrustedJwt); // throws: Unable to validate JWT signature
// after
if (!jwtPattern.matcher(untrustedJwt).matches()) {
throw new IllegalArgumentException("Malformed JWT");
}
// ensure server uses matching keys, then:
String json = jwtBuilder.unpack(untrustedJwt); Defensive patterns
Strategy: validation
Validate before calling
private static final Pattern JWT = Pattern.compile("^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]*$");
if (!JWT.matcher(token).matches()) throw new IllegalArgumentException("Malformed JWT before unpack"); Try / catch
try {
String json = jwtBuilder.unpack(jwt);
} catch (IllegalArgumentException e) {
// signature/decryption verification failed against configured keys
throw new InvalidTokenException("JWT rejected: " + e.getMessage(), e);
} Prevention
- Keep signing/encryption keys synchronized across all CAS nodes and the token-issuing services
- Never hand-edit or URL-decode JWT strings before passing them to unpack
- Version your crypto configuration; when rotating keys, re-issue outstanding tokens
- Enable TRACE logging on JwtBuilder during rollout of key changes
When it happens
Trigger: Calling JwtBuilder.unpack(jwtJson) (directly or via the token ticket validation path) with a JWT that was signed with different keys than the server's cas.authn.token.* default global keys, or a corrupted/re-serialized JWT string that fails cipherExecutor.decode().
Common situations: Keys were rotated or differ between the service generating the token and the CAS server verifying it; JWT was produced before global keys were configured (self-signed/other library keys); token string was truncated, URL-decoded incorrectly, or wrapped/quoted; copy-pasting tokens between environments (dev vs prod).
Related errors
- Token encryption/signing is not enabled explicitly in the…
- Unable to accept the ID token with an invalid [sub] claim
- Unknown authorization header type
- Token has expired: and is after
- Token cannot be used before
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/57490dd4118266bc.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-token-core-api/src/main/java/org/apereo/cas/token/JwtBuilder.java:186
});
val jwt = JWTParser.parse(jwtJson);
if (jwt instanceof SignedJWT || jwt instanceof EncryptedJWT) {
if (service.isPresent()) {
val registeredService = service.get();
LOGGER.trace("Locating service signing and encryption keys for [{}]", registeredService.getServiceId());
if (registeredServiceCipherExecutor.supports(registeredService)) {
LOGGER.trace("Decoding JWT based on keys provided by service [{}]", registeredService.getServiceId());
return parse(registeredServiceCipherExecutor.decode(jwtJson, Optional.of(registeredService)));
}
}
return FunctionUtils.doIf(defaultTokenCipherExecutor.isEnabled(),
() -> {
LOGGER.trace("Decoding JWT based on default global keys");
return parse(defaultTokenCipherExecutor.decode(jwtJson));
}, () -> {
throw new IllegalArgumentException("Unable to validate JWT signature");
}).get();
}
return parse(jwtJson);
});
}
/**
* Build JWT.
*
* @param payload the payload
* @return the jwt
* @throws Throwable the throwable
*/
public String build(final JwtRequest payload) throws Throwable {
Objects.requireNonNull(payload.getIssuer(), "Issuer cannot be undefined");
val targetAudience = new ArrayList<>(payload.getServiceAudience());
FunctionUtils.throwIf(targetAudience.isEmpty() && payload.getRegisteredService().isEmpty(),
() -> new IllegalArgumentException("Service audience cannot be empty"));View on GitHub (pinned to e7288fc434)