php 147 lines · 4 tabs

Signed Email-Verification Links with Custom Laravel Notification and Signed Routes

Shared by codesnips Jul 2026
4 tabs
<?php

namespace App\Models;

use App\Notifications\VerifyEmailNotification;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable implements MustVerifyEmail
{
    use Notifiable;

    protected $fillable = ['name', 'email', 'password'];

    protected $hidden = ['password', 'remember_token'];

    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];

    public function hasVerifiedEmail()
    {
        return ! is_null($this->email_verified_at);
    }

    public function markEmailAsVerified()
    {
        return $this->forceFill([
            'email_verified_at' => $this->freshTimestamp(),
        ])->save();
    }

    public function sendEmailVerificationNotification()
    {
        $this->notify(new VerifyEmailNotification());
    }
}
4 files · php Explain with highlit

This snippet shows how Laravel implements a self-contained email-verification flow using its built-in signed URL machinery instead of storing a separate verification token. The core idea is that a signed URL carries a cryptographic signature query parameter derived from the route parameters and the application key, so a link can be validated later without persisting anything server-side. A temporary signed URL adds an expires timestamp to that signature, giving the link a natural expiry that cannot be tampered with.

In User model, the class implements the framework's MustVerifyEmail contract via the MustVerifyEmail trait, which supplies hasVerifiedEmail, markEmailAsVerified, and sendEmailVerificationNotification. The last method is overridden to dispatch the custom VerifyEmailNotification rather than the default one, letting the notification control the link's appearance and lifetime.

In VerifyEmailNotification, verificationUrl calls URL::temporarySignedRoute with the route name verification.verify, an expiry drawn from config, and two parameters: the user id and a hash computed as sha1 of the email address. Hashing the email means the link stays valid only while the address is unchanged — if the user changes their email before clicking, the hash no longer matches and verification is refused. The signed route embeds both expires and signature, so any modification to the id, hash, or timestamp invalidates the link.

In EmailVerificationController, the verify method is guarded by the signed middleware, which rejects requests whose signature does not validate before the controller even runs. Inside, it compares the route's hash against a freshly computed hash of the current email using hash_equals to avoid timing leaks, guards against already-verified users, and calls markEmailAsVerified, firing the Verified event. The notice and resend methods round out the flow for prompting and re-sending.

In routes/web.php, the routes wire these pieces together: verification.verify carries the signed and throttle middleware, while verification.send is throttled separately. The trade-off of this approach is statelessness — no token table to clean up — at the cost of links breaking if the app key rotates. It is the idiomatic choice whenever verification state can be recomputed from existing user data.


Related snips

Share this code

Here's the card — post it anywhere.

Signed Email-Verification Links with Custom Laravel Notification and Signed Routes — share card
Link copied