{"id":562,"date":"2025-09-01T02:57:00","date_gmt":"2025-09-01T02:57:00","guid":{"rendered":"https:\/\/1v0.net\/blog\/?p=562"},"modified":"2025-09-01T02:57:04","modified_gmt":"2025-09-01T02:57:04","slug":"laravel-fortify-2fa-example-enable-challenge-recovery-codes-step-by-step","status":"publish","type":"post","link":"https:\/\/1v0.net\/blog\/laravel-fortify-2fa-example-enable-challenge-recovery-codes-step-by-step\/","title":{"rendered":"Laravel Fortify 2FA Example: Enable, Challenge, Recovery Codes (Step by Step)"},"content":{"rendered":"\n<p>Laravel Fortify provides a headless authentication backend, including built-in Two-Factor Authentication (2FA) with time-based one-time passwords (TOTP). In this guide, you\u2019ll install and configure Fortify, enable 2FA, build minimal Blade views for enabling\/disabling 2FA, display QR codes and recovery codes, handle the two-factor challenge at login, wire useful events, and test the flow end-to-end.<\/p>\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Install &amp; Register Laravel Fortify<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">composer require laravel\/fortify<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This installs Fortify into your Laravel app. Fortify exposes authentication routes and actions (login, logout, 2FA enable\/disable, challenges) without generating UI scaffolding.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">php artisan vendor:publish --provider=<span class=\"hljs-string\">\"Laravel\\Fortify\\FortifyServiceProvider\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Publishing copies the Fortify configuration file, migrations, and language lines to your project so you can customize them (including the 2FA-related columns).<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ config\/app.php (ensure provider is registered if not auto-discovered)<\/span>\n<span class=\"hljs-string\">'providers'<\/span> =&gt; &#91;\n    <span class=\"hljs-comment\">\/\/ ...<\/span>\n    App\\Providers\\FortifyServiceProvider::class,\n],<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Fortify is typically registered via your own <code>App\\Providers\\FortifyServiceProvider<\/code> so you can define views and behaviors. If you don\u2019t have it, create and register it as above.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"Bash\" data-shcb-language-slug=\"bash\"><span><code class=\"hljs language-bash\">php artisan migrate<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Bash<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">bash<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Run migrations to ensure 2FA columns exist on the users table. The published migration adds <code>two_factor_secret<\/code>, <code>two_factor_recovery_codes<\/code>, and timestamps needed for 2FA.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Enable Two-Factor Authentication in Fortify<\/strong><\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ config\/fortify.php<\/span>\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Laravel<\/span>\\<span class=\"hljs-title\">Fortify<\/span>\\<span class=\"hljs-title\">Features<\/span>;\n\n<span class=\"hljs-keyword\">return<\/span> &#91;\n    <span class=\"hljs-comment\">\/\/ ...<\/span>\n    <span class=\"hljs-string\">'features'<\/span> =&gt; &#91;\n        Features::registration(),\n        Features::resetPasswords(),\n        Features::emailVerification(),\n        Features::twoFactorAuthentication(&#91;\n            <span class=\"hljs-string\">'confirmPassword'<\/span> =&gt; <span class=\"hljs-keyword\">true<\/span>,\n        ]),\n    ],\n];<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Enabling <code>Features::twoFactorAuthentication()<\/code> activates Fortify\u2019s 2FA endpoints: enabling\/disabling 2FA, generating recovery codes, and challenging users during login when 2FA is active.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Providers\/FortifyServiceProvider.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Providers<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">ServiceProvider<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Laravel<\/span>\\<span class=\"hljs-title\">Fortify<\/span>\\<span class=\"hljs-title\">Fortify<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">FortifyServiceProvider<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">ServiceProvider<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">boot<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        <span class=\"hljs-comment\">\/\/ Point Fortify to your custom Blade views:<\/span>\n        Fortify::loginView(fn() =&gt; view(<span class=\"hljs-string\">'auth.login'<\/span>)); <span class=\"hljs-comment\">\/\/ your existing login<\/span>\n        Fortify::twoFactorChallengeView(fn() =&gt; view(<span class=\"hljs-string\">'auth.two-factor-challenge'<\/span>));\n        <span class=\"hljs-comment\">\/\/ You can set other views (register, reset, etc.) as needed.<\/span>\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Fortify is \u201cheadless\u201d, so you must provide the login and two-factor challenge views. We will create a minimal set of views next.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Profile UI: Enable \/ Disable 2FA + Show QR &amp; Recovery Codes<\/strong><\/h2>\n\n\n\n<p>Fortify exposes signed-in endpoints for enabling\/disabling 2FA and regenerating recovery codes. Here\u2019s a simple Blade \u201cProfile Security\u201d section to manage 2FA on the frontend.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/profile\/security.blade.php --&gt;\n@extends(<span class=\"hljs-string\">'layouts.app'<\/span>)\n\n@section(<span class=\"hljs-string\">'content'<\/span>)\n  &lt;h2&gt;Two-Factor Authentication&lt;\/h2&gt;\n\n  @<span class=\"hljs-keyword\">if<\/span> (! auth()-&gt;user()-&gt;two_factor_secret)\n    &lt;form method=<span class=\"hljs-string\">\"POST\"<\/span> action=<span class=\"hljs-string\">\"\/user\/two-factor-authentication\"<\/span>&gt;\n      @csrf\n      &lt;button type=<span class=\"hljs-string\">\"submit\"<\/span>&gt;Enable <span class=\"hljs-number\">2<\/span>FA&lt;\/button&gt;\n    &lt;\/form&gt;\n  @<span class=\"hljs-keyword\">else<\/span>\n    &lt;p&gt;<span class=\"hljs-number\">2<\/span>FA is enabled on your account.&lt;\/p&gt;\n\n    &lt;h3&gt;Scan this QR code in your authenticator app&lt;\/h3&gt;\n    {!! auth()-&gt;user()-&gt;twoFactorQrCodeSvg() !!}\n\n    &lt;h3 <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">mt<\/span>-3\"&gt;<span class=\"hljs-title\">Recovery<\/span> <span class=\"hljs-title\">Codes<\/span>&lt;\/<span class=\"hljs-title\">h3<\/span>&gt;\n    &lt;<span class=\"hljs-title\">ul<\/span>&gt;\n      @<span class=\"hljs-title\">foreach<\/span> (<span class=\"hljs-title\">auth<\/span>()-&gt;<span class=\"hljs-title\">user<\/span>()-&gt;<span class=\"hljs-title\">recoveryCodes<\/span>() <span class=\"hljs-title\">as<\/span> $<span class=\"hljs-title\">code<\/span>)\n        &lt;<span class=\"hljs-title\">li<\/span>&gt;&lt;<span class=\"hljs-title\">code<\/span>&gt;<\/span>{{ $code }}&lt;\/code&gt;&lt;\/li&gt;\n      @<span class=\"hljs-keyword\">endforeach<\/span>\n    &lt;\/ul&gt;\n\n    &lt;form method=<span class=\"hljs-string\">\"POST\"<\/span> action=<span class=\"hljs-string\">\"\/user\/two-factor-recovery-codes\"<\/span>&gt;\n      @csrf\n      &lt;button type=<span class=\"hljs-string\">\"submit\"<\/span>&gt;Regenerate Recovery Codes&lt;\/button&gt;\n    &lt;\/form&gt;\n\n    &lt;form method=<span class=\"hljs-string\">\"POST\"<\/span> action=<span class=\"hljs-string\">\"\/user\/two-factor-authentication\"<\/span>&gt;\n      @csrf\n      @method(<span class=\"hljs-string\">'DELETE'<\/span>)\n      &lt;button type=<span class=\"hljs-string\">\"submit\"<\/span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">mt<\/span>-3\"&gt;<span class=\"hljs-title\">Disable<\/span> 2<span class=\"hljs-title\">FA<\/span>&lt;\/<span class=\"hljs-title\">button<\/span>&gt;\n    &lt;\/<span class=\"hljs-title\">form<\/span>&gt;\n  @<span class=\"hljs-title\">endif<\/span>\n@<span class=\"hljs-title\">endsection<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>When 2FA is disabled, the form posts to <code>\/user\/two-factor-authentication<\/code> to enable it. Once enabled, users see a QR SVG (scan with Google Authenticator, 1Password, Authy, etc.) and recovery codes. They can regenerate codes or disable 2FA via the provided forms.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Two-Factor Challenge View (Login Step)<\/strong><\/h2>\n\n\n\n<p>After a successful password login for a user with 2FA enabled, Fortify redirects to a challenge page to enter the TOTP code or a recovery code. Create this Blade view and wire it in your <code>FortifyServiceProvider<\/code> as shown earlier.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;!-- resources\/views\/auth\/two-factor-challenge.blade.php --&gt;\n@extends(<span class=\"hljs-string\">'layouts.guest'<\/span>)\n\n@section(<span class=\"hljs-string\">'content'<\/span>)\n  &lt;h1&gt;Two-Factor Challenge&lt;\/h1&gt;\n\n  &lt;form method=<span class=\"hljs-string\">\"POST\"<\/span> action=<span class=\"hljs-string\">\"\/two-factor-challenge\"<\/span>&gt;\n    @csrf\n\n    &lt;div&gt;\n      &lt;label&gt;Authentication Code&lt;\/label&gt;\n      &lt;input type=<span class=\"hljs-string\">\"text\"<\/span> name=<span class=\"hljs-string\">\"code\"<\/span> inputmode=<span class=\"hljs-string\">\"numeric\"<\/span> autocomplete=<span class=\"hljs-string\">\"one-time-code\"<\/span>&gt;\n    &lt;\/div&gt;\n\n    &lt;p&gt;<span class=\"hljs-keyword\">Or<\/span> <span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">a<\/span> <span class=\"hljs-title\">recovery<\/span> <span class=\"hljs-title\">code<\/span>:&lt;\/<span class=\"hljs-title\">p<\/span>&gt;\n\n    &lt;<span class=\"hljs-title\">div<\/span>&gt;\n      &lt;<span class=\"hljs-title\">label<\/span>&gt;<span class=\"hljs-title\">Recovery<\/span> <span class=\"hljs-title\">Code<\/span>&lt;\/<span class=\"hljs-title\">label<\/span>&gt;\n      &lt;<span class=\"hljs-title\">input<\/span> <span class=\"hljs-title\">type<\/span>=\"<span class=\"hljs-title\">text<\/span>\" <span class=\"hljs-title\">name<\/span>=\"<span class=\"hljs-title\">recovery_code<\/span>\"&gt;\n    &lt;\/<span class=\"hljs-title\">div<\/span>&gt;\n\n    &lt;<span class=\"hljs-title\">button<\/span> <span class=\"hljs-title\">type<\/span>=\"<span class=\"hljs-title\">submit<\/span>\"&gt;<span class=\"hljs-title\">Verify<\/span>&lt;\/<span class=\"hljs-title\">button<\/span>&gt;\n\n    @<span class=\"hljs-title\">error<\/span>('<span class=\"hljs-title\">code<\/span>') &lt;<span class=\"hljs-title\">p<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">text<\/span>-<span class=\"hljs-title\">danger<\/span>\"&gt;{{ $<span class=\"hljs-title\">message<\/span> }}&lt;\/<span class=\"hljs-title\">p<\/span>&gt; @<span class=\"hljs-title\">enderror<\/span>\n    @<span class=\"hljs-title\">error<\/span>('<span class=\"hljs-title\">recovery_code<\/span>') &lt;<span class=\"hljs-title\">p<\/span> <span class=\"hljs-title\">class<\/span>=\"<span class=\"hljs-title\">text<\/span>-<span class=\"hljs-title\">danger<\/span>\"&gt;{{ $<span class=\"hljs-title\">message<\/span> }}&lt;\/<span class=\"hljs-title\">p<\/span>&gt; @<span class=\"hljs-title\">enderror<\/span>\n  &lt;\/<span class=\"hljs-title\">form<\/span>&gt;\n@<span class=\"hljs-title\">endsection<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Posting to <code>\/two-factor-challenge<\/code> tells Fortify to validate either the 6-digit code from the authenticator app or a recovery code, completing the login flow.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Useful Events: Email Users When 2FA Changes<\/strong><\/h2>\n\n\n\n<p>Fortify fires events when users enable\/disable 2FA or regenerate recovery codes. You can listen to these and notify users for security awareness.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Providers\/EventServiceProvider.php<\/span>\n<span class=\"hljs-keyword\">protected<\/span> $listen = &#91;\n    \\Laravel\\Fortify\\Events\\TwoFactorAuthenticationEnabled::class =&gt; &#91;\n        \\App\\Listeners\\SendTwoFactorEnabledNotification::class,\n    ],\n    \\Laravel\\Fortify\\Events\\TwoFactorAuthenticationDisabled::class =&gt; &#91;\n        \\App\\Listeners\\SendTwoFactorDisabledNotification::class,\n    ],\n    \\Laravel\\Fortify\\Events\\RecoveryCodesGenerated::class =&gt; &#91;\n        \\App\\Listeners\\SendRecoveryCodesRegeneratedNotification::class,\n    ],\n];<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Registering listeners lets you send mail, Slack\/Log notifications, or audit events whenever 2FA settings change.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ app\/Listeners\/SendTwoFactorEnabledNotification.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Listeners<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Support<\/span>\\<span class=\"hljs-title\">Facades<\/span>\\<span class=\"hljs-title\">Mail<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Laravel<\/span>\\<span class=\"hljs-title\">Fortify<\/span>\\<span class=\"hljs-title\">Events<\/span>\\<span class=\"hljs-title\">TwoFactorAuthenticationEnabled<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">SendTwoFactorEnabledNotification<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">handle<\/span><span class=\"hljs-params\">(TwoFactorAuthenticationEnabled $event)<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        $user = $event-&gt;user;\n        Mail::raw(<span class=\"hljs-string\">'Two-Factor Authentication was enabled on your account.'<\/span>, <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">($m)<\/span> <span class=\"hljs-title\">use<\/span> <span class=\"hljs-params\">($user)<\/span> <\/span>{\n            $m-&gt;to($user-&gt;email)-&gt;subject(<span class=\"hljs-string\">'2FA Enabled'<\/span>);\n        });\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This simple listener sends an email whenever a user enables 2FA. You can create similar listeners for disabled and regenerated codes to keep users informed.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Controller Integration: Protect Critical Actions with Password\/2FA<\/strong><\/h2>\n\n\n\n<p>Even with 2FA enabled, you might want to require recent password confirmation (and therefore 2FA at login) before sensitive actions (like deleting an account). Fortify ships a password confirmation route you can require via middleware.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ routes\/web.php<\/span>\nRoute::middleware(&#91;<span class=\"hljs-string\">'auth'<\/span>, <span class=\"hljs-string\">'password.confirm'<\/span>])-&gt;group(<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-params\">()<\/span> <\/span>{\n    Route::delete(<span class=\"hljs-string\">'\/account'<\/span>, &#91;\\App\\Http\\Controllers\\AccountController::class, <span class=\"hljs-string\">'destroy'<\/span>])\n        -&gt;name(<span class=\"hljs-string\">'account.destroy'<\/span>);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Using <code>password.confirm<\/code> ensures the user recently re-entered their password (and has passed 2FA on login). You can also build a custom flow to ask for a fresh TOTP if you prefer a second check before a destructive action.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Feature Test: Happy Path for 2FA Challenge<\/strong><\/h2>\n\n\n\n<p>This example shows how to simulate a user with 2FA enabled and verify that the two-factor challenge gate works. In practice, you can stub the verification logic or seed a valid TOTP using a known secret.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\"><span class=\"hljs-comment\">\/\/ tests\/Feature\/TwoFactorLoginTest.php<\/span>\n<span class=\"hljs-keyword\">namespace<\/span> <span class=\"hljs-title\">Tests<\/span>\\<span class=\"hljs-title\">Feature<\/span>;\n\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Tests<\/span>\\<span class=\"hljs-title\">TestCase<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">App<\/span>\\<span class=\"hljs-title\">Models<\/span>\\<span class=\"hljs-title\">User<\/span>;\n<span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">Illuminate<\/span>\\<span class=\"hljs-title\">Foundation<\/span>\\<span class=\"hljs-title\">Testing<\/span>\\<span class=\"hljs-title\">RefreshDatabase<\/span>;\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">TwoFactorLoginTest<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">TestCase<\/span>\n<\/span>{\n    <span class=\"hljs-keyword\">use<\/span> <span class=\"hljs-title\">RefreshDatabase<\/span>;\n\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">test_user_with_2fa_is_redirected_to_challenge<\/span><span class=\"hljs-params\">()<\/span>: <span class=\"hljs-title\">void<\/span>\n    <\/span>{\n        $user = User::factory()-&gt;create(&#91;\n            <span class=\"hljs-comment\">\/\/ Pretend 2FA is enabled by seeding secret\/recovery fields:<\/span>\n            <span class=\"hljs-string\">'two_factor_secret'<\/span> =&gt; encrypt(<span class=\"hljs-string\">'TESTSECRET'<\/span>),\n            <span class=\"hljs-string\">'two_factor_recovery_codes'<\/span> =&gt; encrypt(json_encode(&#91;<span class=\"hljs-string\">'recovery-code-1'<\/span>])),\n        ]);\n\n        <span class=\"hljs-comment\">\/\/ First step: password login (simulate posting valid credentials)<\/span>\n        $response = <span class=\"hljs-keyword\">$this<\/span>-&gt;post(<span class=\"hljs-string\">'\/login'<\/span>, &#91;\n            <span class=\"hljs-string\">'email'<\/span> =&gt; $user-&gt;email,\n            <span class=\"hljs-string\">'password'<\/span> =&gt; <span class=\"hljs-string\">'password'<\/span>, <span class=\"hljs-comment\">\/\/ matches default factory<\/span>\n        ]);\n\n        $response-&gt;assertRedirect(<span class=\"hljs-string\">'\/two-factor-challenge'<\/span>);\n\n        <span class=\"hljs-comment\">\/\/ Second step: submit a recovery code (bypassing TOTP for test)<\/span>\n        $challenge = <span class=\"hljs-keyword\">$this<\/span>-&gt;post(<span class=\"hljs-string\">'\/two-factor-challenge'<\/span>, &#91;\n            <span class=\"hljs-string\">'recovery_code'<\/span> =&gt; <span class=\"hljs-string\">'recovery-code-1'<\/span>,\n        ]);\n\n        $challenge-&gt;assertRedirect(<span class=\"hljs-string\">'\/home'<\/span>); <span class=\"hljs-comment\">\/\/ or your intended location<\/span>\n        <span class=\"hljs-keyword\">$this<\/span>-&gt;assertAuthenticatedAs($user);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The test verifies that a user with 2FA enabled is redirected to the challenge after password login, and that providing a valid recovery code authenticates them fully.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Troubleshooting &amp; Notes<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>QR not showing?<\/strong> Ensure you\u2019ve run the vendor publish &amp; migrations, and that your user has a generated secret after enabling 2FA. The <code>twoFactorQrCodeSvg()<\/code> helper renders only when 2FA is enabled.<\/li>\n<li><strong>Time drift errors?<\/strong> TOTP is time-based: make sure your server clock is accurate (use NTP) so codes match authenticator apps.<\/li>\n<li><strong>Lost device?<\/strong> Users can sign in with a recovery code and immediately regenerate new recovery codes from the profile screen.<\/li>\n<li><strong>Security hardening:<\/strong> Consider emailing users on 2FA changes (examples above) and auditing those events.<\/li>\n<\/ul>\n\n\n\n<p>These tips help ensure a smooth 2FA experience and keep your app\u2019s authentication flow secure and user-friendly.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Wrapping Up<\/strong><\/h2>\n\n\n\n<p>With Laravel Fortify, adding 2FA is straightforward: enable the feature, provide minimal UI for enabling\/disabling and challenges, and wire event listeners for better security hygiene. The built-in helpers for QR codes and recovery codes make UX smooth, while middleware like <code>password.confirm<\/code> protects sensitive operations. You now have a production-ready baseline for strong, user-friendly 2FA in Laravel.<\/p>\n\n\n\n\n<div class=\"wp-block-spacer\" style=\"height:100px\" aria-hidden=\"true\"><\/div>\n\n\n\n\n<h2 class=\"wp-block-heading\"><strong>What\u2019s Next<\/strong><\/h2>\n\n\n\n<p>Keep strengthening your auth stack with these related guides:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n  <li><a href=\"\/blog\/implementing-two-factor-authentication-in-laravel\">Implementing Two-Factor Authentication in Laravel<\/a><\/li>\n  <li><a href=\"\/blog\/how-to-build-email-verification-in-laravel-12-step-by-step\">How to Build Email Verification in Laravel 12 (Step by Step)<\/a><\/li>\n  <li><a href=\"\/blog\/implementing-password-reset-in-laravel-12-without-packages\">Implementing Password Reset in Laravel 12 Without Packages<\/a><\/li>\n<\/ul>\n\n","protected":false},"excerpt":{"rendered":"<p>Laravel Fortify provides a headless authentication backend, including built-in Two-Factor Authentication (2FA) with time-based one-time passwords (TOTP). In this guide, you\u2019ll install and configure Fortify, enable 2FA, build minimal Blade views for enabling\/disabling 2FA, display QR codes and recovery codes, handle the two-factor challenge at login, wire useful events, and test the flow end-to-end. Install [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":566,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[11,12,46,13,22],"class_list":["post-562","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-laravel","tag-2fa","tag-authentication","tag-events","tag-login","tag-security"],"_links":{"self":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/562","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/comments?post=562"}],"version-history":[{"count":1,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/562\/revisions"}],"predecessor-version":[{"id":565,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/posts\/562\/revisions\/565"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media\/566"}],"wp:attachment":[{"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/media?parent=562"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/categories?post=562"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/1v0.net\/blog\/wp-json\/wp\/v2\/tags?post=562"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}