apereo/cas · error · CredentialsException
Invalid token:
Error message
Invalid token:
What it means
Thrown by the OAuth 2.0 refresh-token authenticator when the submitted refresh_token cannot be validated: it is not found in the ticket registry, has expired, or its clientId does not match the authenticated client. The authenticator returns null instead of an OAuth20RefreshToken in that case and rejects the credentials.
Solutions
- Confirm the client_id sent with the refresh request exactly matches the one used when the refresh token was issued
- Check the refresh token expiration (cas.authn.oauth.refreshToken.timeToKillInSeconds) and registry TTLs; issue a new token via authorization code if expired
- Verify all CAS nodes share the same ticket registry (e.g. Redis/Mongo) so tokens issued by one node are visible to others
- Log/inspect the token string for truncation or URL-encoding corruption in transit
Example fix
// before POST /cas/oauth2.0/token grant_type=refresh_token&client_id=NEW_ID&refresh_token=... // after POST /cas/oauth2.0/token grant_type=refresh_token&client_id=ORIGINAL_ID&refresh_token=...
Defensive patterns
Strategy: try-catch
Try / catch
try {
await refreshToken(clientId, token);
} catch (e) {
if (String(e.message).startsWith('Invalid token')) {
// fall back to a full authorization-code flow to obtain a new refresh token
}
} Prevention
- Always send the same client_id used at token issuance
- Refresh proactively before the token TTL expires
- Use a shared, persistent ticket registry in clustered deployments
When it happens
Trigger: Calling the token endpoint with grant_type=refresh_token where the token was revoked/expired in the ticket registry, the token string is wrong or truncated, or client_id in the request (basic auth or form param) differs from the clientId stored on the refresh token.
Common situations: Ticket registry cleanup/eviction (e.g. Redis or in-memory registry restarted, short refresh-token timeout); rotating client credentials and sending the new client_id with an old token; load balancer pointing to a different CAS node backed by a non-shared registry.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Invalid token:
- No authentication found for ticket
- Failed to acquire access token
- Invalid client credentials provided for registered service:
- Code verification method is unrecognized:
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/d86dd16c1675a406.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/authenticator/OAuth20RefreshTokenAuthenticator.java:88
return false;
}
@Override
protected void validateCredentials(final UsernamePasswordCredentials credentials,
final OAuthRegisteredService registeredService,
final CallContext callContext,
final OAuth20ClientAuthenticationMethods authnMethod) {
val token = credentials.getPassword();
LOGGER.trace("Received refresh token [{}] for authentication", token);
val refreshToken = FunctionUtils.doAndHandle(() -> {
val state = getTicketRegistry().getTicket(token, OAuth20RefreshToken.class);
return state == null || state.isExpired() ? null : state;
});
val clientId = credentials.getUsername();
if (refreshToken == null || refreshToken.isExpired() || !Strings.CI.equals(refreshToken.getClientId(), clientId)) {
LOGGER.error("Refresh token [{}] is either not found in the ticket registry, has expired or does not belong to the client [{}]", token, clientId);
throw new CredentialsException("Invalid token: " + token);
}
}
}
View on GitHub (pinned to e7288fc434)