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
- Retry the request after the nbf timestamp passes
- Fix clock synchronization (NTP) between token issuer and CAS server
- Increase cas.authn.oidc.core.skew to tolerate modest drift
- 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
- Check nbf before using freshly minted tokens
- Keep issuer and CAS clocks synchronized
- Ask issuer to set nbf slightly in the past when minting
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
- Token has expired: and is after
- Proof iat is in the future
- Proof JWT is too old
- Unable to accept the ID token with an invalid [sub] claim
- Unknown authorization header type
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)