Search by

ongudidan / laravel-multi-2fa

ongudidan

Unified Multi-Method Two-Factor Authentication for Laravel: Email OTP, Authenticator App (TOTP via Fortify), and Passkeys (WebAuthn).

Package info

github.com/ongudidan/laravel-multi-2FA

pkg:composer/ongudidan/laravel-multi-2fa

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.3 2026-10-14 04:59 UTC

This package is auto-updated.

Last update: 2026-10-14 07:09:14 UTC


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:

  1. Passkeys (WebAuthn / FIDO2): Fast, passwordless biometric authentication (Touch ID, Face ID, YubiKey, Windows Hello).
  2. Authenticator App (TOTP): Industry-standard 6-digit codes generated by Google Authenticator, Authy, Microsoft Authenticator, 1Password, etc.
  3. Email OTP: Single-use, timed 6-digit verification codes sent directly to the user's registered email address.
  4. 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/json is requested.

Requirements

  • PHP ^8.2
  • Laravel Framework ^11.0 or ^12.0
  • laravel/fortify ^1.20 or ^1.30
  • laravel/passkeys ^0.1 or ^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

  1. Publish the configuration:
php artisan vendor:publish --tag=multi-2fa-config
  1. 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/)

  1. 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

  1. User enters email & password on /login.
  2. RedirectIfEmailOtpRequired validates credentials and queries TwoFactorService::getAvailableMethodsForUser($user).
  3. Smart Routing:
    • Multiple methods available (e.g. user has Passkey + Email OTP, or Authenticator + Email OTP):
      • User is redirected to /login-challenge-selection to pick their preferred method.
    • Only Email OTP available:
      • Code is generated, emailed, and user is redirected directly to /email-otp-login.
    • Only Authenticator App active:
      • Pipeline falls through to RedirectIfTwoFactorAuthenticatable which redirects to /two-factor-challenge.
    • No 2FA configured for this user:
      • Normal login completes instantly without prompts.
  4. 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.