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

  1. Confirm the client_id sent with the refresh request exactly matches the one used when the refresh token was issued
  2. Check the refresh token expiration (cas.authn.oauth.refreshToken.timeToKillInSeconds) and registry TTLs; issue a new token via authorization code if expired
  3. Verify all CAS nodes share the same ticket registry (e.g. Redis/Mongo) so tokens issued by one node are visible to others
  4. 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

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.

Related errors


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)