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
- Synchronize clocks with NTP — an ahead-of-time CAS server makes valid tokens appear expired.
- Have the user restart the login flow to obtain a fresh token instead of replaying the old one.
- If tokens consistently expire in transit, reduce latency or slightly extend the IdP's token lifetime (ADFS TokenLifetime).
- 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
- Always initiate a fresh redirect to the IdP when a previous assertion is expired.
- Keep CAS server clocks accurate; a fast clock shortens perceived token lifetime.
- If latency is chronic, increase the IdP TokenLifetime rather than disabling the check.
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
- Ticket is issued before the allowed drift. Issued on
- Ticket is issued after the allowed drift. Retrieved on
- Token has expired: and is after
- Issuer [ ] is invalid since the expected issuer should be […
- Found multiple values for id attribute
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)