Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Passwordless Login with Magic Links

By Kokil Thapa | Last reviewed: September 2026

Users forget passwords. Support tickets pile up. Reset flows break on mobile. Laravel passwordless login with magic links solves this by emailing a one-time, time-limited URL that logs the user in after a single click. On client portals and booking apps I have shipped with custom Laravel software, this pattern cut login friction without weakening security when tokens are signed, hashed, and short-lived. This guide walks through a complete implementation you can drop into Laravel 13 on PHP 8.3 or higher.

Magic-link authentication replaces the password step with email verification. The user submits an email address. Your app creates a random token, stores a hash of it, and sends a link. When the user opens the link, Laravel validates the token, marks it used, and logs them in.

This is not the same as "Login with Google." OAuth delegates identity to a third party. Magic links keep auth inside your app and your mail pipeline. You control expiry, rate limits, and audit logs.

Common use cases include:

Magic Link Login FlowUserEnters emailLaravelCreates tokenMail QueueSends linkInboxUser clicksVerify RouteHash + expiry checkSession StartAuth::login()Security Layer: rate limits, HTTPS, single-use tokensInvalidate old tokens on each new request
End-to-end Laravel passwordless login with magic links from email submission to authenticated session

The pattern fits well alongside Sanctum for API tokens. Web users get magic links. Mobile or SPA clients can still use token-based auth on separate routes.

Start with a migration for login tokens. Never store the raw token in the database. Hash it with SHA-256, the same approach Laravel uses for remember tokens.

Step 1: Create the login_tokens table

php artisan make:migration create_login_tokens_table

public function up(): void
{
    Schema::create('login_tokens', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->cascadeOnDelete();
        $table->string('token_hash', 64)->unique();
        $table->timestamp('expires_at');
        $table->timestamp('used_at')->nullable();
        $table->string('ip_address', 45)->nullable();
        $table->timestamps();

        $table->index(['user_id', 'expires_at']);
    });
}

Step 2: Add a LoginToken model

namespace App\Models;

class LoginToken extends Model
{
    protected $fillable = [
        'user_id', 'token_hash', 'expires_at', 'used_at', 'ip_address',
    ];

    protected function casts(): array
    {
        return [
            'expires_at' => 'datetime',
            'used_at' => 'datetime',
        ];
    }

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function isValid(): bool
    {
        return is_null($this->used_at) && $this->expires_at->isFuture();
    }
}

Step 3: Create a service to issue tokens

namespace App\Services;

use App\Models\LoginToken;
use App\Models\User;
use Illuminate\Support\Str;

class MagicLinkService
{
    public function createForUser(User $user, ?string $ip = null): string
    {
        LoginToken::where('user_id', $user->id)
            ->whereNull('used_at')
            ->delete();

        $plainToken = Str::random(64);

        LoginToken::create([
            'user_id' => $user->id,
            'token_hash' => hash('sha256', $plainToken),
            'expires_at' => now()->addMinutes(15),
            'ip_address' => $ip,
        ]);

        return $plainToken;
    }

    public function verify(string $plainToken): ?User
    {
        $record = LoginToken::where('token_hash', hash('sha256', $plainToken))
            ->first();

        if (! $record || ! $record->isValid()) {
            return null;
        }

        $record->update(['used_at' => now()]);

        return $record->user;
    }
}

Deleting unused tokens before creating a new one enforces one active link per user. That reduces the window if someone requests multiple emails.

Step 4: Wire routes and controllers

// routes/web.php
Route::middleware('guest')->group(function () {
    Route::get('/login/magic', [MagicLinkController::class, 'create'])
        ->name('login.magic.create');
    Route::post('/login/magic', [MagicLinkController::class, 'store'])
        ->middleware('throttle:5,1')
        ->name('login.magic.store');
    Route::get('/login/magic/{token}', [MagicLinkController::class, 'verify'])
        ->middleware('signed')
        ->name('login.magic.verify');
});
public function store(MagicLinkRequest $request, MagicLinkService $service): RedirectResponse
{
    $user = User::where('email', $request->validated('email'))->first();

    if ($user) {
        $token = $service->createForUser($user, $request->ip());
        $user->notify(new MagicLinkNotification($token));
    }

    return back()->with('status', 'If that email exists, we sent a login link.');
}

