Pre-Production Verification Checklist
This document is an operational verification checklist and audit guide to run against a Drupal site after the TFA module (2.x) has been installed and configured, but before deploying to production.
Its primary purpose is to verify that two-factor authentication cannot be silently bypassed by custom code, contrib module conflicts, or infrastructure misconfigurations, and that any deliberate bypass is properly documented with compensating controls.
For full architectural background on TFA's 3-layer security model, see Enforcement Architecture.
1. Environment & Infrastructure Readiness
1.1 Cache Backend Verification (cache.tfa_memcache)
TFA relies on a dedicated cache.tfa_memcache bin for request-scoped security flags (tfa_complete, tfa_auth_method_bypass_approved, tfa_account_switcher_bypass).
Critical Security Requirement
This bin must use non-persistent memory storage (Drupal\Core\Cache\MemoryBackend). If shared persistent caches (Redis, Memcached, database cache) are inadvertently applied to this bin, authentication approval flags will leak across requests and user sessions. See Install & Infrastructure Hardening.
Run the following Drush verification command against the runtime environment:
* Pass criteria: Output confirmsstring(...) "Drupal\Core\Cache\MemoryBackend".
* Remediation if failed: Ensure $settings['cache']['bins']['tfa_memcache'] = 'cache.backend.memory'; is defined in settings.php after all global cache backend declarations.
1.2 Encryption Profile & Key Decryption Pre-Flight
TFA validation plugins (TOTP, HOTP, Recovery Codes) encrypt sensitive seeds and tokens using Drupal's Key and Encrypt modules. Uncaught encryption service exceptions (e.g., missing key files, invalid key configuration) produce fatal HTTP 500 errors during user authentication.
Execute a decryption pre-flight check for the active encryption profile:
drush php:eval "
\$config = \Drupal::config('tfa.settings');
\$profile_id = \$config->get('encryption_profile') ?: \$config->get('default_validation_plugin');
echo 'Configured encryption profile: ' . \$profile_id . PHP_EOL;
if (\$profile = \Drupal\encrypt\Entity\EncryptionProfile::load(\$profile_id)) {
\$service = \Drupal::service('encryption');
\$encrypted = \$service->encrypt('test-payload', \$profile);
\$decrypted = \$service->decrypt(\$encrypted, \$profile);
echo (\$decrypted === 'test-payload') ? 'SUCCESS: Encryption profile operational.' : 'FAIL: Decryption mismatch.';
} else {
echo 'FAIL: Encryption profile could not be loaded.';
}
echo PHP_EOL;
"
SUCCESS: Encryption profile operational.
* Remediation if failed: Check Key entity configuration at /admin/config/system/keys and Encryption Profile at /admin/config/system/encryption/profiles.
1.3 Version Pinning in composer.json
TFA 2.x public APIs and service decorators are under active evolution.
* Check: Verify that composer.json pins an exact version or commit hash (e.g., "drupal/tfa": "2.0.0-alpha5" or specific commit) rather than an open wildcard.
* Procedure: Review the root composer.json and ensure test suites are re-run on any version bump.
2. Service Decoration & Compiler Integrity
Verify that TFA's Symfony service decorators and container compiler passes remain active in the compiled container.
2.1 user.auth Decoration Chain
TFA decorates user.auth (TfaUserAuth, decoration_priority: 100) to enforce token validation during credential checks.
Drupal\tfa\TfaUserAuth (or a known, tested decorator wrapping it).
* Investigation: If another class is returned, search custom and contrib modules for user.auth overrides:
Refer to Module Compatibility: Service Decoration Collisions.
2.2 account_switcher Decoration Chain
TFA decorates account_switcher (TfaAccountSwitcher, decoration_priority: 250) to track legitimate administrative switches.
Drupal\tfa\TfaAccountSwitcher.
2.3 authentication_provider Tag Registrations
Enumerate all registered authentication providers on the site:
drush php:eval "print_r(array_keys(\Drupal::getContainer()->findTaggedServiceIds('authentication_provider')));"
cookie and http_basic: Protected by core TFA handlers; must not be exempted.
- Third-party API / OAuth / JWT providers: Must either delegate to user.auth, be explicitly listed in tfa.auth_provider_bypass, or be documented as an accepted exception.
- Review Exempting Authentication Providers and SSO / externalauth Integrations.
3. Code-Path & Session Integrity Audits
TfaUserSetSubscriber intercepts AccountEvents::SET_USER with priority 50 to fail closed against unvalidated account assignments. Audit custom and contrib code for non-standard session or user manipulation.
3.1 Direct Session UID Injection
Search for code writing raw UIDs into sessions, bypassing user_login_finalize():
grep -rn "session->set('uid'" web/modules/custom --include="*.php"
grep -rn "\$_SESSION\['uid'\]" web/modules/custom --include="*.php"
user_login_finalize() and go through the TFA challenge flow.
3.2 Direct AccountProxy::setAccount() Invocations
Search for direct setAccount() calls in custom modules:
\Drupal::service('account_switcher')->switchTo($account) instead. See AccountEvents::SET_USER Protection.
3.3 Custom One-Time Login Links & Direct Finalization
Search for user_login_finalize() outside standard login flows:
user.reset.login routes. Any custom magic-link, token login, or impersonation controller calling user_login_finalize() will be blocked by TfaUserSetSubscriber unless integrated with TFA. See Module Compatibility: Password Resets & Drush.
3.4 Direct Manipulation of Internal TFA Flags
Scan the codebase outside the tfa module for unauthorized reading/writing of internal memory cache or session flags:
grep -rnE "(tfa_complete|bypass_tfa_auth_for_user|tfa_auth_method_bypass_approved|tfa_account_switcher_bypass)" \
--exclude-dir="tfa" \
web/modules/ web/sites/ web/themes/
tfa module folder. Any external writing to these flags represents a potential authentication bypass.
3.5 Account Switcher Caller Audit
Search custom and contrib modules for direct calls to account_switcher:
3.6 Discovery of externalauth SSO Consumers
Enumerate all active modules and code consuming externalauth across the codebase:
# 1. List enabled contrib modules dependent on externalauth
drush pm:list --status=enabled --filter="externalauth"
# 2. Search codebase for direct service injections or invocations of userLoginFinalize
grep -rnE "(externalauth\.externalauth|\$this->externalAuth|ExternalAuthInterface|userLoginFinalize)" \
web/modules/custom web/modules/contrib
tfa.auth_provider_bypass) or if Drupal must challenge the second factor. See Module Compatibility: SSO.
4. Form Lifecycle & Module Compatibility
4.1 Login Form Alter Handlers
Audit modules altering user_login_form or user_login_block:
$form['#validate'] or $form['#submit'] after TFA registers tfaLoginFormPreValidation, tfaValidateSubmit, and tfaSetRedirectAfterSubmit.
* See Login Form Alterations.
4.2 Login Redirect Handlers (login_destination, Custom Redirects)
- Risk: If a module attaches a post-login redirect handler that runs after TFA's submit handler and unconditionally overwrites
$form_state->setRedirect(), the user may be redirected away from/tfa/{uid}/{hash}. Because the UID was stripped, the user arrives at the destination unauthenticated. - See Module Compatibility: Login Redirect Modules.
4.3 Alternate Identifier Logins (mail_login, email_registration)
- Risk: If an email-to-username module converts credentials inside
user.authrather than before,TfaUserAuthmay fail account lookup (loadByProperties(['name' => $username])). - See Module Compatibility: Alternate Identifiers.
5. Documented Exception Register
Before going live, any authentication provider, API endpoint, or administrative script that deliberately operates without Drupal TFA enforcement must be reviewed and recorded in the project's exception register:
| Provider / Path | Mechanism | Justification | Compensating Control | Approved By | Review Date |
|---|---|---|---|---|---|
e.g., oauth2_token |
Listed in tfa.auth_provider_bypass |
Machine-to-machine REST API | Short-lived JWTs, IP allowlist, automated token rotation | SecOps Lead | YYYY-MM-DD |
e.g., samlauth |
Listed in tfa.auth_provider_bypass |
Enterprise SSO IdP | MFA enforced upstream at Okta/Azure AD IdP | CISO / IAM Team | YYYY-MM-DD |
e.g., drush uli |
CLI one-time login | Emergency break-glass admin recovery | Server SSH key required, bastion host logging | Lead DevOps | YYYY-MM-DD |
If no exceptions are approved, verify that tfa.auth_provider_bypass is empty in all services.yml files.
6. End-to-End Runtime Test Protocol
Execute the following 8 tests against the built staging/pre-production environment:
Test 1: Baseline TFA Challenge & Unauthenticated State
- Enable TFA and configure a TOTP seed for a test account whose role requires TFA.
- Submit valid username and password on
/user/login. - Verify: Browser redirects to
/tfa/{uid}/{hash}. - Open a second tab to
/useror any authenticated page. - Verify: User remains anonymous (session is not established until the second factor is validated).
Test 2: Token Entry & Login Completion
- On the
/tfa/{uid}/{hash}challenge screen, enter the correct 6-digit TOTP code. - Submit the form.
- Verify: User is authenticated, redirected to their account or target destination, and the session functions normally.
Test 3: Invalid / Expired Code Rejection & Flood Control
- Submit an invalid TOTP code on
/tfa/{uid}/{hash}. - Verify: Access is rejected with an error message; session remains unauthenticated.
- Submit repeated invalid attempts.
- Verify: Flood control engages and blocks further attempts for the configured window.
Test 4: Direct Session Injection Immunity (Fail-Closed Check)
- In a non-production test harness, programmatically write an arbitrary UID into the session storage without calling TFA validation.
- Send a request with that session cookie.
- Verify:
TfaUserSetSubscriberrejects the request with an HTTP 403 Access Denied and destroys the session.
Test 5: Redirect Module Interaction
- If using
login_destinationor custom redirect rules, initiate a login with a?destination=/admin/reports/statusquery parameter. - Verify: The initial redirect lands on
/tfa/{uid}/{hash}(TFA challenge is not bypassed). - Complete the TFA challenge.
- Verify: Post-TFA redirect lands on the expected destination
/admin/reports/status.
Test 6: Alternate Login Identifier (Email Login)
- If
mail_loginor similar modules are enabled, attempt logging in using the user's email address and password. - Verify: TFA challenge screen is reached and authentication completes normally.
Test 7: One-Time Login / Password Reset Flow
- Generate a password reset link for a TFA-enabled user (
drush uli --name=testuseror via/user/password). - Visit the reset link in a clean browser session.
- Verify: TFA redirects to the challenge form before allowing the user to set a new password or access authenticated routes.
Test 8: Administrative Account Switching (masquerade)
- Log in as an administrator who has completed TFA.
- Use
masqueradeor core'saccount_switcherto switch to a test user. - Verify: Switch succeeds without requiring the test user's TOTP code.
- Switch back to the administrator.
- Verify: Administrative session remains intact and valid.
Test 9: Stateless Gateway & Intermediate Session Isolation (JSON:API / REST)
- Initiate a login at
/user/loginwith a TFA-required account, stopping at/tfa/{uid}/{hash}. - Copy the session cookie from developer tools and send
curl -b "SSESS...=..." https://example.com/jsonapi/user/user. - Verify: Endpoint returns HTTP 401/403 (session is anonymous). The browser session remains valid to complete the OTP challenge.
Test 10: HTTP Basic Auth Passcode Concatenation
- Send a request with standard HTTP Basic Auth headers (
username:password) for a TFA-required account. - Verify: Request returns HTTP 401/403.
- Send the request with the TOTP code appended to the password (
username:password<totp_code>). - Verify: Request returns HTTP 200 OK.
Test 11: Federated SSO Skip Depletion & Lockout Check
- For an active SSO provider (
samlauth,openid_connect, etc.), configure a test account whose role requires TFA, with allowed skips set to1, but leave TFA unconfigured. - Initiate login via the SSO provider.
- Verify: First login succeeds, displaying a warning that 0 skips remain.
- Log out and initiate a second SSO login with the same unconfigured account.
- Verify: Login fails closed with an HTTP 403 Forbidden (no session established).