{"record":{"id":"1c1373d69619a9fe","repo":"apereo/cas","slug":"principal-assigned-to-token-is-unauthorized-f","errorCode":null,"errorMessage":"Principal assigned to token [{}] is unauthorized for token [{}]","messagePattern":"Principal assigned to token \\[(.+?)\\] is unauthorized for token \\[(.+?)\\]","errorType":"exception","errorClass":"FailedLoginException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-simple-mfa-core/src/main/java/org/apereo/cas/mfa/simple/validation/BaseCasSimpleMultifactorAuthenticationService.java","lineNumber":41,"sourceCode":"public abstract class BaseCasSimpleMultifactorAuthenticationService implements CasSimpleMultifactorAuthenticationService {\n    protected final TicketRegistry ticketRegistry;\n\n    @Override\n    public CasSimpleMultifactorAuthenticationTicket getMultifactorAuthenticationTicket(final CasSimpleMultifactorTokenCredential credential) {\n        val tokenId = normalize(credential.getId());\n        return ticketRegistry.getTicket(tokenId, CasSimpleMultifactorAuthenticationTicket.class);\n    }\n\n    protected Principal validateTokenForPrincipal(final Principal resolvedPrincipal, final CasSimpleMultifactorAuthenticationTicket acct)\n        throws FailedLoginException {\n        if (!acct.getProperties().containsKey(CasSimpleMultifactorAuthenticationConstants.PROPERTY_PRINCIPAL)) {\n            LOGGER.warn(\"Unable to locate principal for token [{}]\", acct.getId());\n            deleteToken(acct);\n            throw new FailedLoginException(\"Failed to authenticate code \" + acct.getId());\n        }\n        val principal = (Principal) acct.getProperties().get(CasSimpleMultifactorAuthenticationConstants.PROPERTY_PRINCIPAL);\n        if (!principal.equals(resolvedPrincipal)) {\n            LOGGER.warn(\"Principal assigned to token [{}] is unauthorized for token [{}]\", principal.getId(), acct.getId());\n            deleteToken(acct);\n            throw new FailedLoginException(\"Failed to authenticate code \" + acct.getId());\n        }\n        return principal;\n    }\n\n    protected static String normalize(final String tokenId) {\n        if (!tokenId.startsWith(CasSimpleMultifactorAuthenticationTicket.PREFIX)) {\n            return CasSimpleMultifactorAuthenticationTicket.PREFIX + UniqueTicketIdGenerator.SEPARATOR + tokenId;\n        }\n        return tokenId;\n    }\n\n    protected void deleteToken(final CasSimpleMultifactorAuthenticationTicket acct) {\n        FunctionUtils.doUnchecked(_ -> ticketRegistry.deleteTicket(acct.getId()));\n    }\n\n}","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-simple-mfa-core/src/main/java/org/apereo/cas/mfa/simple/validation/BaseCasSimpleMultifactorAuthenticationService.java#L23-L59","documentation":"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.","triggerScenarios":"Presenting a simple-MFA code whose stored principal's id differs from the resolved principal id of the current authentication attempt.","commonSituations":"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).","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Pre-check ownership before submitting\nPrincipal stored = (Principal) ticket.getProperties()\n    .get(CasSimpleMultifactorAuthenticationConstants.PROPERTY_PRINCIPAL);\nif (!stored.getId().equals(currentUserPrincipal.getId()))\n    throw new IllegalStateException(\"Token belongs to a different user\");","typeGuard":null,"tryCatchPattern":"try {\n    service.validateTokenForPrincipal(resolvedPrincipal, ticket);\n} catch (FailedLoginException e) {\n    // treat as invalid code: delete token, issue a fresh one for this user\n}","preventionTips":["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"],"tags":["mfa","authentication","principal-mismatch","security"],"backgroundTag":"invalid-token","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}