apereo/cas · error · InvalidProxyGrantingTicketForServiceTicketException

Service ticket [ ] issued for service [ ] has already…

Error message

Service ticket [{}] issued for service [{}] has already allotted a proxy-granting ticket

What it means

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.

Solutions

  1. 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.
  2. 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.
  3. If load balancing causes the duplicate, ensure ticket registry state (invalidation after use) is shared across all CAS-facing nodes.
  4. If you truly need multiple grants, issue multiple service tickets rather than re-granting a PGT from one.

Example fix

// before (client retries on failure)
String pgtIou = validate(st); // retried after timeout, ST already consumed
// after
String pgtIou = pgtIouCache.computeIfAbsent(st, this::validateOnce); // only one validate per ST; on failure redirect user to CAS for a new ST
Defensive patterns

Strategy: try-catch

Validate before calling

if (processedTickets.contains(stId)) { throw new IllegalStateException("ST already validated: " + stId); }

Try / catch

try { return st.grantProxyGrantingTicket(pgtId, auth, policy, tracking); }
catch (final InvalidProxyGrantingTicketForServiceTicketException e) {
  // one-time-use violation: redirect user to CAS for a fresh service ticket
  throw new ServiceTicketAlreadyUsedException(e.getService().getId(), e);
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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/c7ba9c917e494b48. Report an issue: GitHub.

Appendix: source

Thrown at core/cas-server-core-tickets-api/src/main/java/org/apereo/cas/ticket/ServiceTicketImpl.java:81

        @Nullable @JsonProperty("ticketGrantingTicket") final TicketGrantingTicket ticket,
        @JsonProperty("service") final @NonNull Service service,
        @JsonProperty("credentialProvided")
        @JsonSetter(nulls = Nulls.SKIP)
        final boolean credentialProvided,
        @JsonProperty("expirationPolicy") final ExpirationPolicy policy) {
        super(id, policy);
        this.ticketGrantingTicket = ticket;
        this.service = Objects.requireNonNull(service);
        this.fromNewLogin = credentialProvided || (ticket != null && ticket.getCountOfUses() == 0);
    }

    @Override
    public ProxyGrantingTicket grantProxyGrantingTicket(
        final @NonNull String id, final @NonNull Authentication authentication,
        final ExpirationPolicy expirationPolicy,
        final TicketTrackingPolicy proxyGrantingTicketTrackingPolicy) throws AbstractTicketException {
        if (this.grantedTicketAlready) {
            LOGGER.warn("Service ticket [{}] issued for service [{}] has already allotted a proxy-granting ticket", getId(), service.getId());
            throw new InvalidProxyGrantingTicketForServiceTicketException(service);
        }
        this.grantedTicketAlready = Boolean.TRUE;
        val proxyGrantingTicket = new ProxyGrantingTicketImpl(id, service, ticketGrantingTicket, authentication, expirationPolicy);
        proxyGrantingTicket.setTenantId(service.getTenant());
        proxyGrantingTicketTrackingPolicy.trackTicket(ticketGrantingTicket, proxyGrantingTicket, service);
        return proxyGrantingTicket;
    }

    @Override
    @JsonIgnore
    public Authentication getAuthentication() {
        return Objects.requireNonNullElseGet(authentication, () -> ticketGrantingTicket != null ? ticketGrantingTicket.getAuthentication() : null);
    }

    @Override
    public String getPrefix() {
        return ServiceTicket.PREFIX;

View on GitHub (pinned to e7288fc434)