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
- Generate a new token for the current user and use that code
- Ensure principal resolution is consistent (same resolver/attributes) at token creation and validation time
- Verify the user's principal id has not changed since the code was issued; if it did, invalidate outstanding tokens
- 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
- Keep principal resolution configuration identical across issuance and validation
- Invalidate outstanding tokens when a user's principal id changes
- Isolate ticket registries per environment to prevent cross-tenant token reuse
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Credential principal
- Failed to authenticate code
- Unable to extract credentials for multifactor authentication
- Duo Security authentication has failed
- Cannot validate authentication for: [login]
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)