Always return the same message whether the email exists or not. This prevents account enumeration. The same rule applies to password reset flows in the Laravel password reset documentation.

Step 5: Send the notification

namespace App\Notifications;

class MagicLinkNotification extends Notification
{
    public function __construct(private string $token) {}

    public function via(object $notifiable): array
    {
        return ['mail'];
    }

    public function toMail(object $notifiable): MailMessage
    {
        $url = URL::temporarySignedRoute(
            'login.magic.verify',
            now()->addMinutes(15),
            ['token' => $this->token]
        );

        return (new MailMessage)
            ->subject('Your login link')
            ->line('Click below to sign in. This link expires in 15 minutes.')
            ->action('Sign in', $url)
            ->line('If you did not request this, ignore this email.');
    }
}

Queue the notification in production. Use ShouldQueue on the notification class and a working queue driver. I have seen magic-link login fail silently because mail sat in the sync driver under load on a shared server.

Token Security ControlsStorage RulesStore SHA-256 hash onlyNever save plain token in DBLifetime Rules15-minute expiry defaultMark used_at on first clickRequest Limitsthrottle:5,1 on POST routeSame response for all emailsToken RotationDelete old unused tokensOne active link per userSigned URL wraps token in transitHTTPS required in production always
Security layers every Laravel passwordless login with magic links implementation should enforce

Pick the auth method based on user behaviour and threat model. Magic links trade password fatigue for email dependency.

MethodUser frictionSecurity notesBest fit
PasswordMedium — must remember credentialsWeak passwords, reuse, phishingHigh-frequency daily users
Magic linkLow — one click from emailEmail account becomes the gate; link interception riskInfrequent portal access, B2B clients
OAuth (Google, etc.)Low if user has provider accountThird-party dependency, privacy concernsConsumer apps, broad audiences
Passkeys / WebAuthnLow after setupStrong crypto; browser support variesSecurity-first apps with modern browsers

For a legal-tech portal where clients upload documents once a month, magic links often beat forcing another password. For a daily admin dashboard, passwords plus optional 2FA may still win. Read Laravel policies and gates to lock down what happens after login regardless of method.

Hybrid setups work well. Offer magic link as primary and keep password login behind a "Use password instead" link. Laravel 12 and 13 still ship full password auth out of the box, so you are adding a path rather than replacing the stack.

Magic links fail in production when teams treat them like shareable URLs. They are credentials. Handle them accordingly.

Rate limiting and abuse prevention

Apply throttle middleware on the request endpoint. Five attempts per minute per IP is a sensible starting point. Also throttle the verify route to slow brute-force guessing of token values.

Route::get('/login/magic/{token}', [MagicLinkController::class, 'verify'])
    ->middleware(['signed', 'throttle:10,1'])
    ->name('login.magic.verify');

HTTPS and signed URLs

Never send magic links over HTTP in production. Configure APP_URL with https://. Laravel signed routes add a tamper-proof signature. If someone edits the token parameter, verification fails before your controller runs.

The Laravel signed URL docs explain how expiry is embedded in the signature. Match your signed-route TTL to your database token expiry.

Session fixation and regeneration

After a successful verify, regenerate the session ID. Laravel does this automatically when you call Auth::login($user) if session regeneration is enabled in your auth config.

public function verify(string $token, MagicLinkService $service): RedirectResponse
{
    $user = $service->verify($token);

    if (! $user) {
        return redirect()->route('login.magic.create')
            ->withErrors(['token' => 'This login link is invalid or expired.']);
    }

    Auth::login($user, remember: true);

    return redirect()->intended(route('dashboard'));
}

Logging and monitoring

Log magic-link requests without storing the plain token. Record user ID, IP, and timestamp. Alert on spikes. Pair this with the same monitoring you use for database transaction failures and queue backlogs.

