ongudidan / laravel-multi-2fa
Unified Multi-Method Two-Factor Authentication for Laravel: Email OTP, Authenticator App (TOTP via Fortify), and Passkeys (WebAuthn).
Requires
- php: ^8.2
- illuminate/contracts: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/mail: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/session: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- laravel/fortify: ^1.20|^1.30|^1.40|^1.41
- laravel/passkeys: ^0.1|^0.2
Requires (Dev)
None
Suggests
- inertiajs/inertia-laravel: Required when rendering through the Inertia.js (Vue/React) frontend stack (^1.0|^2.0).
Provides
None
Conflicts
None
Replaces
None
README
A complete, production-ready Two-Factor Authentication (2FA) package for Laravel applications supporting Inertia.js (Vue/React), Blade (Livewire/Alpine/HTML), and Headless JSON APIs.
This package provides a unified layer handling all three modern 2FA channels:
- Passkeys (WebAuthn / FIDO2): Fast, passwordless biometric authentication (Touch ID, Face ID, YubiKey, Windows Hello).
- Authenticator App (TOTP): Industry-standard 6-digit codes generated by Google Authenticator, Authy, Microsoft Authenticator, 1Password, etc.
- Email OTP: Single-use, timed 6-digit verification codes sent directly to the user's registered email address.
- Smart Login Challenge Selection: Automatically detects active methods per user and prompts them with a verification method selection screen when multiple channels are available.
🎨 Supported Frontend Stacks
- Inertia.js (Vue 3 / React): Full UI components with reactive state, pin inputs, and modal management.
- Blade (Vanilla / Alpine.js / Livewire): Built-in responsive HTML & Tailwind views out-of-the-box, with embedded vanilla JavaScript for WebAuthn passkeys (no npm build required!).
- Headless API (Mobile / SPAs): Automatic JSON responses when
Accept: application/jsonis requested.
Requirements
- PHP
^8.2 - Laravel Framework
^11.0or^12.0 laravel/fortify^1.20or^1.30laravel/passkeys^0.1or^0.2
🤖 AI Agent Implementation Prompt
When integrating this package into any new or existing Laravel project (Inertia or Blade), copy and paste this concise prompt directly into your AI coding assistant (Antigravity, Cursor, Claude Code, Copilot, or ChatGPT):
Please integrate the `ongudidan/laravel-multi-2fa` package into this Laravel application by executing the step-by-step instructions in this README file:
1. Detect whether this project uses Inertia.js (Vue/React) or Blade, then execute Step 1 (Install Package & Dependencies) and Step 2 (Publish Assets & Run Migrations).
2. Update the User model following Step 3 (Configure User Model) in app/Models/User.php.
3. Configure the Fortify pipeline and challenge view following Step 4 (Configure Fortify Pipeline) in app/Providers/FortifyServiceProvider.php.
4. If using Inertia, share security settings following Step 5 (Share Security Settings in Inertia) in app/Http/Middleware/HandleInertiaRequests.php.
5. (Optional) Register the route protection middleware following Step 6 (Route Protection Middleware) in bootstrap/app.php.
6. Verify that routes /login-challenge-selection, /email-otp-login, /settings/two-factor, and /settings/passkeys are functional.
Setup & Integration Guide
Step 1: Install Package & Dependencies
If developing locally or using a path repository, register packages/ongudidan/laravel-multi-2fa in your composer.json repositories first:
"repositories": [ { "type": "path", "url": "packages/ongudidan/laravel-multi-2fa", "options": { "symlink": true } } ]
Install the package via Composer:
composer require ongudidan/laravel-multi-2fa laravel/fortify laravel/passkeys
If using Inertia.js (Vue/React):
npm install @laravel/passkeys
(Blade projects do not require @laravel/passkeys because built-in vanilla WebAuthn scripts are included).
Step 2: Publish Assets & Run Migrations
- Publish the configuration:
php artisan vendor:publish --tag=multi-2fa-config
- Publish UI views based on your stack:
-
For Inertia.js (Vue 3):
php artisan vendor:publish --tag=multi-2fa-vue
(Publishes
EmailOtpLogin.vue,LoginChallengeSelection.vue,TwoFactor.vue,Passkeys.vue) -
For Blade:
php artisan vendor:publish --tag=multi-2fa-blade
(Optional: Blade views work out-of-the-box without publishing, but publishing allows you to customize the templates in
resources/views/vendor/multi-2fa/)
- Run migrations:
php artisan migrate
Step 3: Configure User Model
In app/Models/User.php, implement PasskeyUser and attach the 2FA traits:
namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Passkeys\Contracts\PasskeyUser; use Laravel\Passkeys\PasskeyAuthenticatable; class User extends Authenticatable implements PasskeyUser { use TwoFactorAuthenticatable, PasskeyAuthenticatable; protected $hidden = [ 'password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes', ]; }
Step 4: Configure Fortify Pipeline
In app/Providers/FortifyServiceProvider.php, plug the package's pipeline actions into Fortify::authenticateThrough:
use Laravel\Fortify\Fortify; use Laravel\Fortify\Features; use Illuminate\Http\Request; use Ongudidan\Multi2fa\Actions\RedirectIfEmailOtpRequired; use Ongudidan\Multi2fa\Actions\RedirectIfTwoFactorAuthenticatable; use Ongudidan\Multi2fa\Services\TwoFactorService; public function boot(): void { Fortify::authenticateThrough(function (Request $request) { return [ config('fortify.limiters.login') ? null : \Laravel\Fortify\Actions\EnsureLoginIsNotThrottled::class, config('fortify.lowercase_usernames') ? \Laravel\Fortify\Actions\CanonicalizeUsername::class : null, RedirectIfEmailOtpRequired::class, RedirectIfTwoFactorAuthenticatable::class, \Laravel\Fortify\Actions\AttemptToAuthenticate::class, \Laravel\Fortify\Actions\PrepareAuthenticatedSession::class, ]; }); // Configure the challenge view to support switching between methods: Fortify::twoFactorChallengeView(function (Request $request) { $userId = $request->session()->get('login.id'); $canSwitch = false; if ($userId) { $twoFactorService = app(TwoFactorService::class); $userModel = config('auth.providers.users.model', \App\Models\User::class); $user = $userModel::find($userId); if ($user) { $canSwitch = $twoFactorService->canSwitchMethods($user, 'authenticator'); } } // Uses renderView to automatically support Inertia or Blade: return app(TwoFactorService::class)->renderView($request, 'two_factor_challenge', [ 'canSwitch' => $canSwitch, ]); }); }
Step 5: Share Security Settings in Inertia (Inertia Projects Only)
In app/Http/Middleware/HandleInertiaRequests.php, share the security settings with your frontend components:
use Ongudidan\Multi2fa\Services\TwoFactorService; public function share(Request $request): array { return [ ...parent::share($request), 'security_settings' => app(TwoFactorService::class)->getSecuritySettings(), ]; }
Step 6: Route Protection Middleware (Optional)
To prevent users from navigating to two-factor configuration routes when globally disabled in system settings:
In your routing bootstrap (bootstrap/app.php in Laravel 11/12):
use Ongudidan\Multi2fa\Http\Middleware\EnsureTwoFactorEnabledGlobally; ->withMiddleware(function (Middleware $middleware) { $middleware->append(EnsureTwoFactorEnabledGlobally::class); })
Configuration (config/multi-2fa.php)
return [ // Frontend stack: 'auto', 'inertia', 'blade', or 'api' 'stack' => 'auto', // Static feature toggles or runtime fallback 'features' => [ 'email_otp' => true, 'authenticator' => true, 'passkey' => true, ], // Custom resolver to read dynamic DB settings (e.g. system settings table) 'settings_resolver' => \Ongudidan\Multi2fa\Services\DefaultSettingsResolver::class, // Expiry in minutes for email OTP 'otp_expiry_minutes' => 10, // Built-in routes 'routes' => [ 'enabled' => true, 'middleware' => ['web'], ], // View component mappings for each stack 'views' => [ 'inertia' => [ 'email_otp' => 'auth/EmailOtpLogin', 'login_challenge' => 'auth/LoginChallengeSelection', 'two_factor_settings' => 'settings/TwoFactor', 'passkeys_settings' => 'settings/Passkeys', ], 'blade' => [ 'email_otp' => 'multi-2fa::auth.email-otp', 'login_challenge' => 'multi-2fa::auth.challenge-selection', 'two_factor_settings' => 'multi-2fa::settings.two-factor', 'passkeys_settings' => 'multi-2fa::settings.passkeys', ], ], // Redirect after login 'home' => null, // defaults to config('fortify.home') or /dashboard ];
How It Works at Runtime
- User enters email & password on
/login. RedirectIfEmailOtpRequiredvalidates credentials and queriesTwoFactorService::getAvailableMethodsForUser($user).- Smart Routing:
- Multiple methods available (e.g. user has Passkey + Email OTP, or Authenticator + Email OTP):
- User is redirected to
/login-challenge-selectionto pick their preferred method.
- User is redirected to
- Only Email OTP available:
- Code is generated, emailed, and user is redirected directly to
/email-otp-login.
- Code is generated, emailed, and user is redirected directly to
- Only Authenticator App active:
- Pipeline falls through to
RedirectIfTwoFactorAuthenticatablewhich redirects to/two-factor-challenge.
- Pipeline falls through to
- No 2FA configured for this user:
- Normal login completes instantly without prompts.
- Multiple methods available (e.g. user has Passkey + Email OTP, or Authenticator + Email OTP):
- Method Switching:
- While on the Email OTP or Authenticator screens, if the user has alternate methods, a "← Choose a different verification method" option is shown allowing seamless switching.
License
The MIT License (MIT). Developed by Ongudidan.