PHPackages                             spykapps/passwordless-login - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. spykapps/passwordless-login

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

spykapps/passwordless-login
===========================

A highly customizable, multilingual magic link (passwordless) authentication package for Laravel with bot detection, rate limiting, and comprehensive event system.

v1.0.1(1mo ago)0248↑700%11MITPHPPHP ^8.1

Since Feb 24Pushed 1mo agoCompare

[ Source](https://github.com/SpykApp/passwordless-login)[ Packagist](https://packagist.org/packages/spykapps/passwordless-login)[ Docs](https://github.com/spykapps/passwordless-login)[ RSS](/packages/spykapps-passwordless-login/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (2)Dependencies (18)Versions (3)Used By (1)

[![Screenshot](/art/pl.jpeg)](/art/pl.jpeg)

 [ ![Packagist Version](https://camo.githubusercontent.com/372dc00aa178100479a0aaeabad62b79c710718392d428f32b17ab75e6d4b13f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7370796b617070732f70617373776f72646c6573732d6c6f67696e2e7376673f7374796c653d666f722d7468652d6261646765) ](https://packagist.org/packages/spykapps/passwordless-login) [ ![Total Downloads](https://camo.githubusercontent.com/f87ae1828a2a0d84789f75a25952fd3987d269af1538ed3369f1c66ae792a24a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7370796b617070732f70617373776f72646c6573732d6c6f67696e2e7376673f7374796c653d666f722d7468652d6261646765) ](https://packagist.org/packages/spykapps/passwordless-login) [![Laravel 13](https://camo.githubusercontent.com/912dc8f051cbb50fe34e91773952abac9b43ff027bc748c393fc44fcf8ea8a42/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c61726176656c2d31332e782d4646324432303f7374796c653d666f722d7468652d6261646765266c6f676f3d6c61726176656c)](https://laravel.com/docs/13.x) [![PHP 8.3](https://camo.githubusercontent.com/c373bf0b203812c0ee6eaa9d496d61fe5eeec0d2748786f6b95adc35c4dccdd1/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d382e332d3737374242343f7374796c653d666f722d7468652d6261646765266c6f676f3d706870)](https://php.net) [ ![License](https://camo.githubusercontent.com/608c8dfda488178950ce502d7697514db3a6a712579327ed90b9b594260f6355/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d626c75652e7376673f7374796c653d666f722d7468652d6261646765) ](https://github.com/spykapps/passwordless-login/blob/main/LICENSE.md)

Passwordless Login for Laravel
==============================

[](#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
--------

[](#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
------------

[](#requirements)

- PHP 8.1+
- Laravel 10, 11, 12, or 13

Installation
------------

[](#installation)

```
composer require spykapps/passwordless-login
```

### Publish Config

[](#publish-config)

```
php artisan vendor:publish --tag=passwordless-login-config
```

### Publish &amp; Run Migrations

[](#publish--run-migrations)

```
php artisan vendor:publish --tag=passwordless-login-migrations
php artisan migrate
```

### Publish Everything (optional)

[](#publish-everything-optional)

```
php artisan vendor:publish --tag=passwordless-login
```

This publishes config, migrations, views, and language files.

Quick Start
-----------

[](#quick-start)

### 1. Add the Trait to Your User Model

[](#1-add-the-trait-to-your-user-model)

```
use SpykApp\PasswordlessLogin\Traits\HasMagicLogin;

class User extends Authenticatable
{
    use HasMagicLogin;
}
```

### 2. Send a Magic Link

[](#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!

[](#3-thats-it)

The package automatically:

- Registers the magic login route
- Handles bot/prefetch detection
- Authenticates the user
- Redirects to your configured URL

Usage Examples
--------------

[](#usage-examples)

### Basic Usage

[](#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)

[](#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

[](#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

[](#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

[](#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
-------------

[](#configuration)

All options in `config/passwordless-login.php`:

OptionDefaultDescription`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-inCustom 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 activityBot/Prefetch Detection
----------------------

[](#botprefetch-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

[](#how-it-works)

The package uses a multi-layered detection approach:

1. **User-Agent Detection** — Matches known bot/scanner patterns
2. **HTTP Method Detection** — Bots often use HEAD/OPTIONS requests
3. **Prefetch Header Detection** — Checks `X-Purpose`, `Sec-Purpose`, `Sec-Fetch-Dest` headers
4. **Suspicious Header Analysis** — Flags requests without browser-typical headers

### Strategies

[](#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
--------------------------

[](#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

[](#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
-------------------

[](#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
------

[](#events)

EventDispatched WhenPayload`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

[](#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
--------------------

[](#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

[](#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--mailable)

### Custom Notification

[](#custom-notification)

```
// config/passwordless-login.php
'notification' => [
    'class' => \App\Notifications\CustomMagicLink::class,
],
```

Your notification receives: `$url`, `$expiryMinutes`, `$metadata`.

### Custom Mailable

[](#custom-mailable)

```
// config/passwordless-login.php
'notification' => [
    'mailable' => \App\Mail\CustomMagicLinkMail::class,
],
```

Your mailable receives: `$url`, `$expiryMinutes` in the constructor.

Custom Views
------------

[](#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 page
- `emails/magic-link.blade.php` — Email template (markdown)

API / JSON Support
------------------

[](#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
-----------------------

[](#security-best-practices)

1. **Don't reveal user existence** — Use `link_sent_if_exists` message
2. **Keep expiry short** — 15 minutes is a good default
3. **Use one-time links** — Set `max_uses` to 1
4. **Enable `invalidate_previous`** — Only the latest link works
5. **Enable `invalidate_on_login`** — All links consumed after login
6. **Consider IP binding** for high-security applications

Credits
-------

[](#credits)

- [Sanchit Patil](https://github.com/sanchitspatil)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

43

—

FairBetter than 91% of packages

Maintenance89

Actively maintained with recent releases

Popularity18

Limited adoption so far

Community11

Small or concentrated contributor base

Maturity44

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 83.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~27 days

Total

2

Last Release

55d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/619d85ca59369ebee5c640beac15488b61f6d80e4cbb170d82ea422e665341bd?d=identicon)[sanchitpatil](/maintainers/sanchitpatil)

---

Top Contributors

[![sanchitspatil](https://avatars.githubusercontent.com/u/106558065?v=4)](https://github.com/sanchitspatil "sanchitspatil (5 commits)")[![angeloo](https://avatars.githubusercontent.com/u/1065919?v=4)](https://github.com/angeloo "angeloo (1 commits)")

---

Tags

laravelAuthenticationloginPasswordlessmagic-loginmagic-linkpasswordless-login

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/spykapps-passwordless-login/health.svg)

```
[![Health](https://phpackages.com/badges/spykapps-passwordless-login/health.svg)](https://phpackages.com/packages/spykapps-passwordless-login)
```

###  Alternatives

[tymon/jwt-auth

JSON Web Token Authentication for Laravel and Lumen

11.5k49.1M350](/packages/tymon-jwt-auth)[laravel/cashier

Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.

2.5k25.9M107](/packages/laravel-cashier)[php-open-source-saver/jwt-auth

JSON Web Token Authentication for Laravel and Lumen

8359.8M53](/packages/php-open-source-saver-jwt-auth)[yadahan/laravel-authentication-log

Laravel Authentication Log provides authentication logger and notification for Laravel.

416632.8k5](/packages/yadahan-laravel-authentication-log)[laragear/two-factor

On-premises 2FA Authentication for out-of-the-box.

339785.3k8](/packages/laragear-two-factor)[alajusticia/laravel-logins

Session management in Laravel apps, user notifications on new access, support for multiple separate remember tokens, IP geolocation, User-Agent parser

2011.0k](/packages/alajusticia-laravel-logins)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
