Jetstream 2FA ships fast, recovery is on you
Jetstream gives you TOTP-based 2FA via Fortify in minutes, but it leaves user recovery and secret management as an explicit engineering decision.
30 Jul 2026, 12:49 UTC

Jetstream 2FA ships fast, recovery is on you
You ship a Jetstream app, enable two‑factor authentication, and the first real support request is a user who lost their phone and can’t log in. That is the practical tension with Jetstream’s built‑in 2FA: it is fast to turn on via Laravel Fortify, and it pushes the recovery problem back to you.
What Jetstream actually wires up
Jetstream does not implement TOTP itself. It scaffolds the UI and delegates authentication logic to laravel/fortify. When you install Jetstream with Livewire or Inertia, Fortify adds the 2FA challenge to the login pipeline and stores an encrypted secret on the users table.
The relevant columns are two_factor_secret and two_factor_recovery_codes. The secret is encrypted at rest by Laravel’s app key. During login, after the password is verified, Fortify checks whether the user has 2FA enabled and, if so, presents a challenge for a time‑based one‑time password.
The user enable flow you inherit
With a fresh install, the profile page includes an Enable Two‑Factor Authentication button. Clicking it generates a QR code compatible with Google Authenticator, Authy, or similar TOTP apps, and stores recovery codes.
You can customize the challenge screen by overriding resources/views/auth/2fa-challenge.blade.php after publishing Jetstream views. If you publish views but do not publish Fortify’s configuration, changes to behavior can be overridden by vendor defaults on updates.
Turning the feature off cleanly
Disabling 2FA is a configuration decision, not a migration. It is done in Fortify’s feature list.
Run from the project root with write permission to the app directory:
php artisan vendor:publish --provider=\"Fortify\\ServiceProvider\"
Edit the config/fortify.php file and remove the entry for two‑factor authentication:
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
// Features::twoFactorAuthentication(),
],
Risk: removing the feature removes the challenge route and the profile UI for 2FA. Existing users keep their encrypted secrets in the database, but the application will no longer ask for codes. To re‑enable, re‑add Features::twoFactorAuthentication() to the array and clear config cache.
A practical way to check the result is to log in with a test account that previously had 2FA enabled. After the change, the 2FA challenge step should no longer appear. If you use the Inertia or Livewire stack, also verify the profile page no longer shows the enable/disable controls.
Where the safety net ends
Jetstream provides no self‑service recovery flow out of the box. If a user loses access to their authenticator app and has not saved recovery codes, recovery requires manual database intervention: clearing two_factor_secret and two_factor_recovery_codes for that user, which disables 2FA and forces re‑enrollment.
That is a deliberate trade‑off. You get a standards‑compliant TOTP flow with minimal code, but you must decide how to handle account recovery, support verification, and secure secret rotation before shipping to users.
Actionable closing: decide your recovery policy before enabling 2FA. Document how users should store recovery codes, and plan an admin‑only reset path. If you need self‑service recovery, build it explicitly; Jetstream will not provide it for you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.