Skip to content

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 AuthenticationSubscriber completes authentication for the incoming request.
  • Interactive logins: When user_login_finalize() completes a successful login.
  • Programmatic account switches: When \Drupal::service('account_switcher')->switchTo() or switchBack() 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.