apereo/cas · warning

Ticket is too late because it's retrieved on

Error message

Ticket is too late because it's retrieved on [{}] which is after [{}].

What it means

isValid() enforces the token's NotOnOrAfter condition: the retrieval time must not be after this expiry instant. If getRetrievedOn().isAfter(notOnOrAfter), the WS-Federation assertion's validity window has already closed and the credential is rejected with this warning. This is the standard assertion-expiry check mirroring SAML conditions.

Solutions

  1. Synchronize clocks with NTP — an ahead-of-time CAS server makes valid tokens appear expired.
  2. Have the user restart the login flow to obtain a fresh token instead of replaying the old one.
  3. If tokens consistently expire in transit, reduce latency or slightly extend the IdP's token lifetime (ADFS TokenLifetime).
  4. Confirm the configured time drift accounts for any deliberate skew you rely on.

Example fix

// before: replaying a cached assertion from minutes ago
String token = session.getAttribute("wsfedToken");
// after: always fetch a fresh token on each authentication
String token = request.getParameter("wresult");
Defensive patterns

Strategy: validation

Validate before calling

if (Instant.now().isAfter(credential.getNotOnOrAfter())) {
    throw new IllegalStateException("Assertion expired at " + credential.getNotOnOrAfter() + "; restart the login flow.");
}

Prevention

When it happens

Trigger: Calling isValid(expectedAudience, expectedIssuer, timeDrift) on a token whose NotOnOrAfter timestamp (from the IdP assertion conditions) is earlier than the moment CAS retrieved/processed it — the assertion expired before validation.

Common situations: Severe clock skew (CAS server time far ahead of IdP time); replaying an old assertion after its short lifetime (e.g. 5-minute ADFS token lifetime elapsed); stalled requests or retries arriving after expiry.

Related errors


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

Appendix: source

Thrown at support/cas-server-support-wsfederation/src/main/java/org/apereo/cas/support/wsfederation/authentication/principal/WsFederationCredential.java:80

        }
        if (!this.issuer.equalsIgnoreCase(expectedIssuer)) {
            LOGGER.warn("Issuer [{}] is invalid since the expected issuer should be [{}]", this.issuer, expectedIssuer);
            return false;
        }
        val retrievedOnTimeDrift = getRetrievedOn().minus(timeDrift, ChronoUnit.MILLIS);
        if (getIssuedOn().isBefore(retrievedOnTimeDrift)) {
            LOGGER.warn("Ticket is issued before the allowed drift. Issued on [{}] while allowed drift is [{}]",
                this.issuedOn, retrievedOnTimeDrift);
            return false;
        }
        val retrievedOnTimeAfterDrift = getRetrievedOn().plus(timeDrift, ChronoUnit.MILLIS);
        if (getIssuedOn().isAfter(retrievedOnTimeAfterDrift)) {
            LOGGER.warn("Ticket is issued after the allowed drift. Retrieved on [{}] and issued on [{}] while allowed drift is [{}]",
                getRetrievedOn(), getIssuedOn(), retrievedOnTimeAfterDrift);
            return false;
        }
        if (getRetrievedOn().isAfter(this.notOnOrAfter)) {
            LOGGER.warn("Ticket is too late because it's retrieved on [{}] which is after [{}].",
                getRetrievedOn(), this.notOnOrAfter);
            return false;
        }
        LOGGER.debug("WsFed Credential is validated for [{}] and [{}].", expectedAudience, expectedIssuer);
        return true;
    }
}

View on GitHub (pinned to e7288fc434)