Two-factor authentication (2FA) is one of the best ways to protect your user accounts from hackers. In this tutorial, we will explain the most important things you need to know about implementing 2FA in Laravel framework.
How 2FA Generally Works?
Two-factor authentication is simple:
Step 1. After you enter your credentials, the system sends you a code. This code usually arrives as a text message to your phone, through an authenticator app, or via email.
Step 2. You then type this code into the website. If the code matches what the system sent, you're logged in.
It's like having two keys to your house instead of one - even if someone steals your password, they still can't get in without access to your phone or email to receive that second code.
Available 2FA Drivers
There are several ways to deliver that second authentication factor:
- Email and SMS - The most popular options since everyone has an email address and phone number. Simple to set up and use.
- Authenticator Apps - Google Authenticator, Authy, or Microsoft Authenticator generate codes every 30 seconds without needing internet. More secure than SMS.
- Physical Keys - USB devices you plug into your computer for the highest security level.
- Biometric Options - Fingerprint scanners and similar methods that use your body as the key.
There are other methods available too, but these cover most use cases. Each method has trade-offs between convenience and security, so choose what works best for your users.
Official 2FA Options in Laravel 12
In the official Laravel 12 React/Vue/Livewire starter kits, currently there are no two-factor auth functions. But you still have options.
Option 1. Laravel JetStream. You can use Laravel Jetstream, which still works with Laravel 12 - here's our article about it. It comes with ready-made pages and forms that look decent out of the box and uses Fortify under the hood.
Option 2. Laravel Fortify. Or you can use Fortify package directly, which handles all the behind-the-scenes logic but leaves you to build your own pages.
Both work well for basic setups, but if you need to customize how things work, you might find yourself fighting against them rather than working with them. Especially since both are not "advertised" officially that much, as they were released long time ago, in Laravel 8 version.
Building 2FA From Scratch
Another option is to build two-factor logic yourself, it's not that hard.
We have built a similar example using SMS instead of an email to authenticate a user.
When user registers a notification is sent with the code:
use App\Notifications\SendOtpNotification; class RegisteredUserController extends Controller{ // ... public function store(Request $request): RedirectResponse { $request->validate([ 'name' => ['required', 'string', 'max:255'], 'phone' => ['required', 'string', 'max:20', 'unique:' . User::class, new PhoneNumber], 'password' => ['required', 'confirmed', Password::defaults()], ]); $user = User::create([ 'name' => $request->name, 'phone' => $request->phone, 'password' => Hash::make($request->password), ]); // Generate and send OTP via Vonage $otp = rand(100000, 999999); $user->update([ 'otp_code' => $otp, 'otp_expires_at' => now()->addMinutes(10), ]); $user->notify(new SendOtpNotification($otp)); Auth::login($user); return redirect(URL::signedRoute('verification.notice')); }}
Notification channel in this example is vonage
and only sends the OTP code to the user:
use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Notifications\Notification;use Illuminate\Notifications\Messages\VonageMessage; class SendOtpNotification extends Notification implements ShouldQueue{ use Queueable; public function __construct(readonly private string $otp) {} public function via($notifiable): array { return ['vonage']; } public function toVonage($notifiable): VonageMessage { return (new VonageMessage()) ->content('Your OTP code is: ' . $this->otp); }}
When user enters the code it is checked if code is correct and code isn't expired. If everything is correct use can go the application.
use Illuminate\Http\RedirectResponse; class PhoneVerificationController extends Controller{ public function verify(Request $request): RedirectResponse { $request->validate([ 'otp_code' => ['required', 'numeric'], ]); $user = $request->user(); if (!$user || $user->otp_code !== $request->otp_code || !$user->otp_expires_at || $user->otp_expires_at->isPast()) { return back()->withErrors(['otp_code' => __('Invalid or expired OTP.')]); } $user->otp_code = null; $user->otp_expires_at = null; $user->verified_at = now(); $user->save(); return redirect()->intended(route('dashboard', absolute: false)); }}
Full project can be found here.
Laravel Packages for 2FA
If you don't want to build 2FA from scratch, there are third-party packages available.
Package 1. pragmarx/google2fa
The most popular is pragmarx/google2fa
, which focuses on Google Authenticator-style codes and is actually what Filament uses for its admin panel authentication.
You can also use the antonioribeiro/google2fa-laravel
, a Laravel wrapper for pragmarx/google2fa
.
You install the package via composer:
composer require pragmarx/google2fa-laravel
From here, you can generate a secret code:
use Google2FA; return Google2FA::generateSecretKey();
The package also gives a nice Middleware to project your routes.
bootstrap/app.php:
return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ '2fa' => \PragmaRX\Google2FALaravel\Middleware::class, ]); }) ->withExceptions(function (Exceptions $exceptions) { // })->create();
routes/web.php:
Route::get('/admin', function () { return view('admin.index');})->middleware(['auth', '2fa']);
Package 2. Laragear/TwoFactor
Another solid option is Laragear/TwoFactor
, which gives you a more flexible approach to handling different types of 2FA.
The package is installed via composer:
composer require laragear/two-factorphp artisan two-factor:install
Then you run the published migrations and add the TwoFactorAuthenticatable
contract to the User model.
app/Models/User.php:
use Illuminate\Foundation\Auth\User as Authenticatable;use Laragear\TwoFactor\TwoFactorAuthentication;use Laragear\TwoFactor\Contracts\TwoFactorAuthenticatable; class User extends Authenticatable implements TwoFactorAuthenticatable{ use TwoFactorAuthentication; // ...}
Now you can generate codes in your Controllers:
use Illuminate\Http\Request; public function prepareTwoFactor(Request $request){ $secret = $request->user()->createTwoFactorAuth(); return view('user.2fa', [ 'qr_code' => $secret->toQr(), // As QR Code 'uri' => $secret->toUri(), // As "otpauth://" URI. 'string' => $secret->toString(), // As a string ]);}
And confirm the code the user enters:
use Illuminate\Http\Request; public function confirmTwoFactor(Request $request){ $request->validate([ 'code' => 'required|numeric' ]); $activated = $request->user()->confirmTwoFactorAuth($request->code); // ...}
Package 3. WorkOS (partially free)
When talking about Laravel 12 starter kits, I missed (intentionally) one option for 2FA.
When creating a new Laravel application, you may choose WorkOS as an authentication provider, instead of the default Laravel Auth.
Their free plan covers up to 1 million users, which probably means their main target is enterprise-grade companies.
However, with that, you trust this 3rd-party solution with full Auth process that includes two-factor feature.
To be honest, I haven't seen many (any?) people in my Laravel circles who choose WorkOS for their projects, although Taylor Otwell himself thought it would be an easily-installable option.
What Will You Choose?
So, these are the options. What 2FA mechanism/tools do YOU use in your projects? Let's discuss in the comments below.
Hello, if it can be useful to anyone, another possible solution would be to use the package Laravel-one-time-passwords released by Spatie in May 2025.
Maybe this package has been ignored because it is very recent.
We have a video on this:
https://www.youtube.com/watch?v=GBzHePtj1lw