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

  1. 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
  2. If keys differ by design, use the JwtBuilder overload that accepts explicit cipher executor / service keys instead of the default-global-keys path
  3. 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
  4. 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

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


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)