Secure vs Insecure PatternsAvoidPlain token in databaseNo expiry timestampReusable linksDifferent error messagesHTTP links in productionNo rate limitingDo ThisSHA-256 hashed storage15-minute TTL enforcedSingle-use with used_atGeneric success messageSigned HTTPS URLsthrottle middleware
Insecure versus production-safe patterns for Laravel magic link authentication

Local development with Mailpit or Mailhog catches template bugs early. Production needs queue workers, SPF/DKIM records, and a rollback plan.

Feature tests

public function test_magic_link_logs_user_in(): void
{
    Notification::fake();

    $user = User::factory()->create();

    $this->post(route('login.magic.store'), ['email' => $user->email])
        ->assertSessionHas('status');

    Notification::assertSentTo($user, MagicLinkNotification::class);
}

Use factories as described in Laravel seeders vs factories. Extract the token in a notification test by capturing the mailable URL, then hit the verify route and assert authentication.

Mail deliverability

Magic-link login lives or dies on email delivery. Configure SPF, DKIM, and DMARC on your sending domain. Use a transactional provider rather than the host's default SMTP when possible. Costs run roughly Rs 1,500–5,000/month (~USD 11–37) for modest volume on most providers.

Deployment checklist

  1. Set QUEUE_CONNECTION=database or Redis and run a queue worker via Supervisor
  2. Confirm APP_URL matches your production domain with HTTPS
  3. Run migrations for login_tokens before switching traffic
  4. Add a scheduled command to prune expired tokens older than 48 hours
  5. Reload PHP-FPM after deploy so opcache picks up changes, as covered in GitLab CI deploy pipelines for Laravel
// routes/console.php
Schedule::command('model:prune', [
    '--model' => [LoginToken::class],
])->daily();

Add use Illuminate\Database\Eloquent\Prunable; to the model with a prunable() query scoped to expired records. This keeps the table small on high-traffic apps.

Blade login form

Keep the UI simple. One email field beats a cluttered auth page. Generate strong internal tokens with a password generator tool during development, but never expose that pattern to end users for login.

Production Deploy PathGit PushCI TestsMigrateQueue UpMail DNSSPF DKIM DMARCHTTPSAPP_URL checkMonitorLogs + alertsPost-deploy: send test magic linkVerify login on staging before prod cutover
Deployment checklist for Laravel passwordless login with magic links in production

For ongoing reliability, pair this with Laravel support and maintenance so queue workers and mail DNS stay healthy after launch.

Key Takeaways

  • Hash tokens with SHA-256 in the database; never store or log the plain value.
  • Combine signed URLs with a 15-minute expiry and single-use used_at column.
  • Return identical messages for existing and non-existing emails to block enumeration.
  • Queue mail notifications and run throttle middleware on both request and verify routes.
  • Regenerate sessions on login and prune expired tokens on a daily schedule.
  • Magic links suit infrequent-access portals; keep password or OAuth as fallback options.

People Also Ask

Yes, when tokens are hashed, single-use, time-limited, and sent over HTTPS inside signed URLs. The email inbox becomes the authentication factor, so encourage users to protect email with its own 2FA. Follow guidance from the OWASP forgot-password cheat sheet for enumeration and rate-limit rules.

Absolutely. Add magic-link routes under the guest middleware without removing Fortify or Breeze password routes. Many apps show email-first login and hide password auth behind a secondary link.

What Laravel version do you need for this pattern?

Laravel 12 or 13 on PHP 8.2 or higher works. Laravel 13 requires PHP 8.3 minimum. Signed URLs, notifications, and session auth are core features — no extra package is required unless you want a pre-built solution.

Fifteen minutes is the industry default and a good balance. Support-heavy portals can stretch to 30 minutes. Never exceed one hour; longer windows increase risk if email is forwarded or left open on shared devices.

Ship Passwordless Login Without Cutting Corners

Laravel passwordless login with magic links is a small amount of code with a large UX payoff when you enforce hashing, expiry, rate limits, and queued mail. Start with the migration and service class above, add feature tests, then validate deliverability on staging before you touch production traffic. If you want this wired into a client portal, booking flow, or enterprise Laravel application, contact us — or browse the portfolio for portals that already run on similar auth patterns. For broader architecture context, see modern Laravel architecture best practices and building RESTful APIs with Laravel when your app serves both web and mobile clients.

