Multi-factor authentication
Introduction
Users in Filament can sign in with their email address and password by default. However, you can enable multi-factor authentication (MFA) to 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.
Filament includes two methods of MFA which you can enable out of the box:
- App authentication uses a Google Authenticator-compatible app (such as the 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.
In Filament, users set up multi-factor authentication from their profile page. If you use Filament's profile page feature, setting up multi-factor authentication will automatically add the correct UI elements to the profile page:
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->profile();
}
App authentication
To enable app authentication in a panel, you must first add a new column to your users
table (or whichever table is being used for your "authenticatable" Eloquent model in this panel). The column needs to store the secret key used to generate and verify the time-based one-time passwords. 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('app_authentication_secret')->nullable();
});
In the User
model, you need to ensure that this column is encrypted and $hidden
, since this is incredibly sensitive information that should be stored securely:
use Filament\Models\Contracts\FilamentUser;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements FilamentUser, MustVerifyEmail
{
// ...
/**
* @var array<string>
*/
protected $hidden = [
// ...
'app_authentication_secret',
];
/**
* @var array<string, string>
*/
protected $casts = [
// ...
'app_authentication_secret' => 'encrypted',
];
// ...
}
Next, you should implement the HasAppAuthentication
interface on the User
model. This provides Filament with the necessary methods to interact with the secret code and other information about the integration:
use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthentication;
use Filament\Models\Contracts\FilamentUser;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements FilamentUser, HasAppAuthentication, MustVerifyEmail
{
// ...
public function getAppAuthenticationSecret(): ?string
{
// This method should return the user's saved app authentication secret.
return $this->app_authentication_secret;
}
public function saveAppAuthenticationSecret(?string $secret): void
{
// This method should save the user's app authentication secret.
$this->app_authentication_secret = $secret;
$this->save();
}
public function getAppAuthenticationHolderName(): string
{
// In a user's authentication app, each account can be represented by a "holder name".
// If the user has multiple accounts in your app, it might be a good idea to use
// their email address as then they are still uniquely identifiable.
return $this->email;
}
}
TIP
Since Filament uses an interface on your User
model instead of assuming that the app_authentication_secret
column exists, you can use any column name you want. You could even use a different model entirely if you want to store the secret in a different table.
Finally, you should activate the app authentication feature in your panel. To do this, use the multiFactorAuthentication()
method in the configuration, and pass a AppAuthentication
instance to it:
use Filament\Auth\MultiFactor\App\AppAuthentication;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->multiFactorAuthentication([
AppAuthentication::make(),
]);
}
Setting up app recovery codes
If your users lose access to their two-factor authentication app, 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 two-factor authentication app.
In a similar way to the app_authentication_secret
column, you should add a new column to your users
table (or whichever table is being used for your "authenticatable" Eloquent model in this panel). 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('app_authentication_recovery_codes')->nullable();
});
In the User
model, you need to ensure that this column is encrypted as an array and $hidden
, since this is incredibly sensitive information that should be stored securely:
use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthentication;
use Filament\Models\Contracts\FilamentUser;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements FilamentUser, HasAppAuthentication, MustVerifyEmail
{
// ...
/**
* @var array<string>
*/
protected $hidden = [
// ...
'app_authentication_recovery_codes',
];
/**
* @var array<string, string>
*/
protected $casts = [
// ...
'app_authentication_recovery_codes' => 'encrypted:array',
];
// ...
}
Next, you should implement the HasAppAuthenticationRecovery
interface on the User
model. This provides Filament with the necessary methods to interact with the recovery codes:
use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthentication;
use Filament\Auth\MultiFactor\App\Contracts\HasAppAuthenticationRecovery;
use Filament\Models\Contracts\FilamentUser;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements FilamentUser, HasAppAuthentication, HasAppAuthenticationRecovery, MustVerifyEmail
{
// ...
/**
* @return ?array<string>
*/
public function getAppAuthenticationRecoveryCodes(): ?array
{
// This method should return the user's saved app authentication recovery codes.
return $this->app_authentication_recovery_codes;
}
/**
* @param array<string> | null $codes
*/
public function saveAppAuthenticationRecoveryCodes(?array $codes): void
{
// This method should save the user's app authentication recovery codes.
$this->app_authentication_recovery_codes = $codes;
$this->save();
}
}
TIP
Since Filament uses an interface on your User
model instead of assuming that the app_authentication_recovery_codes
column exists, you can use any column name you want. You could even use a different model entirely if you want to store the recovery codes in a different table.
Finally, you should activate the app authentication recovery codes feature in your panel. To do this, pass the recoverable()
method to the AppAuthentication
instance in the multiFactorAuthentication()
method in the configuration:
use Filament\Auth\MultiFactor\App\AppAuthentication;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->multiFactorAuthentication([
AppAuthentication::make()
->recoverable(),
]);
}
Changing the number of recovery codes that are generated
By default, Filament generates 8 recovery codes for each user. If you want to change this, you can use the recoveryCodeCount()
method on the AppAuthentication
instance in the multiFactorAuthentication()
method in the configuration:
use Filament\Auth\MultiFactor\App\AppAuthentication;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->multiFactorAuthentication([
AppAuthentication::make()
->recoverable()
->recoveryCodeCount(10),
]);
}
Preventing users from regenerating their recovery codes
By default, users can visit their profile to regenerate their recovery codes. If you want to prevent this, you can use the regenerableRecoveryCodes(false)
method on the AppAuthentication
instance in the multiFactorAuthentication()
method in the configuration:
use Filament\Auth\MultiFactor\App\AppAuthentication;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->multiFactorAuthentication([
AppAuthentication::make()
->recoverable()
->regenerableRecoveryCodes(false),
]);
}
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 the time they are generated. The time is defined in a "window" of time. By default, Filament 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 AppAuthentication
instance, set to 4
:
use Filament\Auth\MultiFactor\App\AppAuthentication;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->multiFactorAuthentication([
AppAuthentication::make()
->codeWindow(4),
]);
}