PHPackages                             webteractive/laravel-passwordless - 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. webteractive/laravel-passwordless

ActiveLibrary

webteractive/laravel-passwordless
=================================

A Laravel package providing passwordless authentication strategies (magic link, login code) for Laravel apps.

v0.1.3(yesterday)05↑2900%MITPHPPHP ^8.3CI passing

Since Jul 23Pushed yesterdayCompare

[ Source](https://github.com/webteractive/laravel-passwordless)[ Packagist](https://packagist.org/packages/webteractive/laravel-passwordless)[ Docs](https://github.com/webteractive/laravel-passwordless)[ GitHub Sponsors](https://github.com/Webteractive)[ RSS](/packages/webteractive-laravel-passwordless/feed)WikiDiscussions main Synced today

READMEChangelog (4)Dependencies (13)Versions (5)Used By (0)

Laravel Passwordless
====================

[](#laravel-passwordless)

[![Latest Version on Packagist](https://camo.githubusercontent.com/49cc5b3aa89c1b1b3c7e4d8f02f6d93dd9c799efdc2d437357f4bd7e09325c1f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7765627465726163746976652f6c61726176656c2d70617373776f72646c6573732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/webteractive/laravel-passwordless)[![GitHub Tests Action Status](https://camo.githubusercontent.com/479ccf8b3079d49b90626f53c9de206d8442849de2f2b351a3c34c6ca3ca17d5/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f7765627465726163746976652f6c61726176656c2d70617373776f72646c6573732f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/webteractive/laravel-passwordless/actions?query=workflow%3Arun-tests+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/b6c2f6e2994c8637630ab4d4c61784781df285c7bb300ec8d0a4e12507cc9e23/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7765627465726163746976652f6c61726176656c2d70617373776f72646c6573732e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/webteractive/laravel-passwordless)

Drop-in **passwordless authentication** for Laravel 11, 12, and 13 — **magic links**, **email login codes**, and **social (OAuth) login**. Headless by design: it ships secure JSON endpoints, events, and notifications, and stays out of the way of your frontend. An **optional, opt-in UI kit** is available when you want a login page without building one.

```
POST /auth/login-code          { "email": "ada@example.com" }        → 202 sent
POST /auth/login-code/verify   { "email": "ada@example.com", "code": "123456" }  → 204 (logged in)
```

Features
--------

[](#features)

- ✉️ **Magic link** — signed, single-use, time-limited URL, with optional same-browser enforcement.
- 🔢 **Login code** — short numeric OTP over email (SMS/WhatsApp/etc. via a pluggable channel contract).
- 🪄 **magicCode** — one email with both a magic link *and* a code; sign in with either, first one wins. Opt-in.
- 🌐 **Social login (OAuth)** — Google, GitHub, and any Socialite provider: verified-email account linking, auto-registration, and encrypted token storage. Install the driver + add keys → it works.
- 🚧 **Domain limiting** — restrict which email domains may log in and/or auto-register, per strategy type.
- 🛡️ **Secure by default** — hashing at rest, single-use, enumeration protection, lockout, resend cooldown, and burst throttling — all on out of the box.
- 🔌 **Headless** — JSON endpoints, lifecycle events, a pre-auth gate, and an audit funnel. Bring any frontend.
- 🎨 **Optional UI kit** — publish a ready-made login page for Blade, React, or Vue (standalone or matched to an official starter kit). Nothing is routed unless you opt in.
- 🧪 **Test-friendly** — `Passwordless::fake()` for assertion-only strategy stubs.
- 🔐 **Session or API mode** — Laravel's session guard by default; a Sanctum-style `{ token, user }` in `api_mode`.

Requirements
------------

[](#requirements)

- PHP 8.3+
- Laravel 11.x, 12.x, or 13.x
- MySQL, PostgreSQL, or SQLite

Table of contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [How it works](#how-it-works)
- [Quickstart](#quickstart)
- [Endpoints](#endpoints)
- [HTTP responses](#http-responses)
- [Social login](#social-login)
- [Domain limiting](#domain-limiting)
- [Optional UI kit](#optional-ui-kit)
- [Security defaults](#security-defaults)
- [Configuration](#configuration)
- [Events](#events)
- [Extending](#extending)
- [API mode (Sanctum)](#api-mode-sanctum)
- [Testing](#testing)
- [Operational](#operational)

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

[](#installation)

```
composer require webteractive/laravel-passwordless
```

Publish and run the migrations (`passwordless_challenges` + `passwordless_social_accounts`):

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

Publish the config (optional — sensible defaults ship built-in):

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

Publish translations / mail views to customize them (optional):

```
php artisan vendor:publish --tag="passwordless-translations"
php artisan vendor:publish --tag="passwordless-views"
```

By default the user must already exist (looked up by the `email` column). Set `auto_create_users => true` to create users on first successful sign-in.

How it works
------------

[](#how-it-works)

- **Two tables, `users` untouched.** `passwordless_challenges` holds ephemeral magic-link tokens and login codes (hashed, single-use, TTL-bound — prune with `passwordless:prune`). `passwordless_social_accounts` persists linked OAuth identities (tokens encrypted at rest). Neither touches your `users` table.
- **Routes.** Registered under a configurable prefix (`auth` by default) inside the `web`middleware group, so session login and cookies work out of the box.
- **Two modes.** Session mode (default) logs the user into Laravel's session guard; `api_mode`returns a Sanctum token instead. See [API mode](#api-mode-sanctum).
- **Enable or disable each strategy** independently in config; the UI kit hides affordances for strategies you've turned off.

Quickstart
----------

[](#quickstart)

After [installing](#installation), the endpoints are live. Pick how you want to drive them:

**A. Headless** — call the endpoints from your own frontend (SPA, mobile, or your own Blade):

```
await fetch('/auth/login-code', { method: 'POST', body: JSON.stringify({ email }) });        // → 202
await fetch('/auth/login-code/verify', { method: 'POST', body: JSON.stringify({ email, code }) }); // → 204, logged in
```

**B. With the UI kit** — publish a ready-made login page and wire one route, no frontend work:

```
php artisan vendor:publish --tag=passwordless-ui-livewire   # or -react / -vue, and -embed variants
```

Then add the published example route from `routes/passwordless-ui.php` and visit it. See [Optional UI kit](#optional-ui-kit).

Endpoints
---------

[](#endpoints)

Registered under the `route_prefix` (`auth` by default), inside the `web` middleware group:

MethodURIPurpose`POST``/auth/login-code`request a login code`POST``/auth/login-code/verify`verify a code and sign in`POST``/auth/magic-link`request a magic link`GET``/auth/magic-link/{token}`consume a signed link and sign in`POST``/auth/magic-code`request a combined link + code (magicCode)`GET``/auth/magic-code/{token}`consume the magicCode link and sign in`POST``/auth/magic-code/verify`verify the magicCode code and sign in`GET``/auth/social/{provider}/redirect`start the OAuth flow`GET``/auth/social/{provider}/callback`handle the OAuth callback and sign inRequest endpoints always return `202` whether or not the email exists (enumeration protection). Login codes are numeric **strings**, default length **6** (configurable 6–10) — leading zeros are preserved. Full status codes below.

HTTP responses
--------------

[](#http-responses)

ScenarioStatusBody / headersRequest link/code (known or unknown email)`202``{ "status": "sent" }`Verify success — session mode`204`session cookie setVerify success — `api_mode``200``{ "token": "...", "user": {...} }`Validation error`422``{ "message", "errors": {…} }`Invalid / expired token or code`401``{ "message": "…" }` (deliberately vague)Pre-auth gate denied`403``{ "message": "" }`Resend cooldown active`429``Retry-After`, `{ "message", "retry_after" }`Locked out (max attempts)`423``Retry-After`, `{ "message", "retry_after" }`Social login
------------

[](#social-login)

OAuth sign-in via [Laravel Socialite](https://laravel.com/docs/socialite). The package handles identity storage, verified-email account linking, auto-registration, and encrypted token storage — you just enable a provider and supply keys.

1. Install the driver (Google/GitHub/etc. ship with Socialite; others via `socialiteproviders/*`).
2. Add credentials to `config/services.php` (Socialite's convention): ```
    'google' => [
        'client_id'     => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
        'redirect'      => env('GOOGLE_REDIRECT_URI'),
    ],
    ```
3. Enable it in `config/passwordless.php` (this is a thin enable-list — no secrets here): ```
    'social' => [
        'providers' => [
            'google',
            'github' => ['scopes' => ['read:user']],
        ],
        'auto_register' => true,
    ],
    ```
4. Link a button to the redirect route: ```
    Continue with Google
    ```

**How a user is resolved** on callback: a known `(provider, provider_id)` logs straight in; else, for a **verified** email, it links to an existing user, or auto-registers a new one (when `social.auto_register` is on). Only listed providers get routes — others return `404`. Access/refresh tokens are stored **encrypted**. Fires `SocialAuthenticated` + `UserAuthenticated`.

**Email verification (account-takeover protection).** Linking/registering by email requires proof the email is verified — the provider sends `email_verified: true`, or the provider is on the `social.trusted_providers` allow-list (mainstream providers that only return verified emails). An explicit `email_verified: false` always denies. Unverified → `403`. This prevents an attacker with an unverified address at some provider from taking over an existing account. (Known-identity logins skip this check — identity is already proven.) Override the whole resolution with `resolveSocialUserUsing()` if you need custom verification.

**Custom resolution** — override how a Socialite user maps to an app user (stricter verification, custom fields):

```
use Webteractive\Passwordless\Facades\Passwordless;

Passwordless::resolveSocialUserUsing(function (string $provider, $oauth, $container) {
    // return an app user, or null to deny
    return User::firstOrCreate(['email' => $oauth->getEmail()], ['name' => $oauth->getName()]);
});
```

magicCode (link + code in one email)
------------------------------------

[](#magiccode-link--code-in-one-email)

`magicCode` sends **one** email containing both a magic link and a numeric code. The user authenticates with whichever suits their device — click the link on the same machine, or type the code on a phone. The **first path used wins**; the other is invalidated immediately.

It's **opt-in** (disabled by default) and **email-only**. Enable it:

```
// config/passwordless.php
'strategies' => [
    'magic_code' => [
        'enabled' => true,
        'ttl' => 15 * 60,        // shared TTL for BOTH the link and the code
        'same_browser' => true,  // enforced on the LINK path only
        'code' => ['length' => 6],
    ],
],
```

Flow:

```
POST /auth/magic-code            { email }                 -> 202 (always)
GET  /auth/magic-code/{token}    (signed link click)       -> sign in + redirect
POST /auth/magic-code/verify     { email, code }           -> 204 (sign in)

```

While disabled, all three routes return `404`. The link path enforces same-browser (via the signed cookie) just like `magicLink`; the **code path is intentionally device-agnostic**, so a user can request on desktop and type the code on their phone — that flexibility is the whole point. All the usual protections apply: enumeration-safe send, resend cooldown, per-email lockout on failed code verifies, hashed-at-rest secrets, single-use.

```
use Webteractive\Passwordless\Facades\Passwordless;

Passwordless::magicCode()->send('user@example.com');
```

Domain limiting
---------------

[](#domain-limiting)

Restrict which email domains may authenticate. An empty `allowed` list disables all checks (the default — no behavior change). When set, enforcement is independent **per type** (`passwordless` = magic link + login code, `social`) and **per action** (`login` of existing users, `register` / auto-create):

```
'domains' => [
    'allowed' => ['acme.com'],
    'enforce' => [
        'passwordless' => ['login' => false, 'register' => true],
        'social'       => ['login' => true,  'register' => true],
    ],
],
```

Blocked auto-registration is enumeration-safe (behaves like an unknown email); a blocked login returns `403`.

Optional UI kit
---------------

[](#optional-ui-kit)

The core is strictly headless — **no page routes or views render by default.** When you want a ready-made login page, publish the stub that matches your app. The published files become **yours**to edit; the headless core is never touched.

There are two flavors:

- **Standalone** — a self-contained page (its own layout + `@vite`) for apps with **no auth yet**. Submits to the JSON endpoints with `fetch`.
- **Integrated (`-embed`)** — copies an **official starter kit's** auth layout and components, and drives the flow server-side through a published Fortify-style controller. Best when you already run a starter kit.

TagModeStackSubmissionExtra deps`passwordless-ui-livewire`StandaloneBlade + vanilla JS`fetch`none`passwordless-ui-react`StandaloneInertia + React + TS`fetch`Inertia`passwordless-ui-vue`StandaloneInertia + Vue + TS`fetch`Inertia`passwordless-ui-livewire-embed`IntegratedBlade + Flux + ``server-side redirectLivewire kit`passwordless-ui-react-embed`IntegratedInertia page under `pages/auth/*`server-side redirectReact kit`passwordless-ui-vue-embed`IntegratedInertia page under `pages/auth/*`server-side redirectVue kit```
# Standalone (greenfield)
php artisan vendor:publish --tag=passwordless-ui-livewire
php artisan vendor:publish --tag=passwordless-ui-react
php artisan vendor:publish --tag=passwordless-ui-vue

# Integrated with an official starter kit
php artisan vendor:publish --tag=passwordless-ui-livewire-embed
php artisan vendor:publish --tag=passwordless-ui-react-embed
php artisan vendor:publish --tag=passwordless-ui-vue-embed
```

Every stub is a two-step **email → code** flow (paste-to-fill, auto-submit) with an optional "email me a magic link" affordance, dark mode, and reduced-motion support. Affordances follow the strategies you've enabled in `config/passwordless.php`. Each also publishes a **commented example route** (`routes/passwordless-ui.php`) — the package registers no page route, so you wire it up. The `-embed` route names are `passwordless.*` so they coexist with a starter kit's own `login`.

Every variant is browser-tested end-to-end (email → code → authenticated dashboard) against a real Laravel starter kit.

Security defaults
-----------------

[](#security-defaults)

All on by default:

- **Hashed at rest** — tokens and codes stored as SHA-256, single-use, TTL-bound.
- **Email enumeration protection** — request endpoints respond identically for known and unknown emails.
- **Same-browser enforcement** for magic links — a link only consumes from the browser that requested it. Toggle: `strategies.magic_link.same_browser`.
- **Resend cooldown** — default 30s between requests for the same email (`429` + `Retry-After`).
- **Per-strategy lockout** — after N failed verifies, lock the email/strategy for a window (`423` + `Retry-After`). Default 5 attempts / 15 minutes.
- **Burst throttle** middleware — per-email and per-IP, with separate limits for request vs. verify.

Configuration
-------------

[](#configuration)

Every option is documented inline in `config/passwordless.php`. A brief tour:

```
return [
    'user_model' => App\Models\User::class,
    'user_email_column' => 'email',
    'auto_create_users' => false,

    'guard' => 'web',
    'route_prefix' => 'auth',
    'redirect' => '/',          // where the UI kit sends users after login
    'api_mode' => false,        // return a token instead of a session login

    'resend_cooldown' => 30,
    'lockout' => ['max_attempts' => 5, 'window' => 15 * 60],

    'branding' => [
        'app_name' => env('APP_NAME'),
        'support_email' => null,
    ],

    'strategies' => [
        'magic_link' => ['enabled' => true, 'ttl' => 15 * 60, 'same_browser' => true],
        'login_code' => ['enabled' => true, 'length' => 6, 'ttl' => 10 * 60, 'channel' => 'mail'],
    ],

    'social' => [
        'providers' => ['google', 'github' => ['scopes' => ['read:user']]],
        'auto_register' => true,
        'trusted_providers' => ['google', 'github', 'apple', /* … */], // treated as verified-email
    ],

    'domains' => [
        'allowed' => [],          // empty = unrestricted
        'enforce' => [
            'passwordless' => ['login' => false, 'register' => true],
            'social'       => ['login' => false, 'register' => true],
        ],
    ],
];
```

Events
------

[](#events)

Listen for the full lifecycle (namespace `Webteractive\Passwordless\Events`):

EventFired when`MagicLinkRequested`a magic link is requested`MagicLinkConsumed`a magic link is successfully consumed`LoginCodeRequested`a login code is requested`LoginCodeVerified`a login code is verified`LoginCodeFailed`a login code verification fails`MagicCodeRequested`a magicCode (link + code) is requested`MagicCodeConsumed`a magicCode link is consumed`MagicCodeVerified`a magicCode code is verified`MagicCodeFailed`a magicCode code verification fails`SocialAuthenticated`a social provider authenticates a user (carries provider, registered, linked)`AuthenticationDenied`the pre-auth gate or a domain rule denies (carries the reason)`UserAuthenticated`any strategy authenticates a user (umbrella)Prefer a single hook over subscribing to each? See the [audit funnel](#audit-funnel).

Extending
---------

[](#extending)

### Pre-auth gate

[](#pre-auth-gate)

Run a check after user resolution but before login. Denials return `403` and fire `AuthenticationDenied`.

```
use Webteractive\Passwordless\Facades\Passwordless;

Passwordless::gateUsing(fn ($user, $context) =>
    $user->is_active
        ? Passwordless::allow()
        : Passwordless::deny('account disabled')
);
```

### Audit funnel

[](#audit-funnel)

One hook for every authentication event — handy for a custom audit table.

```
use Webteractive\Passwordless\Support\AuthEvent;

Passwordless::recordUsing(fn (AuthEvent $event) => AuditLog::write($event));
```

### Post-auth redirect

[](#post-auth-redirect)

Customize where server-driven logins land — the social callback and the published embed controllers. The closure receives `($user, $request)` and returns a URL. It is used as the fallback for `redirect()->intended(...)`, so a middleware-set intended URL (e.g. the page a guest was bounced from) still wins; the closure only decides where you land otherwise. When no closure is set, `config('passwordless.redirect')`is used.

```
use Webteractive\Passwordless\Facades\Passwordless;

Passwordless::redirectUsing(fn ($user, $request) =>
    $user->is_admin ? '/admin' : '/dashboard'
);
```

> The headless magic-link and login-code endpoints return `204`/JSON and never redirect, so this hook does not apply to them — your frontend navigates itself.

### Custom login-code channels

[](#custom-login-code-channels)

Email is the built-in channel. Add SMS, WhatsApp, etc. by implementing the contract:

```
use Webteractive\Passwordless\Contracts\LoginCodeChannel;

class SmsChannel implements LoginCodeChannel
{
    public function send(mixed $user, string $email, string $code, array $context = []): void
    {
        // Twilio, Vonage, etc.
    }
}

// Register it in a service provider:
$this->app->bind('passwordless.login_code_channels.sms', SmsChannel::class);
```

```
// config/passwordless.php
'strategies' => [
    'login_code' => ['channel' => 'sms'],
],
```

API mode (Sanctum)
------------------

[](#api-mode-sanctum)

Set `api_mode => true` (or wrap the endpoints in your own controller). Successful verification returns `{ token, user }` instead of logging into the session guard. Your `User` model must use `Laravel\Sanctum\HasApiTokens`. For SPA/mobile clients, register the endpoints via `routes/api.php`.

Testing
-------

[](#testing)

```
use Webteractive\Passwordless\Facades\Passwordless;

it('sends a magic link', function () {
    $fake = Passwordless::fake();

    Passwordless::magicLink()->send('user@example.com');

    $fake->assertLinkSent('user@example.com');
});
```

`fake()` swaps the strategy bindings for assertion-only stubs — no real challenges, no notifications.

Operational
-----------

[](#operational)

```
php artisan passwordless:prune   # delete expired / consumed challenges
```

Schedule it (e.g. in `routes/console.php` or your scheduler):

```
Schedule::command('passwordless:prune')->hourly();
```

Contributing
------------

[](#contributing)

```
composer test      # Pest suite (sqlite in-memory)
composer analyse   # Larastan
composer format    # Pint
```

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md).

Security
--------

[](#security)

Report vulnerabilities privately via GitHub security advisories.

License
-------

[](#license)

MIT — see [LICENSE.md](LICENSE.md).

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance100

Actively maintained with recent releases

Popularity5

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity41

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 94.4% 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 ~0 days

Total

4

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b9cd718c82f37ed135a350effd6b2b7c3a75a815a029bbfccbf8c777d1aa3184?d=identicon)[hadefication](/maintainers/hadefication)

---

Top Contributors

[![hadefication](https://avatars.githubusercontent.com/u/6673244?v=4)](https://github.com/hadefication "hadefication (17 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

laravelotpPasswordlessmagic-linklaravel-passwordlessWebteractive

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/webteractive-laravel-passwordless/health.svg)

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

###  Alternatives

[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k4.8M48](/packages/spatie-laravel-pdf)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

44855.7k](/packages/harris21-laravel-fuse)[vormkracht10/laravel-mails

Laravel Mails can collect everything you might want to track about the mails that has been sent by your Laravel app.

24857.5k](/packages/vormkracht10-laravel-mails)[lettermint/lettermint-laravel

Official Lettermint driver for Laravel

1190.2k1](/packages/lettermint-lettermint-laravel)

PHPackages © 2026

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