<?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),
]
);
}
}
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class VerifyEmail extends Mailable implements ShouldQueue
{
use Queueable, SerializesModels;
public function __construct(public string $verificationUrl)
{
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Confirm your email address',
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.verify-email',
with: ['url' => $this->verificationUrl],
);
}
}
<?php
namespace App\Http\Controllers;
use App\Models\ContactEmail;
use App\Services\EmailVerificationService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Symfony\Component\HttpFoundation\Response;
class EmailVerificationController extends Controller
{
public function confirm(Request $request, ContactEmail $contact, string $hash)
{
if (! hash_equals(sha1($contact->email), (string) $hash)) {
abort(Response::HTTP_FORBIDDEN, 'This verification link is no longer valid.');
}
if ($contact->hasVerifiedEmail()) {
return redirect()
->route('contacts.show', $contact)
->with('status', 'This email is already verified.');
}
$contact->markEmailAsVerified();
return redirect()
->route('contacts.show', $contact)
->with('status', 'Email verified successfully.');
}
public function resend(Request $request, ContactEmail $contact, EmailVerificationService $service)
{
if ($contact->hasVerifiedEmail()) {
return back()->with('status', 'Already verified.');
}
$service->send($contact);
return back()->with('status', 'A new verification link has been sent.');
}
}
<?php
use App\Http\Controllers\EmailVerificationController;
use Illuminate\Support\Facades\Route;
Route::middleware(['web', 'auth'])->group(function () {
Route::get('/email/verify/{contact}/{hash}', [EmailVerificationController::class, 'confirm'])
->middleware(['signed', 'throttle:6,1'])
->name('verification.confirm');
Route::post('/email/verify/{contact}/resend', [EmailVerificationController::class, 'resend'])
->middleware('throttle:3,1')
->name('verification.resend');
});
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
Django URL namespacing and reverse lookups
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
<?php
namespace App\Providers;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
Laravel service container and dependency injection
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
package files
import (
"context"
"time"
Presigned S3 upload URLs (AWS SDK v2)
Share this code
Here's the card — post it anywhere.