When adding Fortify two-factor authentication to a Laravel tax-filing app, what do the confirm and confirmPassword options change, and how does login become two steps?
answer
- TwoFactorAuthenticatable trait on User
- three columns, confirmed_at included
- confirm: not active until a code proves it
- confirmPassword adds password.confirm
- login.id in session, then /two-factor-challenge
basics
~20 sWith confirm => true, enabling stores a secret but login is not challenged until the user submits a valid code; confirmPassword => true puts password.confirm on the management routes. Login then stops at /two-factor-challenge before signing in.
solid answer
~50 sThe `User` model uses `Laravel\Fortify\TwoFactorAuthenticatable`, and Fortify's migration adds `two_factor_secret`, `two_factor_recovery_codes` and `two_factor_confirmed_at`. `POST /user/two-factor-authentication` stores an encrypted secret and eight recovery codes. With `'confirm' => true`, as in the Fortify 1.40 stub, that is only half-enabled: the user must post a valid code to `/user/confirmed-two-factor-authentication`, which sets `two_factor_confirmed_at`, and only then does login challenge them, so nobody is locked out by a QR code they never scanned. `'confirmPassword' => true` adds the `password.confirm` middleware to the enable, confirm, disable, QR code, secret key and recovery-code routes. At login, `RedirectIfTwoFactorAuthenticatable` checks the password, and for a two-factor user puts `login.id` and `login.remember` in the session without signing in, then redirects to `/two-factor-challenge` (or returns `{"two_factor": true}` for JSON). Posting a valid `code` or `recovery_code` there signs the user in and regenerates the session. A bare `Features::twoFactorAuthentication()` leaves both options off.
code
php · 15 lines<?php
// config/fortify.php (excerpt)
use Laravel\Fortify\Features;
return [
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
]),
],
];go deeper
Know the steps: add the TwoFactorAuthenticatable trait, run the migration, enable the feature, and expect a /two-factor-challenge screen after the password.
Explain what confirm and confirmPassword each change, the two_factor_confirmed_at column, and how login.id carries a half-authenticated user to the challenge.
Show the production judgement: why confirm prevents lockouts, why management routes need password confirmation, APP_KEY rotation risk, and custom logins bypassing the challenge.
Decide whether two-factor is optional or mandatory for a regulated product, and how support recovers locked-out users without weakening the control.
## The scenario A tax-filing application holds income records and bank details, so it adds **two-factor authentication** (a time-based code from an authenticator app) on top of passwords. With Fortify, the work is mostly configuration plus screens; the protocol details of TOTP itself are a separate topic. ## Setting it up 1. Enable the feature in `config/fortify.php`. The Fortify 1.40 stub already has `Features::twoFactorAuthentication(['confirm' => true, 'confirmPassword' => true])`. 2. Add the `Laravel\Fortify\TwoFactorAuthenticatable` trait to `App\Models\User`. 3. Run the published migration, which adds three nullable columns to `users`: `two_factor_secret`, `two_factor_recovery_codes` and `two_factor_confirmed_at`. 4. Build the settings screen and the challenge screen, and bind the latter with `Fortify::twoFactorChallengeView()` if views are enabled. The trait gives the model `hasEnabledTwoFactorAuthentication()`, `recoveryCodes()`, `replaceRecoveryCode()`, `twoFactorQrCodeSvg()` and `twoFactorQrCodeUrl()`. Secrets and recovery codes are stored **encrypted** with the application's encrypter (via `Fortify::currentEncrypter()`), so rotating `APP_KEY` without care makes them unreadable. ## Enabling, then confirming `POST /user/two-factor-authentication` runs `EnableTwoFactorAuthentication`, which writes a new secret and eight recovery codes. What happens next depends on `confirm`: | | `confirm` off | `confirm` on (stub) | |---|---|---| | After enabling | two-factor is active immediately | secret stored, `two_factor_confirmed_at` still null | | Login challenges the user when | `two_factor_secret` is set | secret set **and** `two_factor_confirmed_at` not null | | Extra step | none | `POST /user/confirmed-two-factor-authentication` with a valid `code` | | Risk it removes | none | locking out a user who never finished scanning the QR code | `ConfirmTwoFactorAuthentication` verifies the code against the decrypted secret and sets `two_factor_confirmed_at = now()`; a bad code raises a validation error in the `confirmTwoFactorAuthentication` error bag. ## `confirmPassword` When `confirmPassword` is true, Fortify adds Laravel's `password.confirm` middleware to every two-factor management route: enable, confirm, disable (`DELETE /user/two-factor-authentication`), the QR code and secret key endpoints, and viewing or regenerating recovery codes. Someone at an unlocked laptop cannot switch two-factor off or read the recovery codes without re-entering the password. How long that confirmation lasts is Laravel's `password_timeout` setting. Note the defaults: the options are read with a strict `=== true` check, so a bare `Features::twoFactorAuthentication()`, which is what Fortify's own package config uses, turns **both** off. Applications get the stub, which turns both on. ## Login becomes two steps With the feature enabled, the login pipeline includes `RedirectIfTwoFactorAuthenticatable` before `AttemptToAuthenticate`: 1. It validates the username and password through the guard's provider (or your `authenticateUsing` callback). 2. If the user has a confirmed secret and uses the trait, it **does not sign them in**. It stores `login.id` and `login.remember` in the session, dispatches `TwoFactorAuthenticationChallenged`, and redirects to the `two-factor.login` route, `/two-factor-challenge`. A JSON request instead gets `{"two_factor": true}` so an SPA can show its own challenge screen. 3. The user posts either `code` or `recovery_code` to `/two-factor-challenge`. The route is throttled by the `two-factor` limiter. 4. On success Fortify removes `login.id`, logs the user in with the remembered flag, **regenerates the session**, and redirects to `home` (a 204 for XHR). A used recovery code is replaced with a new one. Users without two-factor skip step 2 and are signed in by `AttemptToAuthenticate` as before. ## Interview-worthy details - Two-factor applies only to Fortify's login route; a custom login controller that calls `Auth::attempt` directly bypasses it. - Recovery codes should be shown once and regenerated after use, which Fortify supports through `POST /user/two-factor-recovery-codes`. - The `login.id` session value identifies a half-authenticated user, so the challenge route is behind the `guest` middleware and the session is regenerated after success.
- A tax-filing user enabled two-factor but lost the phone before scanning the QR code; with confirm on, can they still log in?Yes. With `confirm` on, the login challenge only fires when `two_factor_confirmed_at` is set, and that happens only after a valid code is submitted. An unconfirmed secret is ignored at login, so the user signs in with the password and can disable or restart the setup.
- How does an SPA using Fortify know to show the two-factor screen after POST /login?For a request that wants JSON, `RedirectIfTwoFactorAuthenticatable` responds with `{"two_factor": true}` instead of redirecting. The SPA checks that flag and then posts `code` or `recovery_code` to `/two-factor-challenge`, which returns 204 on success.
- What happens to a recovery code after it is used at the challenge?Fortify calls `replaceRecoveryCode()` on the user, swapping the used code for a newly generated one in the encrypted `two_factor_recovery_codes` column and dispatching `RecoveryCodeReplaced`, so each code works once.
saying these in an interview costs you the question
- Enabling two-factor with confirm on challenges the user at the very next login.
- Fortify stores the two-factor secret in plain text beside the password hash.
- After a correct password Fortify signs the user in, then asks for the code.
- confirmPassword makes users re-enter the password at every login.
- A bare Features::twoFactorAuthentication() turns confirm on by default.