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
tfaLoginFormPreValidation(Prepended to#validate):- Retrieves the entered username from
$form_state->getValue('name'). - Tells
TfaLoginContextto temporarily suspend the requirement for a second-factor code duringuser.authvalidation. tfaLoginFormPostValidation(Appended to#validate):- Restores the default token validation requirement in
TfaLoginContext.
Submission Handlers
tfaValidateSubmit(Prepended to#submit):- Inspects
$form_state->get('uid'). - Determines whether the user is required to complete TFA.
- If TFA is required, calls
$form_state->set('uid', NULL)(or removes the storage key) so that core'sUserLoginForm::submitForm()cannot complete login. - Stores the validated UID and an authentication hash in
$form_statestorage for TFA's redirect handler. tfaSetRedirectAfterSubmit(Appended to#submit):- Clears the
destinationquery parameter from the current request stack to prevent Drupal core from forcing an immediate redirect to an unauthenticated destination page. - 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
#validateor#submitarrays: 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
tfaruns its form alters cleanly. If another module modifies the callback arrays destructively, adjust module weights usinghook_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 aftertfaSetRedirectAfterSubmit()and unconditionally calls$form_state->setRedirect(), it will overwrite the redirect to/tfa/{uid}/{hash}. - Because the
uidwas 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.