spykapps / passwordless-login
A highly customizable, multilingual magic link (passwordless) authentication package for Laravel with bot detection, rate limiting, and comprehensive event system.
Requires
- php: ^8.1
- illuminate/contracts: ^10.0|^11.0|^12.0
- illuminate/database: ^10.0|^11.0|^12.0
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/mail: ^10.0|^11.0|^12.0
- illuminate/notifications: ^10.0|^11.0|^12.0
- illuminate/routing: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
README
Passwordless Login for Laravel
A highly customizable, multilingual magic link authentication package for Laravel with bot/prefetch detection, rate limiting, conditional auth, and a comprehensive event system.
Features
- 🔗 Magic Link Authentication : Secure, token-based passwordless login
- 🤖 Bot/Prefetch Detection : Detects Outlook, Apple Mail, SafeLinks and other prefetch scanners that consume one-time links
- 🌍 Multilingual : Full i18n support with publishable language files
- 🔒 Configurable Token Security : Token length, hashing algorithm (SHA-256, bcrypt, argon2), IP/UA binding
- 🔄 Usage Control : One-time, multi-use, or unlimited use links
- 🚦 Rate Limiting : Built-in throttling per user
- 📧 Built-in Email Notification : Laravel-style notification (like password reset) with queuing support
- 📋 Conditional Authentication : Allow login only when custom conditions are met (e.g.
is_active,!is_banned) - 🎯 After Login Actions : Run custom code after authentication
- 📡 Comprehensive Events : 8 events covering the full lifecycle
- ⚙️ Everything Configurable : Guard, model, table, routes, expiry, views, redirect, and more
- 🧹 Auto Cleanup : Scheduled cleanup of expired tokens
Requirements
- PHP 8.1+
- Laravel 10, 11, or 12
Installation
composer require spykapps/passwordless-login
Publish Config
php artisan vendor:publish --tag=passwordless-login-config
Publish & Run Migrations
php artisan vendor:publish --tag=passwordless-login-migrations php artisan migrate
Publish Everything (optional)
php artisan vendor:publish --tag=passwordless-login
This publishes config, migrations, views, and language files.
Quick Start
1. Add the Trait to Your User Model
use SpykApp\PasswordlessLogin\Traits\HasMagicLogin; class User extends Authenticatable { use HasMagicLogin; }
2. Send a Magic Link
use SpykApp\PasswordlessLogin\Facades\PasswordlessLogin; // Simple — generates link and sends email automatically $user = User::where('email', $request->email)->first(); $result = PasswordlessLogin::forUser($user)->generate($request); // $result['url'] → the magic link URL // $result['token'] → the MagicLoginToken model // Or use the trait $result = $user->sendMagicLink($request);
3. That's It!
The package automatically:
- Registers the magic login route
- Handles bot/prefetch detection
- Authenticates the user
- Redirects to your configured URL
Usage Examples
Basic Usage
use SpykApp\PasswordlessLogin\Facades\PasswordlessLogin; // In a controller public function sendLink(Request $request) { $request->validate(['email' => 'required|email']); $user = User::where('email', $request->email)->first(); if (!$user) { // Don't reveal if user exists (security best practice) return back()->with('status', __('passwordless-login::messages.link_sent_if_exists')); } try { PasswordlessLogin::forUser($user)->generate($request); } catch (\SpykApp\PasswordlessLogin\Exceptions\ThrottleException $e) { return back()->with('error', $e->getMessage()); } return back()->with('status', __('passwordless-login::messages.link_sent')); }
Fluent Builder (Full Customization)
$result = PasswordlessLogin::forUser($user) ->guard('admin') // Custom guard ->redirectTo('/admin/dashboard') // Custom redirect ->expiresIn(60) // 60 minutes expiry ->maxUses(3) // Usable 3 times ->remember() // Remember the session ->tokenLength(64) // 128-char hex token ->withMetadata(['source' => 'api', 'ip' => $request->ip()]) ->withoutNotification() // Don't send email (handle yourself) ->generate($request); // Send the link your own way Mail::to($user)->send(new MyCustomMail($result['url']));
Using the Trait
$user->sendMagicLink($request, [ 'guard' => 'admin', 'redirect_url' => '/admin', 'expiry_minutes' => 30, 'max_uses' => 1, 'remember' => true, 'metadata' => ['reason' => 'password_reset'], ]);
Get URL Without Sending Email
$result = PasswordlessLogin::forUser($user) ->withoutNotification() ->generate($request); $magicUrl = $result['url']; // Use in SMS, WhatsApp, API response, etc.
Possible ways to generate URLs
// 1. Generate + auto-send email (most common) $result = PasswordlessLogin::forUser($user)->generate($request); // 2. Same thing via trait (identical to above) $result = $user->sendMagicLink($request); // 3. Generate only — NO email sent $result = PasswordlessLogin::forUser($user) ->withoutNotification() ->generate($request); // 4. Same via trait — NO email sent $result = $user->generateMagicLink($request, [ 'send_notification' => false, ]); // 5. Generate without email, send your own way $result = PasswordlessLogin::forUser($user) ->withoutNotification() ->generate($request); Mail::to($user)->send(new YourCustomMail($result['url'])); // or SMS, WhatsApp, etc. // 6. Generate + send with custom notification class $result = PasswordlessLogin::forUser($user) ->useNotification(\App\Notifications\MyMagicLink::class) ->generate($request); // 7. Generate + send with custom mailable class $result = PasswordlessLogin::forUser($user) ->useMailable(\App\Mail\MyMagicLinkMail::class) ->generate($request); // 8. Full fluent example — no email $result = PasswordlessLogin::forUser($user) ->guard('admin') ->redirectTo('/admin/dashboard') ->expiresIn(60) ->maxUses(3) ->remember() ->tokenLength(64) ->withMetadata(['source' => 'api']) ->withoutNotification() ->generate($request); $magicUrl = $result['url']; $tokenModel = $result['token'];
Configuration
All options in config/passwordless-login.php:
| Option | Default | Description |
|---|---|---|
user_model |
App\Models\User |
The authenticatable model |
email_column |
email |
Column used to find users by email |
guard |
null (default) |
Authentication guard |
remember |
false |
Remember me flag |
token.length |
32 |
Token byte length (16–128) |
token.hash_algorithm |
sha256 |
sha256, bcrypt, or argon2 |
expiry_minutes |
15 |
Minutes until link expires |
max_uses |
1 |
Max times a link can be used (null = unlimited) |
route.path |
/magic-login/{token} |
The magic link route path |
route.name |
passwordless.login |
Route name |
route.middleware |
['web', 'guest'] |
Route middleware |
route.prefix |
'' |
Route prefix |
redirect.on_success |
/dashboard |
Redirect after login |
redirect.on_failure |
/login |
Redirect on failure |
throttle.enabled |
true |
Rate limiting |
throttle.max_attempts |
5 |
Max links per decay period |
throttle.decay_minutes |
10 |
Rate limit window |
bot_detection.enabled |
true |
Bot/prefetch detection |
bot_detection.strategy |
both |
confirmation_page, javascript, or both |
notification.enabled |
true |
Auto-send email |
notification.queue |
false |
Queue the notification |
notification.class |
built-in | Custom notification class |
notification.mailable |
null |
Use a Mailable instead |
conditions |
[] |
Callables/classes that must return true |
after_login_action |
null |
Action to run after login |
table |
passwordless_login_tokens |
Database table name |
security.invalidate_previous |
true |
Invalidate old tokens on new generate |
security.invalidate_on_login |
true |
Invalidate all tokens after login |
security.ip_binding |
false |
Bind link to requester's IP |
security.user_agent_binding |
false |
Bind link to requester's UA |
security.audit_log |
true |
Log all activity |
Bot/Prefetch Detection
Email clients like Outlook, Apple Mail, and security scanners like SafeLinks and Barracuda often visit links before the user clicks them. This can consume one-time magic links.
How It Works
The package uses a multi-layered detection approach:
- User-Agent Detection — Matches known bot/scanner patterns
- HTTP Method Detection — Bots often use HEAD/OPTIONS requests
- Prefetch Header Detection — Checks
X-Purpose,Sec-Purpose,Sec-Fetch-Destheaders - Suspicious Header Analysis — Flags requests without browser-typical headers
Strategies
// config/passwordless-login.php 'bot_detection' => [ 'enabled' => true, 'strategy' => 'both', // Options: 'confirmation_page', 'javascript', 'both' ],
confirmation_page— Shows a "Click to continue" button (most compatible)javascript— Auto-redirects via JS (bots can't execute JS)both— JS auto-redirect with a button fallback (recommended)
Conditional Authentication
Restrict who can log in with custom conditions:
// config/passwordless-login.php 'conditions' => [ // Closures fn($user) => $user->is_active, fn($user) => !$user->is_banned, fn($user) => $user->email_verified_at !== null, // Classes implementing LoginCondition \App\Auth\CheckSubscription::class, ],
Custom Condition Class
use SpykApp\PasswordlessLogin\Contracts\LoginCondition; use Illuminate\Contracts\Auth\Authenticatable; class CheckSubscription implements LoginCondition { public function check(Authenticatable $user): bool { return $user->hasActiveSubscription(); } public function message(): string { return 'Your subscription has expired.'; } }
After Login Actions
Run custom code after successful authentication:
// config/passwordless-login.php 'after_login_action' => \App\Actions\UpdateLastLogin::class,
use SpykApp\PasswordlessLogin\Contracts\AfterLoginAction; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Http\Request; class UpdateLastLogin implements AfterLoginAction { public function execute(Authenticatable $user, Request $request): void { $user->update([ 'last_login_at' => now(), 'last_login_ip' => $request->ip(), ]); } }
Events
| Event | Dispatched When | Payload |
|---|---|---|
MagicLinkGenerated |
Link is created | $user, $token, $url, $ipAddress |
MagicLinkSent |
Notification/email sent | $user, $channel |
MagicLinkClicked |
Link URL is visited | $tokenModel, $request, $isBotDetected |
MagicLinkAuthenticated |
User successfully logged in | $user, $request, $guard |
MagicLinkFailed |
Authentication failed | $reason, $request, $token, $ipAddress |
MagicLinkExpired |
Expired link accessed | $tokenModel, $request |
MagicLinkUsed |
Token use count incremented | $tokenModel, $request |
MagicLinkThrottled |
Rate limit exceeded | $user, $availableInSeconds |
BotDetected |
Bot/prefetch detected | $request, $reason, $token |
Listening to Events
// EventServiceProvider or listener use SpykApp\PasswordlessLogin\Events\MagicLinkAuthenticated; use SpykApp\PasswordlessLogin\Events\MagicLinkFailed; Event::listen(MagicLinkAuthenticated::class, function ($event) { Log::info("User {$event->user->email} logged in via magic link"); }); Event::listen(MagicLinkFailed::class, function ($event) { Log::warning("Magic link failed: {$event->reason} from {$event->ipAddress}"); });
Multilingual Support
Publish the language files:
php artisan vendor:publish --tag=passwordless-login-lang
This creates lang/vendor/passwordless-login/en/messages.php. Add translations by creating new locale folders (e.g. es/messages.php, fr/messages.php, de/messages.php).
Example: Spanish Translation
// lang/vendor/passwordless-login/es/messages.php return [ 'email_subject' => 'Tu enlace de inicio de sesión', 'email_greeting' => '¡Hola!', 'email_intro' => 'Recibimos una solicitud de inicio de sesión para tu cuenta.', 'email_action' => 'Iniciar Sesión', 'email_expiry_notice' => 'Este enlace expirará en :minutes minutos.', 'email_outro' => 'Si no solicitaste este enlace, no es necesario realizar ninguna acción.', // ... etc ];
Custom Notification / Mailable
Custom Notification
// config/passwordless-login.php 'notification' => [ 'class' => \App\Notifications\CustomMagicLink::class, ],
Your notification receives: $url, $expiryMinutes, $metadata.
Custom Mailable
// config/passwordless-login.php 'notification' => [ 'mailable' => \App\Mail\CustomMagicLinkMail::class, ],
Your mailable receives: $url, $expiryMinutes in the constructor.
Custom Views
Publish and customize views:
php artisan vendor:publish --tag=passwordless-login-views
Published to resources/views/vendor/passwordless-login/:
confirmation.blade.php— Bot detection confirmation pageemails/magic-link.blade.php— Email template (markdown)
API / JSON Support
The controller automatically returns JSON when Accept: application/json is sent:
Success:
{
"message": "You have been logged in successfully.",
"redirect": "/dashboard",
"user": { "id": 1, "name": "..." }
}
Failure:
{
"message": "This login link has expired.",
"error": true
}
Security Best Practices
- Don't reveal user existence — Use
link_sent_if_existsmessage - Keep expiry short — 15 minutes is a good default
- Use one-time links — Set
max_usesto 1 - Enable
invalidate_previous— Only the latest link works - Enable
invalidate_on_login— All links consumed after login - Consider IP binding for high-security applications
Credits
License
The MIT License (MIT). Please see License File for more information.
