apereo/cas · error · AuthenticationException

State [ : : ] does not have a matching transition for

Error message

State [<stateId>:<eventId>:<transitionId>] does not have a matching transition for <eventId>

What it means

MultifactorAuthenticationUtils.validateEventIdForMatchingTransitionInContext verifies that the event about to be signaled in the login webflow has a matching transition defined on the current flow state. When the current state has no transition for that event id, it logs the state/event/transition triple and throws AuthenticationException, aborting the flow rather than leaving the state machine stuck.

Solutions

  1. Verify the MFA provider id in cas.authn.mfa.* matches the event/transition ids registered in the login webflow.
  2. Have the user restart the login flow (fresh /login request) instead of retrying a stale page/back-button state.
  3. If a custom provider, confirm its transition is registered via the webflow customization/execution listener so the state defines that event.
  4. Clear the bot-stale session/flow state (expire Webflow session storage) and check for load-balanced nodes with mismatched CAS versions.

Example fix

// before: posting event 'mfa-sms' but only google-authenticator is configured
// after: align configured provider with the event
// application.properties
cas.authn.mfa.gauth.core.rank=0
cas.authn.mfa.sms.enabled=true  // ensure provider and its flow transition both registered
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return MultifactorAuthenticationUtils.validateEventIdForMatchingTransitionInContext(event, ctx);
} catch (AuthenticationException e) {
  LOGGER.warn("No matching transition; restarting flow", e);
  return null;
}

Prevention

When it happens

Trigger: Calling the utility (or the MFA webflow 'event' action that uses it) with an eventId that the current webflow state cannot transition on — e.g. posting an MFA provider event like 'mfa-gauth' when the state only defines transitions for registered/supported providers, or a stale browser retry after the flow already advanced.

Common situations: MFA provider id misconfigured (event never registered as a transition); user double-submits or uses browser back so the event no longer matches the current state; custom MFA provider registered without wiring its transition into the login flow; CAS upgrade changing flow state ids while a bookmarked URL replays an old event.

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

Appendix: source

Thrown at core/cas-server-core-authentication-mfa-api/src/main/java/org/apereo/cas/authentication/MultifactorAuthenticationUtils.java:77

     */
    public static Event validateEventIdForMatchingTransitionInContext(final String eventId,
                                                                      final Optional<RequestContext> context,
                                                                      final Map<String, Object> attributes) {
        val attributesMap = new LocalAttributeMap<>(attributes);
        val event = new Event(eventId, eventId, attributesMap);
        LOGGER.trace("Attempting to find a matching transition for event id [{}]", event.getId());
        return context.map(ctx -> {
            LOGGER.trace("Reviewing current state [{}], event [{}] and transition [{}]",
                ctx.getCurrentState(), ctx.getCurrentEvent(), ctx.getCurrentTransition());
            val def = ctx.getMatchingTransition(event.getId());
            if (def == null) {
                val msg = String.format("State [%s:%s:%s] does not have a matching transition for %s",
                    ctx.getCurrentState().getId(),
                    ctx.getCurrentEvent() != null ? ctx.getCurrentEvent().getId() : "N/A",
                    ctx.getCurrentTransition() != null ? ctx.getCurrentTransition().getId() : "N/A",
                    event.getId());
                LoggingUtils.error(LOGGER, msg);
                throw new AuthenticationException(msg);
            }
            return event;
        }).orElse(event);
    }


    /**
     * Resolve event via multivalued attribute set.
     *
     * @param principal         the principal
     * @param attributeValue    the attribute value
     * @param registeredService the service
     * @param service           the service
     * @param context           the context
     * @param provider          the provider
     * @param predicate         the predicate
     * @return the set
     */

View on GitHub (pinned to e7288fc434)