{"record":{"id":"57490dd4118266bc","repo":"apereo/cas","slug":"unable-to-validate-jwt-signature","errorCode":null,"errorMessage":"Unable to validate JWT signature","messagePattern":"Unable to validate JWT signature","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-token-core-api/src/main/java/org/apereo/cas/token/JwtBuilder.java","lineNumber":186,"sourceCode":"            });\n\n            val jwt = JWTParser.parse(jwtJson);\n            if (jwt instanceof SignedJWT || jwt instanceof EncryptedJWT) {\n                if (service.isPresent()) {\n                    val registeredService = service.get();\n                    LOGGER.trace(\"Locating service signing and encryption keys for [{}]\", registeredService.getServiceId());\n                    if (registeredServiceCipherExecutor.supports(registeredService)) {\n                        LOGGER.trace(\"Decoding JWT based on keys provided by service [{}]\", registeredService.getServiceId());\n                        return parse(registeredServiceCipherExecutor.decode(jwtJson, Optional.of(registeredService)));\n                    }\n                }\n\n                return FunctionUtils.doIf(defaultTokenCipherExecutor.isEnabled(),\n                    () -> {\n                        LOGGER.trace(\"Decoding JWT based on default global keys\");\n                        return parse(defaultTokenCipherExecutor.decode(jwtJson));\n                    }, () -> {\n                        throw new IllegalArgumentException(\"Unable to validate JWT signature\");\n                    }).get();\n            }\n            return parse(jwtJson);\n        });\n    }\n\n    /**\n     * Build JWT.\n     *\n     * @param payload the payload\n     * @return the jwt\n     * @throws Throwable the throwable\n     */\n    public String build(final JwtRequest payload) throws Throwable {\n        Objects.requireNonNull(payload.getIssuer(), \"Issuer cannot be undefined\");\n        val targetAudience = new ArrayList<>(payload.getServiceAudience());\n        FunctionUtils.throwIf(targetAudience.isEmpty() && payload.getRegisteredService().isEmpty(),\n            () -> new IllegalArgumentException(\"Service audience cannot be empty\"));","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-token-core-api/src/main/java/org/apereo/cas/token/JwtBuilder.java#L168-L204","documentation":"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.","triggerScenarios":"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().","commonSituations":"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).","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"],"exampleFix":"// before\nString json = jwtBuilder.unpack(untrustedJwt); // throws: Unable to validate JWT signature\n// after\nif (!jwtPattern.matcher(untrustedJwt).matches()) {\n    throw new IllegalArgumentException(\"Malformed JWT\");\n}\n// ensure server uses matching keys, then:\nString json = jwtBuilder.unpack(untrustedJwt);","handlingStrategy":"validation","validationCode":"private static final Pattern JWT = Pattern.compile(\"^[A-Za-z0-9_-]+\\\\.[A-Za-z0-9_-]+\\\\.[A-Za-z0-9_-]*$\");\nif (!JWT.matcher(token).matches()) throw new IllegalArgumentException(\"Malformed JWT before unpack\");","typeGuard":null,"tryCatchPattern":"try {\n    String json = jwtBuilder.unpack(jwt);\n} catch (IllegalArgumentException e) {\n    // signature/decryption verification failed against configured keys\n    throw new InvalidTokenException(\"JWT rejected: \" + e.getMessage(), e);\n}","preventionTips":["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"],"tags":["jwt","signature-validation","crypto","token"],"backgroundTag":"jwt-signature-validation-failed","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}