apereo/cas · error · AuthenticationException
Token has expired
Error message
Token %s has expired
What it means
AuthenticationException thrown by DefaultQRAuthenticationTokenValidatorService.validate when the JWT claim's expiration time is in the past relative to the current UTC clock. The token id references a TicketGrantingTicket in the ticket registry, but the token itself has a fixed expiry that must not have elapsed.
Solutions
- Refresh the QR code on the client and retry immediately.
- Increase the QR token expiration (qr authentication token expiration policy) in cas.authn.qr properties.
- Synchronize server clocks via NTP, especially across clustered nodes.
- Check client-side auto-refresh of the QR image so expired tokens are not displayed.
Example fix
// before cas.authn.qr.token.expiration-time-in-seconds=30 // after cas.authn.qr.token.expiration-time-in-seconds=120
Defensive patterns
Strategy: validation
Validate before calling
// Decode and check expiry before validating
long exp = claims.getExpirationTime().getTime();
if (Instant.now().isAfter(Instant.ofEpochSecond(exp))) { refreshQrCode(); } Try / catch
try { validatorService.validate(request); } catch (AuthenticationException e) { if (e.getMessage().contains("has expired")) { return newQrCodeEvent(); } throw e; } Prevention
- Set a QR token TTL that comfortably covers user scan time.
- Auto-refresh the QR code on the client before expiry.
- NTP-sync all servers performing validation.
When it happens
Trigger: now.isAfter(localDateTimeOf(claims.getExpirationTime())) when validating a QR token: the token was minted with a short TTL and submitted after expiry.
Common situations: User scanned an old QR code; long delay between token generation and validation; server clock skewed forward; QR token TTL property set too short for the login UX.
Related errors
- Token has expired: and is after
- Token does not belong to the assigned principal
- Token has an invalid issuer that does not match
- Request is assigned an invalid device identifier
- Unable to accept the ID token with an invalid [sub] claim
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/326745e1300faa18.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-qr-authentication/src/main/java/org/apereo/cas/qr/validation/DefaultQRAuthenticationTokenValidatorService.java:46
private final TicketRegistry ticketRegistry;
private final CasConfigurationProperties casProperties;
private final QRAuthenticationDeviceRepository deviceRepository;
@Override
public QRAuthenticationTokenValidationResult validate(final QRAuthenticationTokenValidationRequest request) {
val claims = jwtBuilder.unpack(request.getRegisteredService(), request.getToken());
LOGGER.trace("Unpacked QR token as [{}]", claims);
val tgt = ticketRegistry.getTicket(claims.getJWTID(), TicketGrantingTicket.class);
val dt = DateTimeUtils.localDateTimeOf(claims.getExpirationTime());
val now = LocalDateTime.now(Clock.systemUTC());
if (now.isAfter(dt)) {
LOGGER.trace("Comparing now at [{}] with token's expiration time [{}]", now, dt);
throw new AuthenticationException(String.format("Token %s has expired", tgt.getId()));
}
val authentication = tgt.getAuthentication();
LOGGER.trace("Authentication attempt linked to [{}] is [{}]", tgt.getId(), authentication);
if (!authentication.getPrincipal().getId().equals(claims.getSubject())) {
val message = String.format("Token %s does not belong to the assigned principal", claims.getSubject());
throw new AuthenticationException(message);
}
if (!claims.getIssuer().equals(casProperties.getServer().getPrefix())) {
val message = String.format("Token %s has an invalid issuer %s that does not match %s", tgt.getId(),
claims.getIssuer(), casProperties.getServer().getPrefix());
throw new AuthenticationException(message);
}
val tokenDeviceId = FunctionUtils.doUnchecked(() -> claims.getStringClaim(QRAuthenticationConstants.QR_AUTHENTICATION_DEVICE_ID));
if (!Strings.CI.equals(tokenDeviceId, request.getDeviceId())) {View on GitHub (pinned to e7288fc434)