apereo/cas · error · AuthenticationException

Token cannot be used before

Error message

Token cannot be used before %s and now is %s

What it means

Also in validateClaims: if the token carries an nbf (not-before) claim that is still in the future beyond the allowed clock skew, the parser rejects it with AuthenticationException 'Token cannot be used before <nbf> and now is <now>'. The token is structurally valid but used too early.

Solutions

  1. Retry the request after the nbf timestamp passes
  2. Fix clock synchronization (NTP) between token issuer and CAS server
  3. Increase cas.authn.oidc.core.skew to tolerate modest drift
  4. Have the issuer correct nbf generation if tokens are minted with a future not-before unintentionally

Example fix

// before: issuer sets nbf far in future
long nbf = now + 3600;
// after
long nbf = now - 60;
Defensive patterns

Strategy: retry

Validate before calling

Date nbf = claimsSet.getNotBeforeTime();
if (nbf != null && nbf.after(new Date())) {
    throw new IllegalStateException("Token not yet valid, wait until " + nbf);
}

Try / catch

try {
    return parser.claims(token);
} catch (AuthenticationException e) {
    // schedule retry after nbf passes
}

Prevention

When it happens

Trigger: validateClaims receives a JWTClaimsSet whose notBeforeTime is non-null and not before now within maxClockSkew — i.e. the request arrives before the token's validity start.

Common situations: Clock skew where the client's clock is behind the CAS server; tokens pre-issued for scheduled/future use presented immediately; misconfigured issuer setting nbf to a future timestamp; distributed systems with unsynchronized clocks.

Related errors


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

Appendix: source

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

    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));
        } catch (final Throwable e) {
            LOGGER.debug(e.getMessage(), LOGGER.isTraceEnabled() ? e : null);
            return Optional.empty();
        }

View on GitHub (pinned to e7288fc434)