AccountEvents::SET_USER Event Protection
The AccountEvents::SET_USER event is dispatched by \Drupal\Core\Session\AccountProxy::setAccount() (the Drupal current_user service) upon every user context change.
For an overview of how this event subscriber fits into TFA's overall defense-in-depth model, see Enforcement Architecture.
Common Triggers for the Event
This event is dispatched whenever the active account context is established or switched:
- Every authenticated request: When
AuthenticationSubscribercompletes authentication for the incoming request. - Interactive logins: When
user_login_finalize()completes a successful login. - Programmatic account switches: When
\Drupal::service('account_switcher')->switchTo()orswitchBack()is called.
Simplified Callstack
---
title: Simplified Callstack Related to Setting the Active User
---
flowchart TD
IMPER["Account Switcher<br>(account_switcher service)"]
AUTH_SUBSCRIBER["AuthenticationSubscriber<br>(Every request)"]
SET_ACCOUNT["AccountProxy::setAccount()"]
FINALIZE_LOGIN["user_login_finalize()"]
TFA_USER_LOGIN["tfa_user_login()<br>Sets session flag for future requests"]
TFA_USER_SET_SUBSCRIBER["TfaUserSetSubscriber"]
TFA_PERMIT["Allow Request / User Context Set"]
TFA_ACCESS_DENIED["Throw TfaAccessDenied<br>(HTTP 403)"]
IMPER --> SET_ACCOUNT
AUTH_SUBSCRIBER --> SET_ACCOUNT
FINALIZE_LOGIN <--> |1| SET_ACCOUNT
FINALIZE_LOGIN <--> |2| TFA_USER_LOGIN
SET_ACCOUNT --> TFA_USER_SET_SUBSCRIBER
TFA_USER_SET_SUBSCRIBER --> TFA_ACCESS_DENIED
TFA_USER_SET_SUBSCRIBER --> TFA_PERMIT
TfaUserSetSubscriber Processing Flow
---
title: TfaUserSetSubscriber Processing of AccountEvents::SET_USER Events
---
flowchart TD
TFA_PERMIT["Allow Request"]
TFA_SET_MEM["Set Validated in Memory Cache"]
REVOKE_SESSION["Invalidate Session"]
TFA_ACCESS_DENIED["Throw TfaAccessDenied (403)"]
IS_EXEMPT_PROVIDER{"Requested by exempt<br>Authentication Provider?"}
IS_ACCOUNT_SWITCHER{"Requested by<br>'account_switcher'?"}
IS_ANONYMOUS{"Target Account<br>is Anonymous (UID 0)?"}
IS_VALIDATED_SESSION{"Session contains verified<br>tfa_complete flag for UID?"}
LOGIN_THIS_REQUEST{"user.auth validated<br>in this request?"}
IS_TFA_DISABLED{"Is TFA disabled for user<br>or globally?"}
CAN_LOGIN_WITHOUT_TFA{"Can user login without TFA<br>(grace period / role not required)?"}
IS_EXEMPT_PROVIDER -->|Yes| TFA_PERMIT
IS_EXEMPT_PROVIDER -->|No| IS_ACCOUNT_SWITCHER
IS_ACCOUNT_SWITCHER -->|Yes| TFA_PERMIT
IS_ACCOUNT_SWITCHER -->|No| IS_ANONYMOUS
IS_ANONYMOUS -->|Yes| TFA_PERMIT
IS_ANONYMOUS -->|No| IS_VALIDATED_SESSION
IS_VALIDATED_SESSION -->|Yes| TFA_PERMIT
IS_VALIDATED_SESSION -->|No| LOGIN_THIS_REQUEST
LOGIN_THIS_REQUEST -->|Yes| TFA_PERMIT
LOGIN_THIS_REQUEST -->|No| IS_TFA_DISABLED
IS_TFA_DISABLED -->|Yes| TFA_SET_MEM
IS_TFA_DISABLED -->|No| CAN_LOGIN_WITHOUT_TFA
CAN_LOGIN_WITHOUT_TFA -->|Yes| TFA_SET_MEM
CAN_LOGIN_WITHOUT_TFA -->|No| REVOKE_SESSION
TFA_SET_MEM --> TFA_PERMIT
REVOKE_SESSION --> TFA_ACCESS_DENIED
Validation Conditions Explained
TfaUserSetSubscriber::rejectUserIfTfaBypassed() verifies the account context against the following checkpoints:
1. Request by Exempt Authentication Provider
Authentication providers configured in tfa.auth_provider_bypass are decorated by TfaAuthDecorator or TfaChallengeAuthDecorator. When they successfully authenticate a request, a request-scoped flag (tfa_auth_method_bypass_approved) is placed in cache.tfa_memcache for the authenticated UID. If this matches the target account, access is granted and the flag is immediately cleared.
2. Request by Account Switcher Service
The account_switcher service is used to temporarily run operations as another user. TfaAccountSwitcher sets a temporary tfa_account_switcher_bypass flag in cache.tfa_memcache during switchTo() and clears it in switchBack(). Legitimate switches are permitted without challenging the administrator for the target account's token.
3. Set Anonymous User
The anonymous user (UID 0) has no credentials and is always permitted.
4. Session Contains a tfa_complete Record
For standard cookie/session authentication, tfa_user_login() writes a tfa_complete marker (storing the UID) directly into the session during user_login_finalize(). On subsequent requests, TfaUserSetSubscriber verifies that the active session's tfa_complete UID matches the account being loaded.
5. The user.auth Service Validated This Request
When authenticating via HTTP Basic Auth or direct user.auth invocation, TfaUserAuth sets a tfa_complete record in cache.tfa_memcache matching the UID. If this flag exists for the current request, access is permitted.
6. TFA Is Disabled or Not Required
If the user account is not required to use TFA (based on configured role requirements) or is within an active setup grace period, TfaLoginContext permits the login and caches the approval for the remainder of the request.
Fail-Secure Guarantee
If none of the validation conditions match the exact UID being set:
1. The active session is purged ($this->session->invalidate()).
2. A \Drupal\tfa\Exceptions\TfaAccessDenied exception is thrown.
3. The exception is converted by TfaUserSetSubscriber::onException() into an HTTP 403 maintenance response.
Internal Storage Flags & Session Keys
TfaUserSetSubscriber relies on the following internal flags to evaluate account switches:
| Scope | Storage Key | Legitimate Owner & Purpose | Security Consequence if Manipulated Externally |
|---|---|---|---|
Memory Cache (cache.tfa_memcache) |
tfa_complete |
TfaUserAuth::authenticate() (validates credentials + OTP) |
Unconditional bypass of TfaUserSetSubscriber for the target UID during the current request. |
Memory Cache (cache.tfa_memcache) |
bypass_tfa_auth_for_user |
TfaLoginFormHelper::tfaLoginFormPreValidation() |
Bypasses OTP validation in TfaUserAuth. |
Memory Cache (cache.tfa_memcache) |
tfa_auth_method_bypass_approved |
TfaAuthDecorator::authenticate() (exempt providers) |
Grants request-level exemption to TfaUserSetSubscriber. |
Memory Cache (cache.tfa_memcache) |
tfa_account_switcher_bypass |
TfaAccountSwitcher::switchTo() |
Allows switching user context without triggering Layer 3 rejection. |
Session ($session) |
tfa_complete |
TfaLoginFormHelper::loginWithTfa() |
Marks persistent session as having completed MFA. |
Session ($session) |
tfa_user |
TfaLoginFormHelper::loginWithTfa() |
Tracks active authenticated UID for session matching. |
Security Notice
This fail-closed backstop relies on cache.tfa_memcache utilizing non-persistent, request-scoped memory storage (cache.backend.memory). See Install & Infrastructure Hardening for caching setup instructions.