apereo/cas · warning

Found removable encoded ticket

Error message

Found removable encoded ticket [{}] yet cipher operations are disabled.

What it means

AbstractTicketRegistry.decodeTicket detects an EncodedTicket in the registry while the registry's ticket cipher executor is disabled (isCipherExecutorEnabled() == false). Because the ticket cannot be decoded, it is treated as garbage: it is deleted and null is returned (caller sees 'ticket not found'). The warning points at an encryption configuration mismatch over the ticket registry's lifetime.

Solutions

  1. Flush/clear the ticket registry when toggling cas.ticket.crypto.enabled so no stale EncodedTicket entries remain (users simply log in again).
  2. Keep ticket crypto configuration consistent across ALL nodes in the cluster during rolling upgrades; enable/disable everywhere at once behind a maintenance window.
  3. Re-enable the cipher executor if disabling was unintentional, restoring the previous crypto (signing/encryption) keys so existing encoded tickets decode.
  4. If the warnings are transient after a migration, they are self-healing: each warning deletes one stale ticket; monitor until it stops.

Example fix

// before (rolling restart with mixed config)
node1: cas.ticket.crypto.enabled=false
node2: cas.ticket.crypto.enabled=true
// after: uniform config + registry flush before restart
all nodes: cas.ticket.crypto.enabled=false
# then: FLUSHDB the redis ticket registry (or equivalent) before serving traffic
Defensive patterns

Strategy: fallback

Validate before calling

if (!registry.isCipherExecutorEnabled()) { /* ensure registry was flushed of EncodedTicket entries before switching crypto off */ }

Type guard

boolean needsDecodeCheck = ticket instanceof EncodedTicket && !registry.isCipherExecutorEnabled();

Try / catch

// decodeTicket returns null for stale encoded tickets; caller must handle null
Ticket t = ticketRegistry.getTicket(id);
if (t == null) { /* force re-login */ }

Prevention

When it happens

Trigger: cas.ticket.crypto.enabled was switched from true to false (or the crypto signing/encryption keys/config changed) while encoded tickets from the previous configuration still exist in the shared registry; decodeTicket then deletes each such ticket on read.

Common situations: Rolling upgrades where some nodes encrypt tickets and others have crypto disabled; reverting a ticket-encryption rollout without flushing the ticket store; shared Redis/Memcached/JDBC registry containing pre-change encoded tickets.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/8d04c4fe45fcc165. Report an issue: GitHub.

Appendix: source

Thrown at core/cas-server-core-tickets-api/src/main/java/org/apereo/cas/ticket/registry/AbstractTicketRegistry.java:336

    }

    protected @Nullable Ticket encodeTicket(final Ticket ticket) throws Exception {
        if (!isCipherExecutorEnabled()) {
            LOGGER.trace(TICKET_ENCRYPTION_LOG_MESSAGE);
            return ticket;
        }
        if (ticket == null) {
            LOGGER.debug("Ticket passed is null and cannot be encoded");
            return null;
        }
        val encodedTicket = createEncodedTicket(ticket);
        LOGGER.debug("Created encoded ticket [{}]", encodedTicket);
        return encodedTicket;
    }

    protected @Nullable Ticket decodeTicket(final Ticket ticketToProcess) {
        if (ticketToProcess instanceof EncodedTicket && !isCipherExecutorEnabled()) {
            LOGGER.warn("Found removable encoded ticket [{}] yet cipher operations are disabled.", ticketToProcess.getId());
            FunctionUtils.doUnchecked(_ -> deleteTicket(ticketToProcess));
            return null;
        }

        if (!isCipherExecutorEnabled()) {
            LOGGER.trace(TICKET_ENCRYPTION_LOG_MESSAGE);
            return ticketToProcess;
        }
        if (ticketToProcess == null) {
            LOGGER.warn("Ticket passed is null and cannot be decoded");
            return null;
        }
        if (!(ticketToProcess instanceof final EncodedTicket encodedTicket)) {
            LOGGER.debug("Ticket passed is not an encoded ticket: [{}], no decoding is necessary.",
                ticketToProcess.getClass().getSimpleName());
            return ticketToProcess;
        }
        LOGGER.debug("Attempting to decode [{}]", ticketToProcess);

View on GitHub (pinned to e7288fc434)