apereo/cas · error · AuthenticationException

Token has expired: and is after

Error message

Token has expired: %s and is after %s

What it means

During JWT claim validation, the parser compares the token's exp claim to the current time allowing a configurable clock skew (cas.authn.oidc.core.skew). If the expiration time is not after 'now' within the skew, it throws AuthenticationException 'Token has expired: <exp> and is after <now>'.

Solutions

  1. Obtain a fresh token and retry the request
  2. Increase the allowed clock skew via cas.authn.oidc.core.skew if servers' clocks differ mildly
  3. Synchronize clocks with NTP on both issuer and CAS hosts
  4. Implement token refresh in the client before expiry instead of reusing expired tokens

Example fix

// before
cas.authn.oidc.core.skew=PT0S
// after
cas.authn.oidc.core.skew=PT2M
Defensive patterns

Strategy: validation

Validate before calling

Date exp = claimsSet.getExpirationTime();
if (exp != null && exp.before(new Date())) {
    throw new IllegalStateException("Token expired, refresh before calling");
}

Try / catch

try {
    return parser.claims(token);
} catch (AuthenticationException e) {
    // trigger token refresh flow and retry once
}

Prevention

When it happens

Trigger: validateClaims (called from parseAuthorizationHeader) receives a JWTClaimsSet whose expirationTime is non-null and has passed (minus allowed clock skew).

Common situations: Client cached a token beyond its lifetime; long-running service replaying an old assertion; severe clock drift between issuer and CAS server; token issued for a short-lived flow (e.g. OIDC access token) reused later.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-heimdall/src/main/java/org/apereo/cas/heimdall/engine/DefaultAuthorizationPrincipalParser.java:137

            return Optional.empty();
        }
    }

    protected JWTClaimsSet buildClaimSetFromAuthentication(final String token) throws Throwable {
        val usernamePass = Splitter.on(':').splitToList(EncodingUtils.decodeBase64ToString(token));
        val credential = new UsernamePasswordCredential(usernamePass.getFirst(), usernamePass.getLast());
        val authResultBuilder = authenticationSystemSupport.handleInitialAuthenticationTransaction(null, credential);
        val authentication = authenticationSystemSupport.finalizeAllAuthenticationTransactions(authResultBuilder, null);
        val claimsMap = buildClaimsFromAuthentication(authentication.getAuthentication());
        return JWTClaimsSet.parse(claimsMap);
    }

    protected JWTClaimsSet validateClaims(final JWTClaimsSet claimsSet) {
        val maxClockSkew = Beans.newDuration(casProperties.getAuthn().getOidc().getCore().getSkew()).toSeconds();
        val now = new Date();
        val exp = claimsSet.getExpirationTime();
        if (exp != null && !DateUtils.isAfter(exp, now, maxClockSkew)) {
            throw new AuthenticationException("Token has expired: %s and is after %s".formatted(exp, now));
        }
        val nbf = claimsSet.getNotBeforeTime();
        if (nbf != null && !DateUtils.isBefore(nbf, now, maxClockSkew)) {
            throw new AuthenticationException("Token cannot be used before %s and now is %s".formatted(nbf, now));
        }
        return claimsSet;
    }

    private Optional<JWTClaimsSet> getJwtClaimsSetFromAccessToken(final String token) {
        try {
            val ticket = ticketRegistry.getTicket(token, OAuth20AccessToken.class);
            FunctionUtils.throwIf(ticket == null || ticket.isExpired(),
                () -> new AuthenticationException("Token %s is not found or has expired".formatted(token)));
            val claimsMap = buildClaimsFromAuthentication(ticket.getAuthentication());
            claimsMap.putAll(ticket.getClaims());
            claimsMap.put(OAuth20Constants.SCOPE, ticket.getScopes());
            claimsMap.put(OAuth20Constants.TOKEN, token);
            return Optional.of(JWTClaimsSet.parse(claimsMap));

View on GitHub (pinned to e7288fc434)