{"record":{"id":"95455cb19882c633","repo":"apereo/cas","slug":"invalid-token","errorCode":null,"errorMessage":"Invalid token: ","messagePattern":"Invalid token: ","errorType":"exception","errorClass":"CredentialsException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/authenticator/OAuth20ProofKeyCodeExchangeAuthenticator.java","lineNumber":90,"sourceCode":"                                       final OAuthRegisteredService registeredService,\n                                       final CallContext callContext,\n                                       final OAuth20ClientAuthenticationMethods authnMethod) {\n        val clientSecret = getRequestParameterResolver().resolveClientIdAndClientSecret(callContext).getRight();\n        if (!getClientSecretValidator().validate(registeredService, clientSecret)) {\n            throw new CredentialsException(\"Client Credentials provided is not valid for service: \" + registeredService.getName());\n        }\n        val codeVerifier = getRequestParameterResolver()\n            .resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE_VERIFIER)\n            .map(String::valueOf).orElse(StringUtils.EMPTY);\n        val code = getRequestParameterResolver()\n            .resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE)\n            .map(String::valueOf).orElse(StringUtils.EMPTY);\n\n        LOGGER.debug(\"Received PKCE code verifier [{}] along with code [{}]\", codeVerifier, code);\n        val token = getTicketRegistry().getTicket(code, OAuth20Code.class);\n        if (token == null || token.isExpired()) {\n            LOGGER.error(\"Provided code [{}] is either not found in the ticket registry or has expired\", code);\n            throw new CredentialsException(\"Invalid token: \" + code);\n        }\n\n        val method = StringUtils.defaultIfEmpty(token.getCodeChallengeMethod(), \"plain\");\n        val hash = calculateCodeVerifierHash(method, codeVerifier);\n        if (!hash.equalsIgnoreCase(token.getCodeChallenge())) {\n            LOGGER.error(\"Code verifier [{}] does not match the challenge [{}]\", hash, token.getCodeChallenge());\n            throw new CredentialsException(\"Code verification does not match the challenge assigned to: \" + token.getId());\n        }\n        LOGGER.debug(\"Validated code verifier using verification method [{}]\", method);\n    }\n}\n","sourceCodeStart":72,"sourceCodeEnd":102,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/authenticator/OAuth20ProofKeyCodeExchangeAuthenticator.java#L72-L102","documentation":"During PKCE token exchange, the authenticator looks up the authorization code in the ticket registry as an OAuth20Code. If the ticket is absent or expired, it cannot verify the code challenge, so it throws CredentialsException 'Invalid token: <code>'. This protects against replay of stale, already-used, or forged codes.","triggerScenarios":"validateCredentials: getTicketRegistry().getTicket(code, OAuth20Code.class) returns null or token.isExpired() is true — the code was already redeemed (single-use), expired past its TTL, issued by a different CAS node/registry, or never existed.","commonSituations":"Client retrying the token exchange after an earlier successful redemption consumed the one-time code; slow clients exceeding the code expiration TTL; load-balanced CAS cluster nodes pointing at different/backing-out-of-sync ticket registries; clock skew affecting expiry; client sending the wrong parameter as 'code'.","solutions":["Restart the flow: obtain a fresh authorization code and exchange it immediately — codes are single-use and short-lived","Ensure all CAS nodes share the same ticket registry (e.g. Redis/Mongo) so any node can find the code","Check that the client exchanges the code within the expiration window (code.timeToKillInSeconds) and fix client-side delays","Verify clock synchronization (NTP) across CAS nodes and ticket registry stores","Confirm the client sends the correct code parameter value in the token request"],"exampleFix":"// client flow before\ncode = getAuthorizationCode(); ...slow work...; exchange(code)  // expired\n\n// after\ncode = getAuthorizationCode(); exchange(code)  // immediately redeem","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n    tokenResponse = oauthClient.exchangeCode(code, verifier);\n} catch (OAuthException e) {\n    // treat 'Invalid token' as expired/replayed: restart authorization from scratch\n    code = null;\n    startNewAuthorizationRequest();\n}","preventionTips":["Exchange authorization codes immediately; they are single-use and short-lived","Never retry a token exchange with the same code after a successful response — fetch a new code","Ensure all CAS cluster nodes share one ticket registry backend","Synchronize clocks (NTP) across nodes to avoid premature expiry checks"],"tags":["oauth2","pkce","ticket-registry","expired-token"],"backgroundTag":"jwt-token-expired","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"}