spatie/laravel-one-time-passwords
Securely generate and consume one-time passwords in Laravel. Ships with notifications (email by default, extensible to SMS/other channels) and a ready-to-use Livewire login component. Optionally enhances the OTP input UI automatically when Flux is installed.
Installation:
composer require spatie/laravel-one-time-passwords
Publish the config file:
php artisan vendor:publish --provider="Spatie\OneTimePassword\OneTimePasswordServiceProvider"
Configuration:
.env for OTP settings (e.g., OTP_EXPIRATION_MINUTES=5).config/otp.php file to define:
model (default: Spatie\OneTimePassword\Models\OneTimePassword).table_name (default: one_time_passwords).expiration (default: 5 minutes).max_attempts (default: 5).enforce_same_origin (default: false) to control cross-origin OTP handling.First Use Case: Generate and send an OTP to a user via email/SMS:
use Spatie\OneTimePassword\OneTimePassword;
$otp = OneTimePassword::createForUser($user);
Mail::to($user->email)->send(new SendOtpEmail($otp));
config/otp.php for tweaking behavior, including the new enforce_same_origin setting.app/Models/OneTimePassword.php (if extending the default model).Spatie\OneTimePassword\Events\OneTimePasswordGenerated for custom logic on OTP creation.$otp = OneTimePassword::createForUser($user); // Generates a 6-digit code
$otp = OneTimePassword::createForUser($user, codeLength: 8);
Mail or Notifiable facade to dispatch the OTP:
$user->notify(new SendOtpNotification($otp->code));
if (OneTimePassword::verify($user, $code)) {
// OTP is valid; proceed with authentication.
}
$otp = OneTimePassword::findValidForUser($user);
if ($otp && $otp->code === $code) {
// Custom validation (e.g., check IP, device fingerprint).
}
OTP Middleware: Create middleware to enforce OTP verification before login:
namespace App\Http\Middleware;
use Closure;
use Spatie\OneTimePassword\OneTimePassword;
class VerifyOtp
{
public function handle($request, Closure $next)
{
if (!$request->user() || !OneTimePassword::verify($request->user(), $request->input('otp'))) {
return redirect()->route('login.otp');
}
return $next($request);
}
}
Register in app/Http/Kernel.php:
protected $routeMiddleware = [
'otp.verify' => \App\Http\Middleware\VerifyOtp::class,
];
OTP Login Route:
Route::post('/login/otp', function (Request $request) {
$request->validate(['otp' => 'required|string']);
if (OneTimePassword::verify($request->user(), $request->otp)) {
Auth::login($request->user());
return redirect()->intended('/dashboard');
}
return back()->withErrors(['otp' => 'Invalid code.']);
});
max_attempts (default: 5) per OTP. Customize in config/otp.php:
'max_attempts' => 3, // Reduce for stricter security.
OneTimePassword model to track failed attempts:
public function markAttempt($user)
{
$this->failed_attempts++;
$this->save();
if ($this->failed_attempts >= config('otp.max_attempts')) {
$this->markAsUsed(); // Lock the OTP.
}
}
enforce_same_origin in config/otp.php to restrict OTP verification to the same origin:
'enforce_same_origin' => env('OTP_ENFORCE_SAME_ORIGIN', true),
This prevents cross-origin OTP verification attempts, improving security for SPAs or multi-origin apps.OneTimePassword::fake() to mock OTPs in tests:
use Spatie\OneTimePassword\Testing\FakesOneTimePassword;
public function test_otp_verification()
{
$this->actingAs($user);
OneTimePassword::fake(['123456']); // Fake a valid OTP.
$response = $this->post('/login/otp', ['otp' => '123456']);
$response->assertRedirect('/dashboard');
}
Override the default storage (e.g., Redis) by binding a custom repository:
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->bind(
\Spatie\OneTimePassword\Repositories\OneTimePasswordRepository::class,
\App\Repositories\CustomOtpRepository::class
);
}
Combine with Laravel’s built-in auth:
// After email/password login, require OTP.
if (Auth::check() && !session()->has('otp_verified')) {
return redirect()->route('verify.otp');
}
Localize OTP messages by extending the notification:
// app/Notifications/SendOtpNotification.php
public function toMail($notifiable)
{
return (new MailMessage)
->subject(__('OTP Verification'))
->line(__('Your OTP code is :code', ['code' => $this->code]));
}
Log OTP events (e.g., generation, verification) using Laravel’s logging:
// In a service or controller.
\Log::info('OTP generated for user', ['user_id' => $user->id, 'code' => $otp->code]);
If using enforce_same_origin = true, ensure your frontend sends OTP verification requests from the same origin. For SPAs, this may require:
if (!OneTimePassword::verify($user, $code)) {
$otp = OneTimePassword::findValidForUser($user);
if (!$otp) {
return back()->withErrors(['otp' => __('OTP has expired.')]);
}
return back()->withErrors(['otp' => __('Invalid code.')]);
}
DB::transaction(function () use ($user, $code) {
$otp = OneTimePassword::findValidForUser($user);
if ($otp && $otp->code === $code) {
$otp->markAsUsed();
// Proceed with auth.
How can I help you explore Laravel packages today?