Add Two-Factor Authentication to Laravel with Fortify
Two-factor authentication (2FA) is one of the highest-impact security features you can add to an app, and Laravel Fortify gives you a backend implementation for free. Fortify is a headless authentication backend: it registers the routes and logic but leaves the views to you, which makes it a great fit whether you use Blade, Livewire, or a JavaScript frontend. This tutorial adds time-based one-time password (TOTP) 2FA, the kind you scan into Google Authenticator or 1Password, to an app for the fictional PHP Architect portal.
Step 1: Install Fortify
composer require laravel/fortify
php artisan fortify:install
php artisan migrate
The install command publishes Fortify's configuration and service provider. The migration adds the columns Fortify needs on your users table: two_factor_secret, two_factor_recovery_codes, and two_factor_confirmed_at.
Step 2: Enable the Feature
Open config/fortify.php and make sure the two-factor feature is enabled. Requiring password confirmation and a confirmation step is strongly recommended.
use Laravel\Fortify\Features;
'features' => [
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
]),
],
The confirm option means a freshly generated secret is not considered active until the user proves they can produce a valid code, which prevents users from locking themselves out with a misconfigured authenticator app.
Step 3: Add the Trait to Your User Model
Fortify's two-factor logic lives in a trait. Add it to App\Models\User:
use Laravel\Fortify\TwoFactorAuthenticatable;
class User extends Authenticatable
{
use TwoFactorAuthenticatable;
}
Step 4: The Routes Fortify Gives You
With the feature enabled, Fortify registers the endpoints your UI will call. You do not write these controllers, you just call them from your settings page:
POST /user/two-factor-authenticationenables 2FA and generates the secret.POST /user/confirmed-two-factor-authenticationconfirms it with a code from the authenticator app.GET /user/two-factor-qr-codereturns the QR code SVG to display.GET /user/two-factor-recovery-codesreturns the recovery codes.DELETE /user/two-factor-authenticationdisables 2FA.
Step 5: Build the Enable Flow
On your security settings page, a button posts to the enable route. Because we required password confirmation, the user will be prompted for their password first. After enabling, show the QR code and the recovery codes.
<form method="POST" action="/user/two-factor-authentication">
@csrf
<button type="submit">Enable Two-Factor Authentication</button>
</form>
@if (auth()->user()->two_factor_secret)
<div class="qr">
{!! auth()->user()->twoFactorQrCodeSvg() !!}
</div>
<p>Scan the code above, then enter a generated code to confirm:</p>
<form method="POST" action="/user/confirmed-two-factor-authentication">
@csrf
<input name="code" inputmode="numeric" autocomplete="one-time-code">
<button type="submit">Confirm</button>
</form>
@endif
Always show the recovery codes at least once and tell users to store them safely. They are the only way back in if the device with the authenticator app is lost.
<ul>
@foreach (json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true) as $code)
<li>{{ $code }}</li>
@endforeach
</ul>
Step 6: The Login Challenge
Once 2FA is confirmed, Fortify automatically intercepts the login. After a valid email and password, instead of logging the user straight in, it redirects to a two-factor challenge and stores the pending login in the session. You provide a view at the route Fortify redirects to, with a form that posts the code to /two-factor-challenge:
<form method="POST" action="/two-factor-challenge">
@csrf
<label>Authentication Code</label>
<input name="code" inputmode="numeric" autocomplete="one-time-code">
<label>Or use a recovery code</label>
<input name="recovery_code">
<button type="submit">Log in</button>
</form>
Fortify accepts either the six-digit TOTP code or one of the recovery codes, and each recovery code can only be used once.
Wrapping Up
That is the whole flow: install Fortify, enable the feature, add the trait, and build a few small views around the routes it provides. The cryptography, code generation, recovery codes, and login interception are all handled for you. If you would rather not build the views by hand, the Laravel starter kits ship with these screens already implemented on top of Fortify, which is a good reference even if you customize them later.
Further Reading
- Laravel Fortify documentation, "Two-Factor Authentication" (laravel.com/docs/13.x/fortify#two-factor-authentication)
- Laravel documentation, "Authentication" (laravel.com/docs/13.x/authentication)