Laravel Multi-Factor Authentication package (Email, SMS, Google Authenticator TOTP)
coding-libs/laravel-mfa is a Laravel package for laravel multi-factor authentication package (email, sms, google authenticator totp).
It currently has 2 GitHub stars and 34 downloads on Packagist (latest version v1.1.6).
Install it with composer require coding-libs/laravel-mfa.
Discover more Laravel packages by coding-libs
or browse all Laravel packages to compare alternatives.
Last updated
Multi Factor Authentication CodingLibs Laravel MFA
Installation
composer require coding-libs/laravel-mfa
php artisan vendor:publish --tag=mfa-config
php artisan vendor:publish --tag=mfa-migrations
php artisan migrate
Features
mfa_remembered_devicesMFA facade/service for issuing and verifying codesMFA Channels
log)Compatibility
Usage
use CodingLibs\MFA\Facades\MFA;
// Email/SMS - Generate and send automatically
$challenge = MFA::issueChallenge(auth()->user(), 'email');
// then later
$ok = MFA::verifyChallenge(auth()->user(), 'email', '123456');
// Generate challenge without sending
$challenge = MFA::generateChallenge(auth()->user(), 'email');
// or
$challenge = MFA::issueChallenge(auth()->user(), 'email', false);
// Now handle sending manually
// TOTP
$setup = MFA::setupTotp(auth()->user());
// $setup['otpauth_url'] -> QR code; then verify
$ok = MFA::verifyTotp(auth()->user(), '123456');
// Generate QR code (base64 PNG) from existing TOTP (uses bacon/bacon-qr-code)
$base64 = MFA::generateTotpQrCodeBase64(auth()->user(), issuer: 'MyApp');
// <img src="$base64" />
// Remember device (set cookie on successful MFA)
[$token, $cookie] = [null, null];
$result = MFA::rememberDevice(auth()->user(), lifetimeDays: 30, deviceName: 'My Laptop');
$token = $result['token'];
$cookie = $result['cookie']; // Symfony Cookie instance — attach to response
// Later, skip MFA if remembered device cookie is valid
$shouldSkip = MFA::shouldSkipVerification(auth()->user(), MFA::getRememberTokenFromRequest(request()));
// Recovery Codes
// Generate a fresh set (returns plaintext codes to show once)
$codes = MFA::generateRecoveryCodes(auth()->user());
// Verify and consume a recovery code
$ok = MFA::verifyRecoveryCode(auth()->user(), $inputCode);
// Count remaining unused codes
$remaining = MFA::getRemainingRecoveryCodesCount(auth()->user());
// Clear all codes
$deleted = MFA::clearRecoveryCodes(auth()->user());
Remember Devices (Optional)
config/mfa.php under remember (or via env: see below)MFA::rememberDevice(...) and attach the returned cookie to the responseMFA::shouldSkipVerification($user, MFA::getRememberTokenFromRequest($request))MFA::forgetRememberedDevice($user, $token)Recovery Codes
mfa_recovery_codes.mfa.recovery.hash_algo (default sha256).// Generate N codes (defaults come from config)
$codes = MFA::generateRecoveryCodes($user); // array of plaintext codes
// Show these codes once to the user and prompt them to store securely
// e.g., render as a list and offer a download/print option
if (MFA::verifyRecoveryCode($user, $input)) {
// Success: log user in and consider rotating codes if desired
}
mfa.recovery.regenerate_on_use = true to automatically replace a consumed code with a new one so the remaining count stays steady.// Count remaining unused codes
$remaining = MFA::getRemainingRecoveryCodesCount($user);
// Replace all existing codes with a new set
$fresh = MFA::generateRecoveryCodes($user); // replaceExisting=true by default
// Append without deleting existing codes
$extra = MFA::generateRecoveryCodes($user, count: 2, replaceExisting: false);
// Clear all codes
$deleted = MFA::clearRecoveryCodes($user);
Configuration
config/mfa.php for all options. Key settings:
log (default) or custom integrationconfig('app.name')mfa_rd)sha256)Environment variables (examples)
MFA_EMAIL_ENABLED=true
MFA_EMAIL_FROM_ADDRESS="[email protected]"
MFA_EMAIL_FROM_NAME="Example App"
MFA_EMAIL_SUBJECT="Your verification code"
MFA_EMAIL_CHANNEL="App\Channels\CustomEmailChannel"
MFA_SMS_ENABLED=true
MFA_SMS_DRIVER=log
MFA_SMS_FROM="ExampleApp"
MFA_SMS_CHANNEL="App\Channels\CustomSmsChannel"
MFA_TOTP_ISSUER="Example App"
MFA_TOTP_DIGITS=6
MFA_TOTP_PERIOD=30
MFA_TOTP_WINDOW=1
MFA_REMEMBER_ENABLED=true
MFA_REMEMBER_COOKIE=mfa_rd
MFA_REMEMBER_LIFETIME_DAYS=30
MFA_REMEMBER_PATH=/
MFA_REMEMBER_DOMAIN=
MFA_REMEMBER_SECURE=null
MFA_REMEMBER_HTTP_ONLY=true
MFA_REMEMBER_SAME_SITE=lax
MFA_RECOVERY_ENABLED=true
MFA_RECOVERY_CODES_COUNT=10
MFA_RECOVERY_CODE_LENGTH=10
MFA_RECOVERY_REGENERATE_ON_USE=false
MFA_RECOVERY_HASH_ALGO=sha256
Database
mfa_methods: tracks enabled MFA methods per user; stores encrypted TOTP secretmfa_challenges: stores pending OTP codes for email/sms with expiry and consumed_atmfa_remembered_devices: stores hashed tokens for device recognition with IP, UA, and expirymfa_recovery_codes: stores hashed recovery codes and usage timestampAPI Overview (Facade MFA)
['secret','otpauth_url']['token','cookie']You can extend the built-in Email and SMS channels by configuring custom channel classes:
// config/mfa.php
'email' => [
'enabled' => true,
'channel' => \App\Channels\CustomEmailChannel::class,
'from_address' => '[email protected]',
// ... other config
],
'sms' => [
'enabled' => true,
'channel' => \App\Channels\CustomSmsChannel::class,
'driver' => 'custom',
// ... other config
],
// app/Channels/CustomEmailChannel.php
use CodingLibs\MFA\Channels\EmailChannel;
class CustomEmailChannel extends EmailChannel
{
public function send(Authenticatable $user, string $code, array $options = []): void
{
// Custom sending logic
Mail::to($user->email)->send(new CustomMfaMail($code, $this->config));
}
}
// In a service provider
MFA::registerChannelFromConfig('custom_channel', [
'channel' => CustomChannel::class,
'channel_name' => 'custom_channel',
'custom_setting' => 'value'
]);
Generate challenge codes without automatic delivery:
// Generate challenge without sending
$challenge = MFA::generateChallenge(auth()->user(), 'email');
echo $challenge->code; // Use the code as needed
// Or use issueChallenge with send=false
$challenge = MFA::issueChallenge(auth()->user(), 'email', false);
// Manual sending
$channel = MFA::getChannel('email');
$channel->send(auth()->user(), $challenge->code, ['subject' => 'Custom Subject']);
Steps
CodingLibs\MFA\Contracts\MfaChannel with a unique getName() and a send(...) methodMFA::registerChannel(...)MFA::issueChallenge($user, 'your-channel')use CodingLibs\MFA\Contracts\MfaChannel;
use CodingLibs\MFA\Facades\MFA;
use Illuminate\Contracts\Auth\Authenticatable;
class WhatsAppChannel implements MfaChannel {
public function __construct(private array $config = []) {}
public function getName(): string { return 'whatsapp'; }
public function send(Authenticatable $user, string $code, array $options = []): void {
// send via provider...
}
}
// register at boot
MFA::registerChannel(new WhatsAppChannel(config('mfa.whatsapp', [])));
// then issue
MFA::issueChallenge(auth()->user(), 'whatsapp');
Notes
log. Integrate your provider by implementing a custom channel
or enhancing SmsChannel in your app via service container bindings.secret is stored encrypted by default via Eloquent cast.