profile-filament-plugin
Auth
Multi-Factor Authentication
On this page
- Introduction
- App Authentication
- Email Authentication
- WebAuthn Authentication
- Recovery
- Requiring multi-factor authentication
- Custom mfa providers
- Customizing the MultiFactorChallenge
- Security notes about multi-factor authentication
Introduction
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:
- App Authentication uses a Google Authenticator-compatible app (such as Google Authenticator, Authy or Microsoft Authenticator apps) to generate a time-based one-time password (TOTP) that is used to verify the user.
- Email Authentication sends a one-time code to the user's email address, which they must enter to verify their identity.
- WebAuthn Authentication allows a user to use either a Passkey or a Hardware Security Key to verify their identity.
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
])
);
}

Prep User Model
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.
Modify Login
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.
App Authentication
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()
])
);
}
Changing the app code expiration time
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),
])
Customizing the App Authentication Brand Name
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'),
])
Limit App Registrations
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)
])
Setting Up Recovery Codes
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
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(),
]);
Changing the code expiration time.
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),
]);
Changing the notification
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 Authentication
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()
])
);
}
WebAuthn Routes
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
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 />'))
):
])
Customizing the 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.
Customize the relying party
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.
Limit Security Key Registrations
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)
])
Recovery
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;
class User extends Authenticatable
{
use InteractsWithAuthenticationRecovery;
}
{tip} The 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 recovery codes in a completely separate table.
The plugin automatically registers our default Recover provider when you add MFA providers using the multiFactorAuthentication() method on the plugin, so you do not need to enable manually.
Using a Custom Recovery Provider
For the majority of applications, the plugin's RecoveryCodeProvider should be more than adequate. If your application needs differ than what the default provider offers, you may define your own provider instead. Your custom provider must implement the RecoveryProvider interface.
Here is a simple example of a custom recovery code provider you could create:
use Illuminate\Contracts\Auth\Authenticatable;
use Rawilk\ProfileFilament\Auth\Multifactor\Recovery\Contracts\RecoveryProvider;
use Rawilk\ProfileFilament\Auth\Multifactor\Recovery\Contracts\HasMultiFactorAuthenticationRecovery;
class CustomRecoveryProvider implements RecoveryProvider
{
public static function make(): static
{
return app(static::class);
}
public function isEnabled(HasMultiFactorAuthenticationRecovery $user): bool
{
return filled($user->getAuthenticationRecoveryCodes());
}
public function needsToBeSetup(HasMultiFactorAuthenticationRecovery $user): bool
{
return blank($user->getAuthenticationRecoveryCodes());
}
public function getManagementSchemaComponents(): array
{
return [
// Any filament schema components
];
}
public function generateRecoveryCodes(): array
{
return [
// ...
];
}
public function saveRecoveryCodes(HasMultiFactorAuthenticationRecovery $user, ?array $codes): void
{
// Save codes for the user
}
public function getChallengeFormComponents(Authenticatable $user): array
{
return [
// Any filament schema components
];
}
public function getChallengeSubmitLabel(): ?string
{
return 'Verify account';
}
public function getChangeToProviderActionLabel(Authenticatable $user): ?string
{
return 'Use recovery code';
}
}
Now you just need to tell the plugin to use your custom recovery code provider:
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
ProfileFilamentPlugin::make()
->multiFactorAuthentication(providers: [
// ...
], recoveryProvider: CustomRecoveryProvider::make())
Alternatively, you could use the multiFactorRecovery() method on the plugin instead after you call multiFactorAuthentication().
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
ProfileFilamentPlugin::make()
->multiFactorAuthentication(providers: [
// ...
])
->multiFactorRecovery(CustomRecoveryProvider::make())
Changing the number of recovery codes that are generated
By default, the plugin generates 8 recovery codes for each user. To change this you can use the codeCount() method on the RecoveryCodeProvider instance when defining multi-factor authentication on the plugin.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Recovery\RecoveryCodeProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication(providers: [
// ...
], recoveryProvider: RecoveryCodeProvider::make()->codeCount(10))
Preventing users from regenerating their recovery codes
By default, users can visit their profile and regenerate their recovery codes. If you want to prevent this, you can use the regenerableCodes(false) method on the RecoveryCodeProvider instance when defining multi-factor authentication on the plugin.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Recovery\RecoveryCodeProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication(providers: [
// ...
], recoveryProvider: RecoveryCodeProvider::make()->regenerableCodes(false))
Customizing how recovery codes are generated
The default RecoveryCodeProvider generates a 16-digit random string with dashes after every 4 characters and capitalizes the whole string. The recovery codes will be in the format XXXX-XXXX-XXXX-XXXX by default. You can use the generateCodesUsing() method on the RecoveryCodeProvider instance when defining multi-factor authentication on the plugin to change how recovery codes are generated.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use Rawilk\ProfileFilament\Auth\Multifactor\Recovery\RecoveryCodeProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication(providers: [
// ...
], recoveryProvider: RecoveryCodeProvider::make()
->generateCodesUsing(fn () => Str::random(8))
)
{note} The
RecoveryCodeProviderhashes each recovery code before it saves them to the user. You should extend or create your own recovery provider if this is undesired behavior.
Disabling Recovery
Although not recommended, if you do not wish to provide a recovery mechanism for your users, you may pass false as a value for the recoveryProvider parameter in the multiFactorAuthentication() method on the plugin.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
ProfileFilamentPlugin::make()
->multiFactorAuthentication(providers: [
// ...
], recoveryProvider: false)
Alternatively you could also set the Recovery provider instance to null using the multiFactorRecovery() method on the plugin after you call the multiFactorAuthentication() method.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
ProfileFilamentPlugin::make()
->multiFactorAuthentication(providers: [
// ...
])
->multiFactorRecovery(null)
Requiring multi-factor authentication
By default, users are not required to set up multi-factor authentication. You can require users to configure it by passing isRequired: true as a parameter to the multiFactorAuthentication() method on the plugin.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
// ...
], isRequired: true)
When this is enabled, users will be prompted to set up multi-factor authentication after they sign in, if they have not already done so.
Custom mfa providers
If you need other multi-factor methods for your users, can create your own custom MFA providers. Your class needs to implement the MultiFactorAuthenticationProvider interface:
namespace App\Auth\Providers;
use Illuminate\Contracts\Auth\Authenticatable;
use Rawilk\ProfileFilament\Auth\Multifactor\Contracts\MultiFactorAuthenticationProvider;
use Filament\Schemas\Components\Component;
use Filament\Actions\Action;
use Filament\Auth\MultiFactor\Contracts\HasBeforeChallengeHook;
class SmsProvider implements MultiFactorAuthenticationProvider, HasBeforeChallengeHook
{
public static function make(): static
{
return app(static::class);
}
public function isEnabled(Authenticatable $user): bool
{
// ...
}
public function getId(): string
{
return 'sms';
}
/**
* This is a label that's shown in the default preferred mfa
* provider select ui component.
*/
public function getSelectLabel(): string
{
return __('SMS Text Codes');
}
public function beforeChallenge(Authenticatable $user): void
{
// send sms code
}
/**
* @return array<Component|Action>
*/
public function getManagementSchemaComponents(): array
{
return [
// ...
];
}
/**
* @return array<Component|Action>
*/
public function getChallengeFormComponents(Authenticatable $user): array
{
return [
// ...
];
}
public function getChallengeSubmitLabel(): ?string
{
// return `null` to hide the submit button in the form.
return __('Verify');
}
public function getChangeToProviderActionLabel(Authenticatable $user): ?string
{
return __('Use an SMS code');
}
}
In this provider we are also using Filament's HasBeforeChallengeHook interface. This allows us to execute some code before the provider's challenge is shown to the user. In this case, the provider is sending a text to the user with a verification code. If your provider doesn't need to do something like this, you can omit the interface.
With your custom MFA provider created, you can enable it on the panel through the plugin instance:
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use App\Auth\Providers\SmsProvider;
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
SmsProvider::make(),
])
{note} If you want your custom provider to be available for sudo challenges, you will need to create a custom sudo challenge provider too. Your custom sudo challenge provider should use the same
getId()value as your custom mfa provider does.
Customizing the MultiFactorChallenge
The MultiFactorChallenge page behaves very similarly to the Login page. Like the login process, we utilize Laravel's Pipeline to send a custom MultiFactorEventBag object through a series of actions that help finish the multi-factor authentication process. We have a set of default classes that the plugin provides to handle this; however, you may wish to define your own multi-factor authentication process.
To do this, use the sendMultiFactorChallengeThrough() method on the plugin instance.
use Rawilk\ProfileFilament\ProfileFilamentPlugin;
use App\Actions\Auth\Login\AuthenticateUser;
ProfileFilamentPlugin::make()
->multiFactorAuthentication([
// ...
])
->sendMultiFactorChallengeThrough([
AuthenticateUser::class,
])
Your action classes should have either a handle() or __invoke() method for the pipeline to interact with. Here is a basic example to get you started:
namespace App\Actions\Auth\Login;
use Closure;
use Rawilk\ProfileFilament\Auth\Multifactor\Filament\Dto\MultiFactorEventBagContract;
class AuthenticateUser
{
public function __invoke(MultiFactorEventBagContract $request, Closure $next)
{
// $data = $request->getData();
return $next($request);
}
}
{note} The multi-factor authentication providers will have already verified the user's identity during form validation, so there is no need for you to handle that in any of your authentication classes.
Here are the default multi-factor authentication actions that we send the MFA challenge request through:
use Rawilk\ProfileFilament\Auth\Login\AuthenticationPipes\PrepareAuthenticatedSession;
use Rawilk\ProfileFilament\Auth\Multifactor\Filament\ChallengePipes\AuthenticateUser;
use Rawilk\ProfileFilament\Auth\Multifactor\Filament\ChallengePipes\GuardAgainstExpiredPasswordConfirmation;
$defaults = [
GuardAgainstExpiredPasswordConfirmation::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.
Security notes about multi-factor authentication
Similar to how Filament handles MFA, the plugin's multi-factor authentication process occurs before the user is actually authenticated into the app. This allows you to be sure that no users can authenticate and access the app without passing the multi-factor authentication step. You do not need to remember to add middleware to any of your authenticated routes to ensure that users completed the multi-factor authentication step.
However, if you have other parts of your Laravel app that authenticate users, be aware that they will not be challenged for multi-factor authentication if they are already authenticated elsewhere and visit the panel, unless multi-factor authentication is required and they have not set it up.