apereo/cas · warning
Token [ ] has expired
Error message
Token [{}] has expired What it means
JpaPasswordlessTokenRepository.findToken locates the stored passwordless token row but its decoded token is past its expiration time, so the repository logs a warning and returns Optional.empty as if the token did not exist. The user must request a new token.
Solutions
- Have the user request a new token
- Increase cas.authn.passwordless.tokens.time-to-kill if tokens expire too quickly
- Check server clock synchronization (NTP) across CAS nodes and the database
- Clean expired rows from the passwordless token table
Example fix
// before cas.authn.passwordless.tokens.time-to-kill=PT1M // after cas.authn.passwordless.tokens.time-to-kill=PT5M
Defensive patterns
Strategy: fallback
Validate before calling
if (Instant.now().isAfter(token.getExpiration())) requestNewToken();
Try / catch
repository.findToken(id).orElseThrow(() -> new PasswordlessTokenExpiredException()); catch -> prompt re-request
Prevention
- Set a generous time-to-kill for tokens
- Schedule purge jobs for expired rows
- Synchronize clocks via NTP
When it happens
Trigger: A passwordless user submits a token after cas.authn.passwordless.tokens.time-to-kill (or the token's expiration) has elapsed; findToken decodes the JPA row and authnToken.isExpired() is true.
Common situations: User waits too long before entering the emailed/OTP token; clock skew between CAS nodes; token expiration configured too short; stale rows in the JPA table never purged.
Related errors
- Cannot save a resource set with inconsistent scopes.
- Passwordless authentication has failed
- Unable to validate JWT signature
- Lookup of datasource
- invalid_request
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/cef46ca8e9a40574.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-passwordless-jpa/src/main/java/org/apereo/cas/impl/token/JpaPasswordlessTokenRepository.java:51
private EntityManager entityManager;
public JpaPasswordlessTokenRepository(final long tokenExpirationInSeconds,
final CipherExecutor cipherExecutor) {
super(tokenExpirationInSeconds, cipherExecutor);
}
@Override
public Optional<PasswordlessAuthenticationToken> findToken(final String username) {
val query = SELECT_QUERY.concat(" WHERE t.username = :username");
val results = entityManager.createQuery(query, JpaPasswordlessAuthenticationEntity.class)
.setParameter(QUERY_PARAM_USERNAME, username)
.setMaxResults(1)
.getResultList();
if (!results.isEmpty()) {
val token = results.getFirst();
val authnToken = decodePasswordlessAuthenticationToken(token.getToken());
if (authnToken.isExpired()) {
LOGGER.warn("Token [{}] has expired", token);
return Optional.empty();
}
LOGGER.debug("Located token [{}]", authnToken);
return Optional.of(authnToken);
}
return Optional.empty();
}
@Override
public void deleteTokens(final String username) {
entityManager.createQuery(DELETE_QUERY.concat("WHERE t.username = :username"))
.setParameter(QUERY_PARAM_USERNAME, username)
.executeUpdate();
}
@Override
public void deleteToken(final PasswordlessAuthenticationToken token) {
val query = DELETE_QUERY.concat(" WHERE t.username = :username AND t.id = :id");View on GitHub (pinned to e7288fc434)