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

  1. Refresh the QR code on the client and retry immediately.
  2. Increase the QR token expiration (qr authentication token expiration policy) in cas.authn.qr properties.
  3. Synchronize server clocks via NTP, especially across clustered nodes.
  4. 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

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


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)