{"record":{"id":"bbdc16b6b3a4f6ae","repo":"apereo/cas","slug":"service-ticket-is-not-assigned-a-valid-ticket","errorCode":null,"errorMessage":"Service ticket [{}] is not assigned a valid ticket granting ticket","messagePattern":"Service ticket \\[(.+?)\\] is not assigned a valid ticket granting ticket","errorType":"exception","errorClass":"InvalidTicketException","httpStatus":null,"severity":"error","filePath":"core/cas-server-core/src/main/java/org/apereo/cas/DefaultCentralAuthenticationService.java","lineNumber":166,"sourceCode":"    }\n\n    @Audit(\n        action = AuditableActions.SERVICE_TICKET_VALIDATE,\n        actionResolverName = AuditActionResolvers.VALIDATE_SERVICE_TICKET_RESOLVER,\n        resourceResolverName = AuditResourceResolvers.VALIDATE_SERVICE_TICKET_RESOURCE_RESOLVER)\n    @Override\n    public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws Throwable {\n        if (!isTicketAuthenticityVerified(serviceTicketId)) {\n            LOGGER.info(\"Service ticket [{}] is not a valid ticket issued by CAS.\", serviceTicketId);\n            throw new InvalidTicketException(serviceTicketId);\n        }\n        val serviceTicket = configurationContext.getTicketRegistry().getTicket(serviceTicketId, ServiceTicket.class);\n        if (serviceTicket == null) {\n            LOGGER.warn(\"Service ticket [{}] does not exist.\", serviceTicketId);\n            throw new InvalidTicketException(serviceTicketId);\n        }\n        if (!(serviceTicket.getTicketGrantingTicket() instanceof TicketGrantingTicket) && !serviceTicket.isStateless()) {\n            LOGGER.warn(\"Service ticket [{}] is not assigned a valid ticket granting ticket\", serviceTicketId);\n            throw new InvalidTicketException(serviceTicketId);\n        }\n\n        try {\n            val selectedService = resolveServiceFromAuthenticationRequest(serviceTicket.getService());\n            val resolvedService = resolveServiceFromAuthenticationRequest(service);\n            LOGGER.debug(\"Resolved service [{}] from the authentication request with service [{}] linked to service ticket [{}]\",\n                resolvedService, selectedService, serviceTicket.getId());\n\n            configurationContext.getLockRepository().execute(serviceTicket.getId(),\n                Unchecked.supplier(() -> {\n                    if (serviceTicket.isExpired()) {\n                        LOGGER.info(\"Service ticket [{}] has expired.\", serviceTicketId);\n                        throw new InvalidTicketException(serviceTicketId);\n                    }\n                    if (!configurationContext.getServiceMatchingStrategy().matches(selectedService, resolvedService)) {\n                        LOGGER.error(\"Service ticket [{}] with service [{}] does not match supplied service [{}]\",\n                            serviceTicketId, serviceTicket.getService().getId(), Objects.requireNonNull(resolvedService).getId());","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/core/cas-server-core/src/main/java/org/apereo/cas/DefaultCentralAuthenticationService.java#L148-L184","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ncas.ticket.tgt.max-time-to-live-in-seconds=60\ncas.ticket.st.time-to-kill-in-seconds=600\n// after\ncas.ticket.tgt.max-time-to-live-in-seconds=28800\ncas.ticket.st.time-to-kill-in-seconds=10","handlingStrategy":"try-catch","validationCode":"// Ensure TGT TTL comfortably exceeds ST TTL in your config\nassert tgtMaxTimeToLiveSeconds > stTimeToKillSeconds : \"TGT expires before ST\";","typeGuard":null,"tryCatchPattern":"try {\n    Assertion a = cas.validateServiceTicket(ticketId, service);\n} catch (InvalidTicketException e) {\n    logger.warn(\"ST has no valid parent TGT (expired/purged): re-authenticating\");\n    // restart SSO flow to mint a fresh TGT+ST\n}","preventionTips":["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"],"tags":["tickets","service-ticket","tgt"],"backgroundTag":"invalid-state-transition","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}