Multi-Factor Auth (MFA)
Last updated: 04/17/2026 · Written by Agent0
Multi-Factor Authentication (MFA)
StackCTL includes built-in multi-factor authentication. When enabled, users who have MFA set up on their account are required to enter a one-time code after their password before being granted access. It supports email-based OTP codes, and includes a trusted device feature so users aren't challenged on every login from a familiar browser.
Enabling MFA
Set mfa_enabled to true in config/auth.php:
'mfa_enabled' => true, 'mfa_driver' => 'email', // 'email', 'app', or 'both' 'mfa_expiration_minutes' => 5,
Enabling MFA at the app level doesn't force it on every user — it makes MFA available. Each user opts in from their profile page. Only users who have enabled and verified their MFA method will be challenged at login.
MFA Drivers
- email — A 6-digit code is generated and emailed to the user. No additional package required. Requires mail to be configured in
config/mail.php. - app — TOTP support for authenticator apps like Google Authenticator or Authy. Requires the
robthree/twofactorauthpackage:composer require robthree/twofactorauth - both — Makes both methods available
The Login Flow with MFA
- User enters email and password as normal
- If credentials are valid and the user has MFA enabled, the system checks for a trusted device cookie
- If the browser is trusted — a new code is not sent and the user is logged in directly
- If the browser is not trusted — a 6-digit code is generated, stored in
mfa_challengeswith the configured expiry, and emailed to the user - The user is redirected to
/otpto enter the code - On success, the challenge is marked as used and the user is logged in
- If the user checks "Trust this device", a secure token is stored and a 30-day cookie is set — future logins from that browser skip the MFA challenge
User Setup — From the Profile Page
Users enable MFA themselves from their profile page at /profile. The flow is:
- User clicks "Enable MFA" — a verification code is sent to their email
- User enters the code on their profile page to confirm the setup
- MFA is now active on their account — they'll be challenged on next login from an untrusted browser
- User can disable MFA at any time from the same profile page
The profile page handles this entirely — no code changes needed.
Trusted Devices
When a user successfully completes an MFA challenge and checks "Trust this device", a hashed token is stored in the trusted_browsers table and a cookie is set in their browser for 30 days. On future logins, the cookie is compared against the stored hash — if it matches and hasn't expired, the MFA step is skipped entirely for that browser.
Trusted device records are user-specific — clearing them from the database (or letting them expire) will require the user to complete MFA again on that browser.
Code Expiry & Cleanup
MFA codes expire based on mfa_expiration_minutes in config/auth.php. Expired and used codes accumulate in the mfa_challenges table over time. Set up a cron job to clean them up periodically:
DELETE FROM mfa_challenges WHERE expires_at < NOW() OR used_at IS NOT NULL;
A cleanup interval of every 5–15 minutes is recommended.
Disabling MFA
Set mfa_enabled to false in config/auth.php. The MFA check is skipped entirely during login and the MFA section is hidden on the profile page. Existing mfa_methods records are left in the database but have no effect.
'mfa_enabled' => false