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

  1. Ensure cas.ticket.tgt.max-time-to-live-in-seconds exceeds cas.ticket.st.time-to-kill-in-seconds
  2. Validate STs promptly after issuance so the parent TGT is still alive
  3. Check the shared ticket registry retains TGT/ST relations consistently across cluster nodes
  4. 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

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


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)