Email Magic Link for Laravel is a passwordless authentication package. It signs users in via an emailed link or a one-time code, works standalone or next to Laravel Fortify, and adds no runtime dependencies beyond the framework itself.
Here's what the package gives you:
- Sign-in by link, one-time code, or both, configurable with a single
modesetting - Scanner-safe links — the emailed
GETonly renders a confirmation page; an explicitPOSTconsumes the token - A no-bypass Fortify handoff that routes TOTP users to the two-factor challenge before authenticating them
- Tokens hashed at rest with keyed HMAC-SHA256, single-use consumption, and per-token brute-force lockout
- A mint API for issuing links and codes yourself when you deliver over SMS, push, or a custom mailable
- Resend limiting with escalating cooldowns and a rolling hourly cap
- JSON responses for SPA and mobile clients, plus sign-in against alternate guards like
admin
Links That Email Scanners Can't Consume
The usual problem with magic links is that security appliances and mail clients pre-fetch every URL in a message. If clicking the link is a GET that consumes the single-use token, the scanner burns it before the user ever sees the email.
This package splits the flow in two. The GET at /magic-link/verify/{token} renders a signed confirmation page and changes nothing. The token is only consumed by an explicit POST from that page, which a scanner won't issue.
The same care shows up at rest. Tokens and codes are never persisted in the clear — only a keyed HMAC-SHA256 hash is stored. Consumption uses a race-free conditional claim, request responses are identical whether or not the email exists, and each token has its own brute-force lockout (max_attempts_per_token, default 5).
Two-Factor Handoff to Fortify
If a user has confirmed TOTP through Fortify, verifying their magic link does not log them in. Instead they're routed to Fortify's two-factor challenge, unauthenticated, so there's no path that trades a link click for a bypassed second factor.
For SPA and mobile clients, set api.enabled to true and the endpoints return JSON that tells you which branch you landed on:
{ "authenticated": false, "two_factor": true, "redirect": "<challenge url>" }
Issuing Links and Codes Yourself
The facade issues credentials without sending anything, which is what you want if you deliver over SMS, push, or your own mailable:
use EmailMagicLink\Facades\EmailMagicLink; $link = EmailMagicLink::issueLink($user);$link->url; // Signed confirmation URL$link->expiresAt; // Carbon instance$link->expiresInMinutes; $code = EmailMagicLink::issueCode($user);$code->code;$code->expiresAt;
You can also inject the EmailMagicLink\Contracts\MagicLinkIssuer contract directly. Rebinding MagicLinkAuthenticator lets you control what happens after verification, and implementing the CaptchaGuard contract adds a CAPTCHA to the request form.
Resend Limiting
Passwordless flows invite users to hammer the resend button. The bundled ResendGuard applies escalating cooldowns (30s, then 60s, then 120s) plus a rolling cap of five sends per hour, and you can reuse it in your own controllers:
use EmailMagicLink\Contracts\ResendGuard; public function resend(Request $request): Response{ $decision = $this->guard->attempt('custom-key'); if (! $decision->allowed) { return back()->with('retry_after', $decision->retryAfterSeconds); } // Send mail…}
Expired tokens are cleaned up by a scheduled command:
Schedule::command('email-magic-link:purge')->daily();
Installation
The package requires PHP 8.4 and Laravel 13, with Fortify optional:
composer require pushery/email-magic-link-for-laravelphp artisan email-magic-link:installphp artisan migrate
You can find installation instructions and full documentation on GitHub.