{"record":{"id":"c7ba9c917e494b48","repo":"apereo/cas","slug":"service-ticket-issued-for-service-has-al","errorCode":null,"errorMessage":"Service ticket [{}] issued for service [{}] has already allotted a proxy-granting ticket","messagePattern":"Service ticket \\[(.+?)\\] issued for service \\[(.+?)\\] has already allotted a proxy-granting ticket","errorType":"console","errorClass":"InvalidProxyGrantingTicketForServiceTicketException","httpStatus":null,"severity":"error","filePath":"core/cas-server-core-tickets-api/src/main/java/org/apereo/cas/ticket/ServiceTicketImpl.java","lineNumber":81,"sourceCode":"        @Nullable @JsonProperty(\"ticketGrantingTicket\") final TicketGrantingTicket ticket,\n        @JsonProperty(\"service\") final @NonNull Service service,\n        @JsonProperty(\"credentialProvided\")\n        @JsonSetter(nulls = Nulls.SKIP)\n        final boolean credentialProvided,\n        @JsonProperty(\"expirationPolicy\") final ExpirationPolicy policy) {\n        super(id, policy);\n        this.ticketGrantingTicket = ticket;\n        this.service = Objects.requireNonNull(service);\n        this.fromNewLogin = credentialProvided || (ticket != null && ticket.getCountOfUses() == 0);\n    }\n\n    @Override\n    public ProxyGrantingTicket grantProxyGrantingTicket(\n        final @NonNull String id, final @NonNull Authentication authentication,\n        final ExpirationPolicy expirationPolicy,\n        final TicketTrackingPolicy proxyGrantingTicketTrackingPolicy) throws AbstractTicketException {\n        if (this.grantedTicketAlready) {\n            LOGGER.warn(\"Service ticket [{}] issued for service [{}] has already allotted a proxy-granting ticket\", getId(), service.getId());\n            throw new InvalidProxyGrantingTicketForServiceTicketException(service);\n        }\n        this.grantedTicketAlready = Boolean.TRUE;\n        val proxyGrantingTicket = new ProxyGrantingTicketImpl(id, service, ticketGrantingTicket, authentication, expirationPolicy);\n        proxyGrantingTicket.setTenantId(service.getTenant());\n        proxyGrantingTicketTrackingPolicy.trackTicket(ticketGrantingTicket, proxyGrantingTicket, service);\n        return proxyGrantingTicket;\n    }\n\n    @Override\n    @JsonIgnore\n    public Authentication getAuthentication() {\n        return Objects.requireNonNullElseGet(authentication, () -> ticketGrantingTicket != null ? ticketGrantingTicket.getAuthentication() : null);\n    }\n\n    @Override\n    public String getPrefix() {\n        return ServiceTicket.PREFIX;","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/core/cas-server-core-tickets-api/src/main/java/org/apereo/cas/ticket/ServiceTicketImpl.java#L63-L99","documentation":"A service ticket in CAS is a one-time-use credential: it may exchange for at most one proxy-granting ticket (PGT). ServiceTicketImpl keeps a `grantedTicketAlready` flag, and grantProxyGrantingTicket throws InvalidProxyGrantingTicketForServiceTicketException when a second PGT is requested from the same ST. This enforces the CAS protocol rule that an ST can be validated only once.","triggerScenarios":"Calling ServiceTicket.grantProxyGrantingTicket(id, authentication, expirationPolicy, trackingPolicy) on a ServiceTicketImpl whose grantedTicketAlready flag is already TRUE — i.e. a second call after a successful PGT grant, or after deserialization of a ticket that previously granted one.","commonSituations":"Client applications retrying the /serviceValidate (or /p3/proxyValidate) endpoint after a network hiccup; backends double-submitting the same ticket (e.g. browser refresh or duplicate AJAX calls); load-balanced CAS clients replaying a ticket to multiple nodes before ticket state is invalidated.","solutions":["Treat the InvalidProxyGrantingTicketForServiceTicketException as a protocol-level one-time-use violation: obtain a fresh service ticket via a new login instead of reusing the ST.","Remove duplicate/retry logic in the client that calls validate with the same ticket id; make the validate call idempotent by storing the ticket id already processed.","If load balancing causes the duplicate, ensure ticket registry state (invalidation after use) is shared across all CAS-facing nodes.","If you truly need multiple grants, issue multiple service tickets rather than re-granting a PGT from one."],"exampleFix":"// before (client retries on failure)\nString pgtIou = validate(st); // retried after timeout, ST already consumed\n// after\nString pgtIou = pgtIouCache.computeIfAbsent(st, this::validateOnce); // only one validate per ST; on failure redirect user to CAS for a new ST","handlingStrategy":"try-catch","validationCode":"if (processedTickets.contains(stId)) { throw new IllegalStateException(\"ST already validated: \" + stId); }","typeGuard":null,"tryCatchPattern":"try { return st.grantProxyGrantingTicket(pgtId, auth, policy, tracking); }\ncatch (final InvalidProxyGrantingTicketForServiceTicketException e) {\n  // one-time-use violation: redirect user to CAS for a fresh service ticket\n  throw new ServiceTicketAlreadyUsedException(e.getService().getId(), e);\n}","preventionTips":["Never reuse a service ticket; treat ST ids as single-use tokens and track consumed ids client-side","Disable HTTP-client retries on the CAS validation endpoint or key retries by ticket id","Share ticket-invalidation state across cluster nodes so a replay cannot hit a second node"],"tags":["cas","service-ticket","one-time-use","protocol-violation"],"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"}