Frequently Asked Questions

It replaces the password step with email verification. The user submits an email, your app creates a random token, stores a hash, sends a one-time URL, and logs them in after click validation—no password field required.

Your app generates a signed, expiring token tied to the user, emails a URL containing it, and verifies the token on click before starting a session. The plain token never lives in the database—only a SHA-256 hash does. Laravel signed routes add a tamper-proof signature, and a used_at column marks the link single-use after successful login.

Yes, when tokens are hashed with SHA-256, single-use, time-limited to 15 minutes, sent over HTTPS inside signed URLs, and protected by rate limiting on both request and verify routes. The email inbox becomes the authentication factor, so users should protect email with its own 2FA. Follow OWASP forgot-password guidance for enumeration and rate-limit rules.

Start with a login_tokens migration storing token_hash, expires_at, used_at, and ip_address. Add a LoginToken model with an isValid method, a MagicLinkService to create and verify tokens, guest middleware routes for create/store/verify, a MagicLinkNotification using URL::temporarySignedRoute, and a Form Request for email validation. Hash tokens before storage and delete unused tokens before issuing new ones.

Fifteen minutes is the industry default and what this implementation uses. Support-heavy portals can stretch to 30 minutes. Never exceed one hour.

Absolutely. Add magic-link routes under guest middleware without removing Fortify or Breeze password routes. Many apps show email-first login and hide password auth behind a secondary link. Laravel 12 and 13 still ship full password auth out of the box, so you are adding a path rather than replacing the stack.

Magic links trade password fatigue for email dependency—low user friction but the inbox becomes the gate. Passwords suit high-frequency daily users but suffer weak credentials and reuse. OAuth delegates identity to a third party like Google. Magic links keep auth inside your app and mail pipeline, fitting infrequent portal access and B2B clients better than consumer OAuth flows.

Laravel 12 or 13 on PHP 8.2 or higher. Laravel 13 requires PHP 8.3 minimum. No extra package is required.

Never. Hash it with SHA-256—the same approach Laravel uses for remember tokens—and store only the 64-character hash in login_tokens.token_hash. Never log or expose the plain token anywhere. If someone edits the token parameter in the URL, Laravel signed route verification fails before your controller runs.

Always return the same message whether the email exists or not: "If that email exists, we sent a login link." This applies on the store endpoint after email submission. The same rule applies to Laravel password reset flows. Without this, attackers can probe which addresses have accounts on your portal.

Apply throttle middleware on both endpoints. Five attempts per minute per IP on the POST request route is a sensible starting point. Also throttle the verify route—ten attempts per minute per IP—to slow brute-force guessing of 64-character token values. Pair this with logging user ID, IP, and timestamp without storing plain tokens.

Magic-link login lives or dies on email delivery. Without queuing, mail can sit in the sync driver under load on a shared server and fail silently. Add ShouldQueue to MagicLinkNotification, set QUEUE_CONNECTION to database or Redis, and run a queue worker via Supervisor. I have seen production magic-link login break because of this exact misconfiguration.

Costs run roughly Rs 1,500–5,000 per month (~USD 11–37) for modest volume on most transactional providers. Configure SPF, DKIM, and DMARC on your sending domain rather than relying on your host default SMTP. Use Mailpit or Mailhog locally to catch template bugs before production.

Magic links fit client portals where users log in rarely—law-firm document portals, lead follow-up flows, internal admin tools for small teams, and booking confirmation return paths. For daily admin dashboards, passwords plus optional 2FA may still win. Hybrid setups offering magic link as primary with password behind a fallback link work well on real client projects.

Write feature tests with Notification::fake, assert MagicLinkNotification is sent, extract the signed URL from the mailable, hit verify, and assert authentication. Before production, run migrations for login_tokens, confirm APP_URL uses HTTPS, queue workers are running, and schedule daily model:prune on LoginToken for records older than 48 hours. Reload PHP-FPM after deploy so opcache picks up changes. Validate deliverability on staging before switching traffic.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: