php 120 lines · 4 tabs

Signed URL Email Verification in Laravel Without the Built-in MustVerifyEmail Contract

Shared by codesnips Aug 2026
4 tabs
<?php

namespace App\Services;

use App\Mail\VerifyEmail;
use App\Models\ContactEmail;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\URL;

class EmailVerificationService
{
    public function send(ContactEmail $contact): void
    {
        $url = $this->signedUrlFor($contact);

        Mail::to($contact->email)->queue(new VerifyEmail($url));
    }

    public function signedUrlFor(ContactEmail $contact): string
    {
        return URL::temporarySignedRoute(
            'verification.confirm',
            now()->addMinutes(60),
            [
                'contact' => $contact->getKey(),
                'hash' => sha1($contact->email),
            ]
        );
    }
}
4 files · php Explain with highlit

This snippet shows how to build email address verification from scratch in Laravel using temporary signed URLs, instead of leaning entirely on the framework's MustVerifyEmail contract. The pattern is useful when the address being verified is not the login identity — for example a secondary contact email, a billing email, or an email change flow — where the default verified middleware and its assumptions do not fit.

In SendEmailVerification action, the entry point is EmailVerificationService::send. It builds a URL::temporarySignedRoute for the verification.confirm route, embedding the record id and a hash of the current email in the URL. Hashing the email into the signature is the key trick: if the address changes after the link is sent, the old link no longer matches and silently becomes invalid, which closes the window where a stale link could confirm the wrong address. The link expires after 60 minutes via now()->addMinutes(60), and the mail is pushed onto the queue with Mail::to(...)->queue(...) so the HTTP request that triggered it stays fast.

VerifyEmail mailable is a queued Mailable that simply carries the pre-signed $verificationUrl into a Blade view. Because the signing already happened in the service, the mailable holds no secrets and can be serialized safely onto the queue.

EmailVerificationController handles the click. The route is wrapped in Laravel's signed middleware, which rejects any request whose signature or expiry does not validate — so the controller can trust that the URL was minted by the app and has not been tampered with. It still performs a defence-in-depth check with hash_equals comparing the hash segment against the current email, guarding against a replay after the address changed. It is idempotent: an already-verified record short-circuits with a friendly redirect rather than erroring, which matters because users routinely click verification links twice.

The routes/web.php tab wires the named routes and applies signed and throttle middleware. The throttle limits brute-force attempts against the confirm endpoint. Together these files form a small, self-contained verification flow that avoids storing verification tokens in the database at all — the signed URL is the token, and its validity is derived cryptographically from the app key.


Related snips

Share this code

Here's the card — post it anywhere.

Signed URL Email Verification in Laravel Without the Built-in MustVerifyEmail Contract — share card
Link copied