rawilk/profile-filament-plugin
Filament plugin that jumpstarts a user profile area with multi-factor authentication, password and session management, migrations, and sensible defaults—opinionated but customizable. Designed to remove boilerplate and integrate cleanly into your panel.
title: Multi-Factor Authentication
Enabling multi-factor authentication (MFA) for your users can add an extra layer of security to your users' accounts. When MFA is enabled, users must perform an extra step before they are authenticated and have access to the application.
Even though Filament introduced an MFA implementation with v4.0, I still wanted it to function a little differently. The package's MFA implementation is based heavily off Filament's implementation but with some slight differences. You should use either Filament's MFA or this package's, but not both; they are not compatible with each other.

This package includes three providers of MFA which you can enable out of the box:
By default, the package provides a security page with a multi-factor authentication management component that allows users to set up multi-factor authentication with. As long as at least one MFA provider is configured to the plugin, the MFA manager component will show up.
use Filament\Panel;
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(
ProfileFilamentPlugin::make()
->multiFactorAuthentication(providers: [
// providers here
])
);
}

Regardless of which MFA providers you choose to enable, your user model must implement the HasMultiFactorAuthentication interface and use the InteractsWithMultiFactorAuthentication trait which provides the necessary methods to interact with multi-factor authentication in general for the plugin.
use Rawilk\ProfileFilament\Auth\Multifactor\Concerns\InteractsWithMultiFactorAuthentication;
use Rawilk\ProfileFilament\Auth\Multifactor\Contracts\HasMultiFactorAuthentication;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements HasMultiFactorAuthentication
{
use InteractsWithMultiFactorAuthentication;
// ...
}
{tip} The plugin provides a default implementation for speed and simplicity, but you could implement the required methods yourself and customize the way your user model indicates to the plugin that a user has MFA enabled on their account.
You should also be sure to run the following database migration to ensure the necessary mfa columns are on the user model. If you publish the package's migrations, you will get the migration shown here added to your app's migrations.
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Schema::table('users', function (Blueprint $table) {
$table->boolean('two_factor_enabled')->default(false);
$table->string('preferred_mfa_provider')->nullable();
});
Our implementation checks for the two_factor_enabled flag on the user model to determine if the user has MFA enabled on their account. The preferred_mfa_provider column is used to store a preference for the user as to which MFA provider is shown initially on MFA and Sudo challenges.
Our MFA process uses a separate page from the Login page, so you will need to customize your panel's login so that the user being authenticated gets stored in the session and redirected to the multi-factor challenge instead.
The plugin's implementation of app authentication allows users to register multiple authenticator apps to their account, so they are stored in a separate table. You should run the following database migration to create the table.
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Rawilk\ProfileFilament\Support\Config;
$authenticatableClass = Config::getAuthenticatableModel();
$authenticatableTableName = (new $authenticatableClass)->getTable();
Schema::create(Config::getTableName('authenticator_app'), function (Blueprint $table) use ($authenticatableClass, $authenticatableTableName) {
$table->id();
$table->foreignIdFor($authenticatableClass, 'user_id')
->constrained(table: $authenticatableTableName, indexName: 'authenticator_apps_authenticatable_fk')
->cascadeOnDelete();
$table->string('name')->nullable();
$table->text('secret')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
In the User model, you should implement the HasAppAuthentication interface and use the InteractsWithAppAuthentication trait which provides the necessary methods to interact with the authenticator apps for the integration.
use Rawilk\ProfileFilament\Auth\Multifactor\App\Contracts\HasAppAuthentication;
use Rawilk\ProfileFilament\Auth\Multifactor\App\Concerns\InteractsWithAppAuthentication;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements HasAppAuthentication
{
use InteractsWithAppAuthentication;
// ...
}
{note} You will need this interface and trait in addition to the
HasMultiFactorAuthenticationinterface shown above on your user model.
Finally, you should add the app authentication provider to the plugin. You can use the multiFactorAuthentication method on the plugin and pass a AppAuthenticationProvider instance to it:
use Filament\Panel;
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\App\AppAuthenticationProvider;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
AppAuthenticationProvider::make()
])
);
}
App codes are issued using a time-based one-time password (TOTP) algorithm, which means that they are only valid for a short period of time before and after they are generated. The time is defined in a "window" of time. By default, the plugin uses an expiration window of 8, which creates a 4-minute validity period on either side of the generation time (8 minutes in total).
To change the window, for example to only be valid for 2 minutes after it is generated, you can use the codeWindow() method on the AppAuthenticationProvider instance, set to 4.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\App\AppAuthenticationProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
AppAuthenticationProvider::make()
->codeWindow(4),
])
Each app authentication integration has a "brand name" that is displayed in the authentication app. By default, this is the name of your app. If you want to change this, you can use the brandName() method on the AppAuthenticationProvider instance when adding it to the plugin.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\App\AppAuthenticationProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
AppAuthenticationProvider::make()
->brandName('Custom App Name'),
])
By default, the provider limits the number of authentication apps a user may register to their account to 3. You can either increase or decrease this limit by using the limitAppRegistrationsTo() method on the AppAuthenticationProvider instance. The example below will allow users to register up to 5 authentication apps to their account.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\App\AppAuthenticationProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
AppAuthenticationProvider::make()
->limitAppRegistrationsTo(5)
])
If your users lose access to their multi-factor authentication app, they will be unable to sign in to your application. To prevent this, recovery codes can be used. See Recovery for more information on setting MFA recovery up.
Email authentication sends the user one-time codes to their email address, which they must enter to verify their identity.
To enable email authentication for the plugin you must first add a new column to your users table. The column needs to store a boolean indicating whether email authentication is enabled for the user.
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Schema::table('users', function (Blueprint $table) {
$table->boolean('has_email_authentication')->default(false);
});
{note} This column is not part of the publishable migrations from this package.
Next, you should implement the HasEmailAuthentication interface on the User model and use the InteractsWithEmailAuthentication trait which provides the plugin the necessary methods to interact with the column that indicates whether email authentication is enabled for the user.
use Rawilk\ProfileFilament\Auth\Multifactor\Email\Contracts\HasEmailAuthentication;
use Rawilk\ProfileFilament\Auth\Multifactor\Email\Concerns\InteractsWithEmailAuthentication;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements HasEmailAuthentication
{
use InteractsWithEmailAuthentication;
// ...
}
{tip} This plugin provides a default implementation for speed and simplicity, but you could implement the required methods yourself and customize the column name or store the value in a completely separate table.
Finally, you should activate the email authentication feature on the plugin. To do this, use the multiFactorAuthentication() method on the plugin and pass a EmailAuthenticationProvider instance to it.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Email\EmailAuthenticationProvider;
ProfileFilamentPlugin::make()
->multiFactorRecovery([
EmailAuthenticationProvider::make(),
]);
Email codes are issued with a lifetime of 15 minutes, after which they expire.
To change the expiration period, for example to be valid for only 5 minutes after codes are generated, you can use the codeExpiryMinutes() method on the EmailAuthenticationProvider instance, set to 5.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Email\EmailAuthenticationProvider;
ProfileFilamentPlugin::make()
->multiFactorRecovery([
EmailAuthenticationProvider::make()
->codeExpiryMinutes(5),
]);
The email authentication provider provides a VerifyEmailAuthenticationNotification by default for sending the email notification with. You are free however to extend the notification or use your own class by providing the class name to the notifyWith() method on the EmailAuthenticationProvider instance.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Email\EmailAuthenticationProvider;
use App\Notifications\CustomVerifyEmailNotification;
ProfileFilamentPlugin::make()
->multiFactorRecovery([
EmailAuthenticationProvider::make()
->notifyWith(CustomVerifyEmailNotification::class),
]);
WebAuthn can be used as an alternative to App Authentication (TOTP) codes. Our implementation with the WebAuthnProvider allows users to use either Passkeys or Hardware Security Keys such as a YubiKey.
To get started with this provider, you'll need to make sure you run the migration to create the webauthn_keys table:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Rawilk\ProfileFilament\Support\Config;
$authenticatableClass = Config::getAuthenticatableModel();
$authenticatableTableName = (new $authenticatableClass)->getTable();
Schema::create(Config::getTableName('webauthn_key'), function (Blueprint $table) use ($authenticatableClass, $authenticatableTableName) {
$table->id();
$table->foreignIdFor($authenticatableClass, 'user_id')
->constrained(table: $authenticatableTableName, indexName: 'webauthn_authenticatable_fk')
->cascadeOnDelete();
$table->string('name')->nullable();
$table->text('credential_id');
$table->json('data');
$table->string('attachment_type', 50)->nullable();
$table->boolean('is_passkey')->default(false);
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
In the User model, you should implement the HasWebauthn interface and use the InteractsWithWebauthn trait which provides the necessary methods to interact with the security keys for the plugin.
use Rawilk\ProfileFilament\Auth\Multifactor\Webauthn\Contracts\HasWebauthn;
use Rawilk\ProfileFilament\Auth\Multifactor\Webauthn\Concerns\InteractsWithWebauthn;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements HasWebauthn
{
use InteractsWithWebauthn;
// ...
}
Finally, you should add the webauthn provider to the plugin. You can use the multiFactorAuthentication method on the plugin and pass a WebauthnProvider instance to it:
use Filament\Panel;
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Webauthn\WebauthnProvider;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
WebauthnProvider::make()
])
);
}
If you intend to use Passkey login, you should register the passkey routes in a route file using the Webauthn() route macro:
// routes/web.php
use Illuminate\Routing\Route;
Route::webauthn();
This will register the necessary routes for our passkey login to function properly.
{note} These routes require sessions to work properly and should be part of the
webmiddleware group.
Passkey login allows your users to authenticate without using their username or password; all they need is a passkey they registered to their account in your application. To add a passkey login action to your login form automatically, you can use the passkeyLogin() method on the plugin instance:
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Webauthn\WebauthnProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
WebauthnProvider::make(),
])
->passkeyLogin()
{tip} Be sure to activate the
WebauthnProvideron the plugin too.
We will append the action to the login form, and here is what it will look like by default:

The layout of the action is intentionally basic, however you could style it however you want by publishing the views from the package.
When the link is clicked on it will show a prompt like this (if you have a password manager installed):

If you wish to place the action somewhere else on the login form, you can always add the blade component yourself:
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\HtmlString;
use Filament\Schemas\Components\Text;
$schema->components([
Text::make(
new HtmlString(Blade::render('<x-profile-filament::passkey-login />'))
):
])
Similar to the multi-factor authentication process, we send the passkey authentication through a series of steps using Laravel's Pipeline.
If you wish to use your own logic, you can use the sendPasskeyLoginThrough() method on the plugin instance and provide an array of your own authentication classes.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Webauthn\WebauthnProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
WebauthnProvider::make(),
])
->passkeyLogin()
->sendPasskeyLoginThrough([
PasskeyLoginClassOne::class,
])
Here are the defaults we use for passkey login:
use Rawilk\ProfileFilament\Auth\Multifactor\Webauthn\PasskeyLoginPipes\FindPasskey;
use Rawilk\ProfileFilament\Auth\Multifactor\Webauthn\PasskeyLoginPipes\AuthenticateUser;
use Rawilk\ProfileFilament\Auth\Login\AuthenticationPipes\PrepareAuthenticatedSession;
$defaults = [
FindPasskey::class,
AuthenticateUser::class,
PrepareAuthenticatedSession::class,
];
{tip} With the default authentication classes, we perform the same authentication check on the user as we do in the login form to ensure the user is actually allowed to sign in to the application.
The Relying Party corresponds to the application that will ask the user to interact with an authenticator.
For most applications, the defaults we have set in the config should work just fine. If you need to customize the relying party, you can modify the relevant config keys under the relying_party config key in the profile-filament config file.
'webauthn' => [
'relying_party' => [
'name' => env('WEBAUTHN_RELYING_PARTY_NAME', env('APP_NAME')),
'id' => env('WEBAUTHN_RELYING_PARTY_ID', parse_url(config('app.url'), PHP_URL_HOST)),
// Image must be encoded as base64.
'icon' => env('WEBAUTHN_RELYING_PARTY_ICON'),
]
]
{note} The
idfor therelying_partyshould be the domain of the application without the scheme, userinfo, port or path. IP addresses are not allowed either. Supported values include:www.sub.domain.com,sub.domain.com,domain.com.
By default, the provider limits the number of security keys a user may register to their account to 5. You can either increase or decrease this limit by using the limitRegistrationsTo() method on the WebauthnProvider instance. The example below will allow users to register up to 10 security keys to their account.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Webauthn\WebauthnProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
WebauthnProvider::make()
->limitRegistrationsTo(10)
])
If your users lose access to their MFA apps or devices, they will be unable to sign in to your application. To prevent this, you can generate a set of recovery codes that users can use to sign in if they lose access to their apps or devices.
Filament ties recovery codes to the AuthenticationApp provider; however, I believe recovery codes should be used no matter which MFA provider is enabled on a user account. I've decided to separate account recovery into its own provider so that it can be used in addition to any of the MFA providers.
To start with recovery, you will need to add a two_factor_recovery_codes column to your users table. The column needs to store the recovery codes. It can be a normal text column in a migration:
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
Schema::table('users', function (Blueprint $table) {
$table->text('two_factor_recovery_codes')->nullable();
});
Next, you should implement the HasMultiFactorAuthenticationRecovery interface on the User model and use the InteractsWithAuthenticationRecovery trait which provides the plugin with the necessary methods to interact with recovery codes.
use Rawilk\ProfileFilament\Auth\Multifactor\Recovery\Contracts\HasMultiFactorAuthenticationRecovery;
use Rawilk\ProfileFilament\Auth\Multifactor\Recovery\Concerns\InteractsWithAuthenticationRecovery;
use Illuminate\Foundation\Auth\User as Authenticatable;
clas...
How can I help you explore Laravel packages today?