Passwordless Login: Magic Links, Passkeys, and How to Add Both Without Breaking Accounts

Swap passwords for something people can actually use. How magic links and passkeys work in a Laravel app, the failure modes each one has, and why every passwordless flow still needs a recovery path.

Passwords are the part of a web app everybody hates for reasons that are all justified: people reuse them, forget them, reset them through links that expire, and support staff spend real hours on all three. Passwordless login removes the thing nobody liked, but it replaces it with new failure modes that are easy to miss in a demo and painful in production. Both major options, magic links and passkeys, are practical to ship in Laravel today. The difference is what breaks when they do.

A magic link is a signed, expiring URL that logs in whoever opens it. The flow is short: user enters an email, the app sends a one-time link, the user clicks it, the app validates the signature and starts a session. With Laravel's built-in tools the core is roughly this:

// route that issues the login email
Route::post('/login/link', function (Request $request) {
    $request->validate(['email' => 'required|email']);

    if ($user = User::where('email', $request->email)->first()) {
        $url = URL::temporarySignedRoute('login.via-link', now()->addMinutes(15));

        Mail::to($user->email)->send(new LoginLink($url));
    }

    // same response either way, so the form can't probe for real accounts
    return back()->with('status', 'If the address belongs to an account, a link is on its way.');
});

Three details decide whether this behaves in production:

  • Single use. The signature proves the URL came from you, not that it hasn't been used. Store a used-mark in cache keyed by the link, and reject replays.
  • Rate limit the request route. throttle:5,1 on the endpoint that sends the email, or your login form becomes a spam cannon pointed at anyone's inbox.
  • Prefetchers will click your links. Corporate mail scanners and aggressive clients follow URLs automatically, can burn a single-use link before the human gets to it. The standard fix is a landing page: the link opens a page with a "Continue to your dashboard" button, and only the human click logs in.

The cost of magic links is deliverability and friction. Every login needs an email that arrives in seconds, which means your sending domain reputation is now load-bearing on your sign-in page. That's a debt worth knowing about before you commit.

Passkeys: login stored in the device

Passkeys move authentication to WebAuthn: a key pair generated on the user's device, where the private key never leaves it. Login is a browser prompt, fingerprint or PIN, signed by the device and verified on your server. Phishing stops working against passkeys in a way it never stopped working against passwords, because the browser binds the signature to the domain.

On the server side you need a WebAuthn library or a packaged implementation; for Laravel, Spatie maintains a passkeys package that handles credential registration and assertion. The moving parts you own regardless of tooling:

  • Registration flow: an already-authenticated user creates a passkey against their account. Passkey-first login requires the passkey to exist first.
  • Session hygiene: on login, call session()->regenerate() the same as you would after a password login. Passwordless doesn't change session fixation rules.
  • Sudo mode: editing billing details or an email address should ask for a fresh passkey assertion rather than an active session.

The failure mode is recovery. Keys live on devices, and devices get lost, replaced, and factory-reset. A passkey-only account with no fallback is an account the user can permanently lose, so every passkey setup needs at least one of: a secondary passkey, a traditional password retained as backup, or recovery codes generated at registration. This is not optional polish. If accounts have real value, skip shipping passkeys until the recovery path is designed.

Which to pick, and how they stack

Magic links are simpler to ship and work on any device with an inbox, but they inherit email's speed and its scanners. Passkeys are faster for the user and safer against phishing, but they need a device interaction and a solid recovery story. They're not rivals. A common shape is passkeys as the primary sign-in, a magic link as the recovery and cross-device path, and the password gone entirely. If you already run Laravel's normal password login, adding passkeys alongside it first is the lowest-risk move: existing users keep working, new users opt in, and you learn the failure modes before making anything mandatory.

The part that stays true for both: the login route is your app's front door, so rate limit it, don't leak whether an account exists, and test the failure paths, expired link, reused link, lost device, before launch, not after a support ticket does it for you.