apereo/cas · error · CredentialsException

Invalid token:

Error message

Invalid token: 

What it means

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.

Solutions

  1. Restart the flow: obtain a fresh authorization code and exchange it immediately — codes are single-use and short-lived
  2. Ensure all CAS nodes share the same ticket registry (e.g. Redis/Mongo) so any node can find the code
  3. Check that the client exchanges the code within the expiration window (code.timeToKillInSeconds) and fix client-side delays
  4. Verify clock synchronization (NTP) across CAS nodes and ticket registry stores
  5. Confirm the client sends the correct code parameter value in the token request

Example fix

// client flow before
code = getAuthorizationCode(); ...slow work...; exchange(code)  // expired

// after
code = getAuthorizationCode(); exchange(code)  // immediately redeem
Defensive patterns

Strategy: try-catch

Try / catch

try {
    tokenResponse = oauthClient.exchangeCode(code, verifier);
} catch (OAuthException e) {
    // treat 'Invalid token' as expired/replayed: restart authorization from scratch
    code = null;
    startNewAuthorizationRequest();
}

Prevention

When it happens

Trigger: 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.

Common situations: 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'.

Understand the failure class

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/95455cb19882c633. Report an issue: GitHub.

Appendix: source

Thrown at support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/authenticator/OAuth20ProofKeyCodeExchangeAuthenticator.java:90

                                       final OAuthRegisteredService registeredService,
                                       final CallContext callContext,
                                       final OAuth20ClientAuthenticationMethods authnMethod) {
        val clientSecret = getRequestParameterResolver().resolveClientIdAndClientSecret(callContext).getRight();
        if (!getClientSecretValidator().validate(registeredService, clientSecret)) {
            throw new CredentialsException("Client Credentials provided is not valid for service: " + registeredService.getName());
        }
        val codeVerifier = getRequestParameterResolver()
            .resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE_VERIFIER)
            .map(String::valueOf).orElse(StringUtils.EMPTY);
        val code = getRequestParameterResolver()
            .resolveRequestParameter(callContext.webContext(), OAuth20Constants.CODE)
            .map(String::valueOf).orElse(StringUtils.EMPTY);

        LOGGER.debug("Received PKCE code verifier [{}] along with code [{}]", codeVerifier, code);
        val token = getTicketRegistry().getTicket(code, OAuth20Code.class);
        if (token == null || token.isExpired()) {
            LOGGER.error("Provided code [{}] is either not found in the ticket registry or has expired", code);
            throw new CredentialsException("Invalid token: " + code);
        }

        val method = StringUtils.defaultIfEmpty(token.getCodeChallengeMethod(), "plain");
        val hash = calculateCodeVerifierHash(method, codeVerifier);
        if (!hash.equalsIgnoreCase(token.getCodeChallenge())) {
            LOGGER.error("Code verifier [{}] does not match the challenge [{}]", hash, token.getCodeChallenge());
            throw new CredentialsException("Code verification does not match the challenge assigned to: " + token.getId());
        }
        LOGGER.debug("Validated code verifier using verification method [{}]", method);
    }
}

View on GitHub (pinned to e7288fc434)