apereo/cas · error · InvalidTicketException
Service ticket [ ] is not assigned a valid ticket granting…
Error message
Service ticket [{}] is not assigned a valid ticket granting ticket What it means
A service ticket must be bound to a TicketGrantingTicket (its parent) unless it was issued statelessly. validateServiceTicket rejects ServiceTickets whose TGT reference is missing/no longer a TicketGrantingTicket, throwing InvalidTicketException, because ticket-granting provenance cannot be established.
Solutions
- Ensure cas.ticket.tgt.max-time-to-live-in-seconds exceeds cas.ticket.st.time-to-kill-in-seconds
- Validate STs promptly after issuance so the parent TGT is still alive
- Check the shared ticket registry retains TGT/ST relations consistently across cluster nodes
- If stateless flows are intended, confirm the ticket is created with isStateless support rather than relying on a TGT
Example fix
// before cas.ticket.tgt.max-time-to-live-in-seconds=60 cas.ticket.st.time-to-kill-in-seconds=600 // after cas.ticket.tgt.max-time-to-live-in-seconds=28800 cas.ticket.st.time-to-kill-in-seconds=10
Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure TGT TTL comfortably exceeds ST TTL in your config assert tgtMaxTimeToLiveSeconds > stTimeToKillSeconds : "TGT expires before ST";
Try / catch
try {
Assertion a = cas.validateServiceTicket(ticketId, service);
} catch (InvalidTicketException e) {
logger.warn("ST has no valid parent TGT (expired/purged): re-authenticating");
// restart SSO flow to mint a fresh TGT+ST
} Prevention
- Keep TGT max-time-to-live well above ST time-to-kill
- Avoid manual purge of TGTs while STs may still be validated
- Use one ticket registry store with consistent TTLs across the cluster
- Do not build custom STs without a proper parent TGT unless stateless
When it happens
Trigger: Validating a service ticket whose parent TGT expired or was purged before the ST was validated (ST TTL > TGT TTL), or a stateless ST incorrectly has no TGT and isStateless() is false — typically after registry inconsistency or manual ticket manipulation.
Common situations: TGT time-to-live shortened while ST TTL stayed long; ticket-registry backends with differing TTLs across nodes; admin purged TGTs (logout/SSO cleanup) leaving orphaned STs; custom ticket factory producing STs without a parent.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Service ticket [ ] does not exist.
- Unknown tenant for service ticket
- No authentication found for ticket
- Impersonating is not allowed
- Final ticket id [ ] length [ ] exceeds [ ] characters
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/bbdc16b6b3a4f6ae.
Report an issue: GitHub.
Appendix: source
Thrown at core/cas-server-core/src/main/java/org/apereo/cas/DefaultCentralAuthenticationService.java:166
}
@Audit(
action = AuditableActions.SERVICE_TICKET_VALIDATE,
actionResolverName = AuditActionResolvers.VALIDATE_SERVICE_TICKET_RESOLVER,
resourceResolverName = AuditResourceResolvers.VALIDATE_SERVICE_TICKET_RESOURCE_RESOLVER)
@Override
public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws Throwable {
if (!isTicketAuthenticityVerified(serviceTicketId)) {
LOGGER.info("Service ticket [{}] is not a valid ticket issued by CAS.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
val serviceTicket = configurationContext.getTicketRegistry().getTicket(serviceTicketId, ServiceTicket.class);
if (serviceTicket == null) {
LOGGER.warn("Service ticket [{}] does not exist.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
if (!(serviceTicket.getTicketGrantingTicket() instanceof TicketGrantingTicket) && !serviceTicket.isStateless()) {
LOGGER.warn("Service ticket [{}] is not assigned a valid ticket granting ticket", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
try {
val selectedService = resolveServiceFromAuthenticationRequest(serviceTicket.getService());
val resolvedService = resolveServiceFromAuthenticationRequest(service);
LOGGER.debug("Resolved service [{}] from the authentication request with service [{}] linked to service ticket [{}]",
resolvedService, selectedService, serviceTicket.getId());
configurationContext.getLockRepository().execute(serviceTicket.getId(),
Unchecked.supplier(() -> {
if (serviceTicket.isExpired()) {
LOGGER.info("Service ticket [{}] has expired.", serviceTicketId);
throw new InvalidTicketException(serviceTicketId);
}
if (!configurationContext.getServiceMatchingStrategy().matches(selectedService, resolvedService)) {
LOGGER.error("Service ticket [{}] with service [{}] does not match supplied service [{}]",
serviceTicketId, serviceTicket.getService().getId(), Objects.requireNonNull(resolvedService).getId());View on GitHub (pinned to e7288fc434)