apereo/cas · error · FailedLoginException

Principal assigned to token

Error message

Principal assigned to token [{}] is unauthorized for token [{}]

What it means

BaseCasSimpleMultifactorAuthenticationService.validateTokenForPrincipal() also verifies that the principal embedded in the MFA token matches the resolved principal of the requester. On mismatch it logs this warning, deletes the token, and throws FailedLoginException. This blocks a user from authenticating with a token issued to someone else.

Solutions

  1. Generate a new token for the current user and use that code
  2. Ensure principal resolution is consistent (same resolver/attributes) at token creation and validation time
  3. Verify the user's principal id has not changed since the code was issued; if it did, invalidate outstanding tokens
  4. Check for ticket-registry keys shared across environments that could serve another tenant's tokens
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check ownership before submitting
Principal stored = (Principal) ticket.getProperties()
    .get(CasSimpleMultifactorAuthenticationConstants.PROPERTY_PRINCIPAL);
if (!stored.getId().equals(currentUserPrincipal.getId()))
    throw new IllegalStateException("Token belongs to a different user");

Try / catch

try {
    service.validateTokenForPrincipal(resolvedPrincipal, ticket);
} catch (FailedLoginException e) {
    // treat as invalid code: delete token, issue a fresh one for this user
}

Prevention

When it happens

Trigger: Presenting a simple-MFA code whose stored principal's id differs from the resolved principal id of the current authentication attempt.

Common situations: Code reuse across accounts (shared inbox, forwarded email/SMS); principal resolution configured differently between token issuance and validation (custom PrincipalResolvers, attribute changes); stale token replayed after the user's principal id changed (e.g. username change).

Understand the failure class

Related errors


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

Appendix: source

Thrown at support/cas-server-support-simple-mfa-core/src/main/java/org/apereo/cas/mfa/simple/validation/BaseCasSimpleMultifactorAuthenticationService.java:41

public abstract class BaseCasSimpleMultifactorAuthenticationService implements CasSimpleMultifactorAuthenticationService {
    protected final TicketRegistry ticketRegistry;

    @Override
    public CasSimpleMultifactorAuthenticationTicket getMultifactorAuthenticationTicket(final CasSimpleMultifactorTokenCredential credential) {
        val tokenId = normalize(credential.getId());
        return ticketRegistry.getTicket(tokenId, CasSimpleMultifactorAuthenticationTicket.class);
    }

    protected Principal validateTokenForPrincipal(final Principal resolvedPrincipal, final CasSimpleMultifactorAuthenticationTicket acct)
        throws FailedLoginException {
        if (!acct.getProperties().containsKey(CasSimpleMultifactorAuthenticationConstants.PROPERTY_PRINCIPAL)) {
            LOGGER.warn("Unable to locate principal for token [{}]", acct.getId());
            deleteToken(acct);
            throw new FailedLoginException("Failed to authenticate code " + acct.getId());
        }
        val principal = (Principal) acct.getProperties().get(CasSimpleMultifactorAuthenticationConstants.PROPERTY_PRINCIPAL);
        if (!principal.equals(resolvedPrincipal)) {
            LOGGER.warn("Principal assigned to token [{}] is unauthorized for token [{}]", principal.getId(), acct.getId());
            deleteToken(acct);
            throw new FailedLoginException("Failed to authenticate code " + acct.getId());
        }
        return principal;
    }

    protected static String normalize(final String tokenId) {
        if (!tokenId.startsWith(CasSimpleMultifactorAuthenticationTicket.PREFIX)) {
            return CasSimpleMultifactorAuthenticationTicket.PREFIX + UniqueTicketIdGenerator.SEPARATOR + tokenId;
        }
        return tokenId;
    }

    protected void deleteToken(final CasSimpleMultifactorAuthenticationTicket acct) {
        FunctionUtils.doUnchecked(_ -> ticketRegistry.deleteTicket(acct.getId()));
    }

}

View on GitHub (pinned to e7288fc434)