Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Tfa Confirmation Laravel Package

wnx/laravel-tfa-confirmation

Adds two-factor authentication confirmation to Laravel apps. Require users to re-enter a TFA code before sensitive actions via middleware and routes, with Fortify support and customizable views.

View on GitHub
Deep Wiki
Context7

Getting Started

This package adds step-up authentication to session-based Laravel apps: before a user can hit a protected route, they must re-enter their TOTP code from their authenticator app (the same secret used by Fortify/Jetstream two-factor). It is not a login package — it assumes the user is already authenticated and already has 2FA enabled.

Prerequisites (check these before anything else):

  1. Laravel Fortify (or Jetstream) two-factor authentication is installed and working — the users table has two_factor_secret and two_factor_confirmed_at, and your User model uses Fortify's TwoFactorAuthenticatable trait.
  2. A real session driver (web middleware group). This package stores the "confirmed" flag in the session, so it does nothing useful for stateless Sanctum/API tokens.

Install and publish:

composer require wnx/laravel-tfa-confirmation
php artisan vendor:publish --provider="Wnx\TfaConfirmation\TfaConfirmationServiceProvider"

Publishing gives you config/tfa-confirmation.php (confirmation lifetime, redirect target, view name) and the Blade confirmation screen. Read the config file first — it is the smallest file in the package and tells you the whole flow.

Register the middleware alias (Laravel 11/12 style):

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'tfa-confirmation' => \Wnx\TfaConfirmation\Http\Middleware\RequiresTwoFactorConfirmation::class,
    ]);
})

Or in app/Http/Kernel.php::$middlewareAliases on Laravel 10.

First use case — protect a destructive settings page:

Route::middleware(['auth', 'tfa-confirmation'])->group(function () {
    Route::get('/settings/api-tokens', [ApiTokenController::class, 'index']);
    Route::delete('/settings/api-tokens/{token}', [ApiTokenController::class, 'destroy']);
    Route::delete('/account', [AccountController::class, 'destroy']);
});

The first request without a confirmation redirects to the package's confirmation screen, the user types their 6-digit code, and is bounced back to the originally requested URL. For the next N minutes (config duration, commonly a few hours) the session is considered confirmed.

Verify the wiring in 30 seconds:

php artisan route:list | grep -i tfa        # the confirmation route exists
php artisan tinker --execute="dump(config('tfa-confirmation'));"

Then hit a protected URL in the browser with a user that has 2FA enabled and watch the redirect chain — that is your fastest sanity check.

Implementation Patterns

Route group, not per-action. Confirm once, act many times. Wrap the whole sensitive area (billing, API tokens, team member management, account deletion) in one tfa-confirmation group so the user is not re-prompted on every click within the confirmation window:

Route::middleware(['auth', 'tfa-confirmation'])->prefix('settings')->group(function () {
    Route::get('/security', ...);
    Route::put('/security/password', ...);
});

Two-tier sensitivity. If some actions must always require a fresh code (e.g. "delete workspace"), don't put them in the long-lived group. Instead keep them in a separate group and shorten the lifetime for that route by overriding the config at boot, or check freshness manually in the controller:

// Controller-level escalation for the rare "always confirm" action
abort_unless(TwoFactorConfirmation::isConfirmedWithin(now()->subMinutes(5)), 423);

Render the right UI in Blade. Use the confirmation state to decide whether to show a normal button or an "unlock to continue" prompt, instead of letting every click bounce through a redirect:

@if (tfa_confirmation_confirmed())
    <button type="submit">Delete API token</button>
@else
    <a href="{{ route('tfa-confirmation.confirm') }}">Confirm your 2FA code to continue</a>
@endif

Inertia / Livewire / SPA frontends. The middleware returns a redirect to the confirmation page for normal requests. In Inertia this surfaces as a 302 that Inertia follows automatically — the confirmation screen must therefore be a full Inertia page (or you publish the view and wrap it). After confirmation the user lands back on intended, which Inertia will render normally. If you prefer to keep the SPA state, have your modal POST the code to the package's confirm route via router.post and then router.reload().

