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
- 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
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
- 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
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Code verification method is unrecognized:
- Client Credentials provided is not valid for service:
- Code verification does not match the challenge assigned to:
- Invalid token:
- No authentication found for ticket
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)