Filament TOTP two-factor authentication with recovery codes
Enable authenticator-app two-factor login in a Filament 4 panel: provider setup, encrypted secret columns and recovery codes on the user.
FilamentAuthenticationfilament2fatotpsecurity
Code
<?php
namespace App\Providers\Filament;
use Filament\Auth\MultiFactor\App\AppAuthentication;
use Filament\Panel;
use Filament\PanelProvider;
class StudioPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('studio')
->path('studio')
->login()
->profile() // gives users the page where they set 2FA up
->multiFactorAuthentication([
AppAuthentication::make()
->recoverable() // one-time recovery codes
->recoveryCodeCount(8)
->codeWindow(4), // tolerate a little clock drift
], isRequired: false); // true forces every user to enrol
}
}
<?php
namespace App\Models\Concerns;
/**
* Add to the User model and implement the two Filament contracts:
* Filament\Auth\MultiFactor\App\Contracts\HasAppAuthentication
* Filament\Auth\MultiFactor\App\Contracts\HasAppAuthenticationRecovery
* Also merge the casts() and hidden() entries shown below.
*/
trait HasTotpSecret
{
// casts(): 'app_authentication_secret' => 'encrypted',
// 'app_authentication_recovery_codes' => 'encrypted:array',
// $hidden: add both columns.
public function getAppAuthenticationSecret(): ?string
{
return $this->app_authentication_secret;
}
public function saveAppAuthenticationSecret(?string $secret): void
{
$this->app_authentication_secret = $secret;
$this->save();
}
/** Label shown inside the authenticator app. */
public function getAppAuthenticationHolderName(): string
{
return $this->name;
}
/** @return ?array<string> */
public function getAppAuthenticationRecoveryCodes(): ?array
{
return $this->app_authentication_recovery_codes;
}
/** @param ?array<string> $codes */
public function saveAppAuthenticationRecoveryCodes(?array $codes): void
{
$this->app_authentication_recovery_codes = $codes;
$this->save();
}
}
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// text, not string: encrypted payloads are longer than the plain value.
$table->text('app_authentication_secret')->nullable();
$table->text('app_authentication_recovery_codes')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['app_authentication_secret', 'app_authentication_recovery_codes']);
});
}
};
Needs: filament/filament@^4.0
How it works
What it solves
Passwords alone are weak for panels that manage money or customer data. Time-based one-time passwords (TOTP) from an authenticator app are the standard second factor, and Filament 4 ships the whole flow, QR code enrolment, challenge screen and recovery codes, so you only supply storage.
How it works
In the panel provider, multiFactorAuthentication() receives a list of providers. AppAuthentication is the TOTP provider; recoverable() adds one-time recovery codes at enrolment, and codeWindow() widens the accepted time slice. isRequired: false makes it optional, users enable it themselves from the profile page, which the profile() call adds.
On the model side, Filament needs to read and write two values. The trait implements the contract methods: the secret getter and saver, the name that appears in the authenticator app, and the recovery code getter and saver. The migration adds two text columns. Both are cast with encrypted (the codes as encrypted:array) so a database dump never exposes a usable secret, and both are hidden from serialisation.
At login, a user with a secret set sees a code prompt after the password step.
Adapting it
Set isRequired: true for admin panels to force enrolment. Use the trait as written, or put the methods straight on User. Pair it with the role guard so only approved accounts reach the panel.
Pitfalls
- Losing
APP_KEYmakes stored secrets undecryptable, so lock-out recovery needs a plan. - Server clock drift breaks TOTP, keep NTP running.
- Recovery codes are shown once; tell users to store them.
How to use it
- Run
composer require filament/filament:"^4.0"and create a panel. - Add the migration and run
php artisan migrate. - Make
UserimplementHasAppAuthenticationandHasAppAuthenticationRecovery, use the trait, and add the encrypted casts and hidden columns. - Add the
multiFactorAuthentication()andprofile()calls to the panel provider. - Log in, open Profile, and scan the QR code with an authenticator app.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.