Passwordless Sign-In with Fortify Two-Factor Support in Laravel
The Email Magic Link for Laravel package (pushery/email-magic-link-for-laravel) brings passwordless authentication to Laravel 13 applications. Users sign in via an emailed link or a one-time code, and the package integrates cleanly alongside Laravel Fortify without adding any runtime dependencies beyond the framework itself.
Scanner-Safe Magic Links
The classic magic-link problem is that security appliances pre-fetch every URL in an email, burning the single-use token before the user ever clicks it. This package solves that with a two-step flow:
- A
GETto/magic-link/verify/{token}renders a signed confirmation page — it changes nothing. - An explicit
POSTfrom that page consumes the token — something an email scanner will never issue.
Tokens are never stored in the clear. Only a keyed HMAC-SHA256 hash is persisted, consumption uses a race-free conditional claim, and each token has its own brute-force lockout (max_attempts_per_token, default 5). Responses are identical whether or not the email address exists, preventing user enumeration.
Fortify Two-Factor Handoff
If a user has confirmed TOTP through Fortify, verifying their magic link does not log them in directly. Instead, they are routed to Fortify's two-factor challenge while still unauthenticated — there is no path that trades a link click for a bypassed second factor.
For SPA and mobile clients, set api.enabled to true and the endpoint returns a JSON response indicating which branch was taken:
{ "authenticated": false, "two_factor": true, "redirect": "<challenge url>" }
Issuing Links and Codes Yourself
The EmailMagicLink facade lets you mint credentials without sending anything, which is useful when delivering over SMS, push notifications, or a custom 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 controls post-verification behaviour, and implementing CaptchaGuard adds a CAPTCHA to the confirmation form.
Resend Limiting
Passwordless flows invite users to hammer the resend button. The bundled ResendGuard applies escalating cooldowns (30 s, 60 s, 120 s) plus a rolling cap of five sends per hour:
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. Fortify is optional.
composer require pushery/email-magic-link-for-laravel
php artisan email-magic-link:install
php artisan migrate
Key Takeaways
- Two-step link flow prevents email scanners from consuming single-use tokens.
- HMAC-SHA256 hashed tokens with per-token brute-force lockout protect at rest.
- Fortify TOTP handoff ensures magic links cannot bypass a configured second factor.
- Mint API lets you issue links or codes for SMS, push, or custom mailables.
- ResendGuard applies escalating cooldowns and a rolling hourly send cap.
- JSON API mode supports SPA and mobile clients, including alternate guards.
Source: Passwordless Sign-In with Fortify Two-Factor Support in Laravel — Laravel News