Pair it with Fortify password confirmation deliberately. Fortify's password.confirm is the other step-up flow. Do not stack both by accident: pick 2FA for "prove it's you now", and reserve password.confirm for flows where the authenticator device may be unavailable. If you do use both, order the middleware ['auth', 'password.confirm', 'tfa-confirmation'] so the user is never asked twice on one request.

Testing routes behind the middleware. The cheapest approach is to drop the middleware in feature tests that are not about confirmation:

$this->withoutMiddleware(\Wnx\TfaConfirmation\Http\Middleware\RequiresTwoFactorConfirmation::class);

And write dedicated confirmation tests by forging the session flag the package writes (inspect session()->all() once after a real manual confirmation to get the exact key):

$this->actingAs($user)
    ->withSession([/* the package's confirmation key */ => now()->timestamp])
    ->get('/settings/api-tokens')
    ->assertOk();

Add one test with a user who has no 2FA enabled — that is the case that breaks in production.

Guard against non-2FA users explicitly. Decide the policy and encode it: either deny access to the protected area entirely ($user->two_factor_confirmed_at === null → redirect to the 2FA setup screen) or fall back to password.confirm. Silent allow is the most common production incident.

Combine with audit logging. Wrap the confirmation route's controller or listen for the confirmation event (if published) to log "user X performed step-up auth for /account DELETE" — step-up auth is exactly the interaction auditors ask about.

Gotchas and Tips

  • Middleware order matters. tfa-confirmation must run after auth/web. If it runs first, $request->user() is null and you get a 500 or a redirect loop. Always list it second.
  • Never protect the confirmation route itself. If it sits inside the protected group you get an infinite redirect. Keep the package's own route in the web group only and double-check with route:list.
  • Users without 2FA get locked out. The package answers "is there a confirmed session?" — it does not force enrolment. Ship a redirect-to-setup path or you will take down settings for every user who hasn't enabled 2FA.
  • The intended redirect only replays GETs. A DELETE or PUT form submission interrupted by the confirmation screen loses its body. Design the UI so confirmation happens before the destructive form is submitted (render the prompt on the GET page), not in the middle of a POST.
  • Session-scoped, so device-scoped. Confirming on your laptop does nothing for your phone, and php artisan session:clear / a session driver change (file → redis) silently invalidates everyone's confirmation. That's fine — just don't treat it as an account-wide flag.
  • array session driver in tests stores nothing. Confirmed-state assertions will fail mysteriously if you switched drivers; use the default test config or set the session flag explicitly.
  • Config cache. After publishing or editing config/tfa-confirmation.php, run php artisan config:clear. The classic "I changed the duration and nothing happened" bug.
  • Check two_factor_confirmed_at, not two_factor_secret. Users mid-enrolment have a secret but no confirmation; treating them as 2FA-enabled creates a prompt they can't satisfy.
  • AJAX clients get a 302, not a 423. If you need a machine-readable signal for fetch/axios, intercept the redirect in a small middleware or ->renderable() handler, and return a JSON payload with the confirm URL so the frontend can open a modal instead of a full navigation.
  • Signed URLs and short-lived tokens. If the protected action is reached via URL::temporarySignedRoute, the round trip through the confirmation screen can outlive the signature. Confirm first, then generate the signed URL.
  • Extension points: publish and edit the Blade view to match your design system; adjust duration per environment (short in production, null/long in local so you don't type codes all day); swap the confirmation route's controller by re-declaring the route in your own routes/web.php if you need custom post-confirmation redirect logic.
  • Debugging recipe: Log::info(session()->all()) inside a protected route after a manual confirmation shows exactly which key/duration the package writes; if it's absent, the middleware never ran (alias typo) or the confirm route posted to a different session (domain/SESSION_DOMAIN mismatch).
  • Small package, no dependents (0 on Packagist). Vendor the middleware understanding into your team's head or your own tests — don't assume a Stack Overflow answer exists. Reading src/Http/Middleware/RequiresTwoFactorConfirmation.php takes five minutes and answers 90% of the questions above.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console