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 mode setting
  • Scanner-safe links — the emailed GET only renders a confirmation page; an explicit POST consumes 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-laravel

php artisan email-magic-link:install

php artisan migrate

You can find installation instructions and full documentation on GitHub.