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

Filament 2Fa Laravel Package

backstage/filament-2fa

Adds two-factor authentication to Filament panels. Users enroll via QR code with authenticator apps, confirm with a one-time code, and sign in with TOTP plus recovery codes. Includes setup pages, middleware, and customizable flows for securing your admin panel.

View on GitHub
Deep Wiki
Context7

Getting Started

What it is: a Filament panel plugin that bolts Fortify-style TOTP two-factor auth onto Filament's own login and profile pages — QR enrollment, confirmation, recovery codes, and a post-login challenge screen — without you having to build any of the Livewire plumbing.

Minimum viable install (≈10 minutes):

composer require backstage/filament-2fa

# config + migration (both tags, in most versions)
php artisan vendor:publish --tag="filament-2fa-config"
php artisan vendor:publish --tag="filament-2fa-migrations"
php artisan migrate
  1. User model — add the package's TwoFactorAuthenticatable trait and make sure the secret is handled safely:

    use Backstage\Filament2FA\Traits\TwoFactorAuthenticatable;
    
    class User extends Authenticatable
    {
        use TwoFactorAuthenticatable;
    
        protected $casts = [
            'two_factor_secret'          => 'encrypted',
            'two_factor_recovery_codes'  => 'encrypted:array',
            'two_factor_confirmed_at'    => 'datetime',
        ];
    }
    

    If you already run Fortify, keep Fortify's own trait/casts and skip the migration — the columns are identical.

  2. Panel provider — register the plugin:

    use Backstage\Filament2FA\Filament2FA;
    
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->plugin(Filament2FA::make());
    }
    
  3. Where to look first: log in, then hit /admin/profile (or your panel's user menu → Security / Two-factor authentication). That page is the whole product surface: enable 2FA → QR code → 6-digit confirmation → recovery codes.

First real use case: enable it on your own super-admin account, scan the QR with Google Authenticator / 1Password, log out, log back in, and confirm you get the challenge screen instead of the dashboard. That single round-trip tells you whether the plugin is wired correctly for your panel, guard, and layout.

Implementation Patterns

Per-panel, not per-app. The plugin is a Panel plugin, so a second panel (/portal, /vendor) needs its own ->plugin(Filament2FA::make()). If your panels share a User model this "just works," but the challenge session is guard-scoped — see Gotchas.

Enrollment modes. Pick one deliberately:

  • Optional (default): users opt in from their profile page. Best for internal admin tools.
  • Forced: config flag (and/or ->force() on the plugin, depending on version) redirects unenrolled users to the setup screen. Pair this with a User::canAccessPanel() bypass for your own account, or you can lock yourself out during a deploy.

Wiring the challenge into the login flow. Don't hand-roll this. Filament's Login page is what the package decorates; after credential validation it stashes the user in the session and renders the challenge page. The practical integration points are:

// app/Providers/Filament/AdminPanelProvider.php
->login()                 // required — the plugin hooks this page
->profile()               // required for the enrollment page
->plugin(Filament2FA::make())

If you use a custom login page (->login(MyLogin::class)), make sure it still extends Filament's Login (or apply the package trait), otherwise the redirect-to-challenge step never fires and 2FA silently becomes decorative.

Recovery codes are a UX problem, not a technical one. The codes are shown exactly once. Two patterns that work:

  • Show them in a modal with a "copy all" and "download .txt" button, and refuse to close until acknowledged.
  • Regenerate on demand from the profile page behind a password confirmation, and invalidate every active session except the current one.

Logging and notification. The package emits Fortify-compatible events. Listen and notify, because your users will lose their phones:

Event::listen(TwoFactorAuthenticationEnabled::class, function ($event) {
    activity()->performedOn($event->user)->log('2FA enabled');
    $event->user->notify(new TwoFactorWasEnabled());
});

Migrating from Fortify's UI. If users already have two_factor_secret populated, don't re-run the migration; just install the plugin, point the panel at it, and disable Fortify's Blade-based 2FA views/routes. Secrets carry over untouched because the column names are identical.

Testing. Don't drive the QR flow in the test suite. Set the session key the challenge middleware checks (or actingAs() a user with two_factor_confirmed_at and a fake confirmed secret in the session) and assert the redirect. Keep one slow, real end-to-end test of enrollment; stub everything else.

Customization surface. Publish views and swap the QR renderer if your hosting lacks imagick — the SVG writer from bacon/bacon-qr-code is usually fine and avoids the extension entirely.

Gotchas and Tips

Don't run Fortify's 2FA and this package at the same time. If config/fortify.php has Features::twoFactorAuthentication([...]) enabled and the Filament plugin is registered, you get two competing challenge redirects and confusing session state. Leave Fortify installed for its other features if you must, but disable its 2FA feature block.

Session scoping bites in multi-panel setups. The "user passed the challenge" flag is stored per-guard, and Filament panels can share a guard. Logging into /portal after challenging on /admin may or may not re-prompt depending on your guard config — test it explicitly before shipping, and don't assume the challenge is airtight across panels.

two_factor_secret must be encrypted, and the column must be text. A varchar(255) looks fine in dev and truncates a base32 secret in production, producing "invalid code" errors that look like clock drift. Check the published migration.

Clock drift is the #1 support ticket. Enable the window option in the Google2FA config (1–2 steps of tolerance) and make sure your server runs NTP. Your support queue will thank you.

Force mode needs an escape hatch. Before enabling force, add a console command or an artisan tinker snippet that clears two_factor_secret for a given user. You will need it at 2am, and canAccessPanel() is not a reliable bypass once forced enrollment is on.

Password confirmation before disabling. Turning 2FA off should require re-entering the password and should log out other devices. If the plugin doesn't enforce this in your version, enforce it in a policy or a wrapper around the disable action.

Config caching. After changing filament-2fa.php, run php artisan optimize:clear. Symptoms of a stale config cache: plugin appears to do nothing, or the issuer name in the QR stays wrong across deploys.

Filament major upgrades. The package tracks Filament v3 and v4 with separate release lines; pin the major (^1.0 vs ^2.0, whatever maps to your Filament version) and read the upgrade notes before bumping. The login/profile page hooks are the parts that break.

Extension points, in order of usefulness: published Blade views → plugin config (issuer, force, window, recovery code count) → replacing the QR generator binding in the container → your own TwoFactorAuthenticatable trait if you need custom recovery-code formatting. Reach for the trait last; it's the most upgrade-fragile surface.

Zero dependents on Packagist is a signal worth weighting: you're an early adopter, so budget time to read the source (it's small — a handful of Livewire components and one middleware) rather than expecting Stack Overflow answers. The upside is that 69 stars and a 2025-06 release suggests it's actively maintained by the Backstage team for their own products — treat it as "vendor-supported" and pin your version.

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.
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment