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
- 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.
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
- 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
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
- Impersonating is not allowed
- Could not grant service ticket
- Unknown tenant for service ticket
- No authentication found for ticket
- Principal attribute [
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)