Skip to content

TFA Altering of Login Forms

Overview

TFA alters standard login forms (user_login_form and user_login_block) to integrate two-factor challenges into the core Drupal authentication lifecycle without disrupting password validation or contrib modules.

Key actions taken by TfaLoginFormHelper::alterLoginForm(): * Temporarily disables token validation in user.auth: Allows core and contrib modules to validate the submitted password alone during initial form validation. * Prevents premature session finalization: Unsets the uid property from $form_state so core's UserLoginForm::submitForm() does not execute user_login_finalize(). * Redirects to the TFA Challenge: Sets a redirect to TfaEntryForm (/tfa/{uid}/{hash}) to capture the second factor. * Strips the destination parameter: Ensures query parameter redirects do not bypass the challenge screen.

Order of Operations & Form Lifecycle

---
title: Order of Operations in Login Form Validation and Submission
---
flowchart TD
  FORM_SUBMIT["User submits user_login_form"] --> PRE_VAL["TFA Pre-Validation<br>(tfaLoginFormPreValidation)"]
  PRE_VAL -->|Disables user.auth token requirement| VALIDATE_PASS["Core & Contrib Password Validation<br>(::validateAuthentication / ::validateFinal)"]
  VALIDATE_PASS --> POST_VAL["TFA Post-Validation<br>(tfaLoginFormPostValidation)"]
  POST_VAL -->|Re-enables user.auth token requirement| SUBMIT_START["Form Submit Processing"]

  SUBMIT_START --> TFA_VAL_SUBMIT["TFA Submit Start<br>(tfaValidateSubmit)"]
  TFA_VAL_SUBMIT --> CHECK_REQ{"TFA Token Entry<br>Required?"}

  CHECK_REQ -->|YES| UNSET_UID["Unset $form_state UID<br><i>Prevents UserLoginForm::submitForm() login</i>"]
  UNSET_UID --> CORE_SUBMIT["Core UserLoginForm::submitForm<br><i>(Does not log in because UID is missing)</i>"]
  CORE_SUBMIT --> TFA_REDIRECT["TFA Submit End<br>(tfaSetRedirectAfterSubmit)"]
  TFA_REDIRECT --> REDIRECT_ENTRY["Redirect to /tfa/{uid}/{hash}"]

  CHECK_REQ -->|NO| CORE_LOGIN["Core completes user login<br>(user_login_finalize)"]

Form API Handler Injection

TFA inserts specific #validate and #submit callbacks during hook_form_alter:

Validation Callbacks

  1. tfaLoginFormPreValidation (Prepended to #validate):
  2. Retrieves the entered username from $form_state->getValue('name').
  3. Tells TfaLoginContext to temporarily suspend the requirement for a second-factor code during user.auth validation.
  4. tfaLoginFormPostValidation (Appended to #validate):
  5. Restores the default token validation requirement in TfaLoginContext.

Submission Handlers

  1. tfaValidateSubmit (Prepended to #submit):
  2. Inspects $form_state->get('uid').
  3. Determines whether the user is required to complete TFA.
  4. If TFA is required, calls $form_state->set('uid', NULL) (or removes the storage key) so that core's UserLoginForm::submitForm() cannot complete login.
  5. Stores the validated UID and an authentication hash in $form_state storage for TFA's redirect handler.
  6. tfaSetRedirectAfterSubmit (Appended to #submit):
  7. Clears the destination query parameter from the current request stack to prevent Drupal core from forcing an immediate redirect to an unauthenticated destination page.
  8. Sets $form_state->setRedirect('tfa.entry', ['user' => $uid, 'hash' => $hash]).

Integration Pitfalls & Module Interactions

1. Form Alter Ordering and Module Weights

Drupal executes hook_form_alter() implementations in order of module weight, then alphabetically by module name.

  • Rebuilding #validate or #submit arrays: If another contrib or custom module resets or re-indexes $form['#validate'] or $form['#submit'] wholesale after TFA's hook has run, TFA's injected callbacks may be removed.
  • Verification: Ensure that tfa runs its form alters cleanly. If another module modifies the callback arrays destructively, adjust module weights using hook_module_implements_alter() so TFA's alter runs last.

2. Destination Parameter and Post-Login Redirects

TFA explicitly clears the destination query parameter in tfaSetRedirectAfterSubmit() to prevent core's RedirectDestination service from overriding the TFA challenge redirect.

  • If a custom module or redirect module (such as login_destination) attaches a submit handler that executes after tfaSetRedirectAfterSubmit() and unconditionally calls $form_state->setRedirect(), it will overwrite the redirect to /tfa/{uid}/{hash}.
  • Because the uid was already unset by TFA, the user will arrive at the destination page as an anonymous user, causing confusing UX.
  • Always check that redirect modules respect existing redirects to tfa.* routes.

3. Missing Username Field in Non-Standard Forms

TfaLoginFormHelper::tfaLoginFormPreValidation() expects the username to be present in $form_state->getValue('name'). If a custom login form uses an alternative field name (e.g., login_identifier or email) without mapping it to name prior to validation, TFA will not disable token validation in time, causing credential checks to fail.

4. Backstop Protection

All form-level alterations are backed by the fail-closed TfaUserSetSubscriber. Even if a misconfigured form alter or custom submit handler somehow calls user_login_finalize() without going through the TFA entry form, TfaUserSetSubscriber intercepts AccountEvents::SET_USER and terminates the session.