apereo/cas · warning

Provided refresh token

Error message

Provided refresh token [{}] cannot be found in the registry or has expired

What it means

OAuth20RefreshTokenGrantTypeTokenRequestValidator retrieves the presented refresh_token from the TicketRegistry as an OAuth20RefreshToken. If the ticket is unknown or has expired, getTicket throws InvalidTicketException; the validator catches it, logs this warning, and returns false so the refresh grant fails.

Solutions

  1. Increase the refresh token expiration (cas.authn.oauth.refresh-token.time-to-kill-in-seconds) to cover the client's refresh interval.
  2. Configure a persistent ticket registry (Redis, Hazelcast, JDBC) so tokens survive CAS restarts.
  3. Have the client handle this failure by re-running the full authorization flow to obtain a new refresh token.
  4. Check the ticket registry backend for eviction/cleanup settings that may purge tickets prematurely.

Example fix

// before (application.properties)
cas.authn.oauth.refresh-token.time-to-kill-in-seconds=3600
// after
cas.authn.oauth.refresh-token.time-to-kill-in-seconds=2592000
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const tokens = await cas.refreshToken(refreshToken);
} catch (e) {
  if (e.status === 400 || e.error === 'invalid_grant') {
    // token expired/unknown: fall back to full authorization flow
    tokens = await cas.authorizeAndAuthenticate();
  } else { throw e; }
}

Prevention

When it happens

Trigger: A token request with grant_type=refresh_token supplies a refresh token string that no longer exists in the ticket registry (expired TTL, registry restart with an in-memory registry, eviction, or a fabricated/typo'd token).

Common situations: Refresh token lifetime (cas.authn.oauth.refresh-token.timeToKillInSeconds) shorter than the client's reuse window; CAS restarted with the default in-memory ticket registry; ticket-registry backend (Redis/Hazelcast/JDBC) evicted or flushed entries; clock skew causing early expiry.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-oauth-core-api/src/main/java/org/apereo/cas/support/oauth/validator/token/OAuth20RefreshTokenGrantTypeTokenRequestValidator.java:51

    protected boolean validateInternal(final WebContext context, final String grantType,
                                       final ProfileManager manager, final UserProfile uProfile) throws Throwable {
        val configurationContext = getConfigurationContext().getObject();
        val callContext = new CallContext(context, configurationContext.getSessionStore());
        val clientId = configurationContext.getRequestParameterResolver()
            .resolveClientIdAndClientSecret(callContext).getLeft();
        val refreshTokenResult = configurationContext.getRequestParameterResolver()
            .resolveRequestParameter(context, OAuth20Constants.REFRESH_TOKEN);
        if (refreshTokenResult.isEmpty() || clientId.isEmpty()) {
            return false;
        }

        var refreshToken = (OAuth20RefreshToken) null;
        val token = refreshTokenResult.get();
        try {
            refreshToken = configurationContext.getTicketRegistry().getTicket(token, OAuth20RefreshToken.class);
            LOGGER.trace("Found valid refresh token [{}] in the registry", refreshToken);
        } catch (final InvalidTicketException e) {
            LOGGER.warn("Provided refresh token [{}] cannot be found in the registry or has expired", token);
            return false;
        }

        LOGGER.debug("Received grant type [{}] with client id [{}]", grantType, clientId);
        val registeredService = OAuth20Utils.getRegisteredOAuthServiceByClientId(
            configurationContext.getServicesManager(), clientId);
        val audit = AuditableContext.builder()
            .registeredService(registeredService)
            .build();
        val accessResult = configurationContext.getRegisteredServiceAccessStrategyEnforcer().execute(audit);
        accessResult.throwExceptionIfNeeded();

        if (!isGrantTypeSupportedBy(Objects.requireNonNull(registeredService), grantType)) {
            LOGGER.warn("Requested grant type [{}] is not authorized by service definition [{}]",
                grantType, Objects.requireNonNull(registeredService).getServiceId());
            return false;
        }

View on GitHub (pinned to e7288fc434)