Introduction

Signed URLs are URLs Laravel cryptographically signs with an expiry and a set of parameters. They let you expose a route to a specific recipient without requiring them to log in — perfect for unsubscribe links, magic login links, and download tokens.

Key Concepts

  • URL::signedRoute(): Generates a URL with an appended signature query parameter derived from the route, parameters, and your APP_KEY.
  • URL::temporarySignedRoute(): Same as above but bakes in an expiry.
  • signed middleware: Verifies the signature on incoming requests and aborts with 403 if it is tampered with or expired.

Real World Context

Email unsubscribe links are the classic use case: you need the link to work from an email client (no session) but you also need to prove the click is legitimate. Signed URLs give you both without storing one-time tokens in a database table.

Deep Dive

Generating a Signed URL

php
use Illuminate\Support\Facades\URL;

// Permanent signed URL (valid until APP_KEY changes)
$url = URL::signedRoute('unsubscribe', ['user' => $user->id]);

// Expires after 30 minutes
$url = URL::temporarySignedRoute(
    'unsubscribe',
    now()->addMinutes(30),
    ['user' => $user->id]
);

Laravel appends a ?signature=... (and ?expires=... for temporary URLs) that includes a hash of the route name, parameters, and any query string.

Validating a Signed URL

Attach the signed middleware to the route:

php
Route::get('/unsubscribe/{user}', [UnsubscribeController::class, 'handle'])
    ->name('unsubscribe')
    ->middleware('signed');

If the signature is missing, mismatched, or expired, the middleware aborts with a 403 before your controller runs. You can also check manually: $request->hasValidSignature() returns a boolean.

Ignoring Query Parameters

By default, signatures include every query parameter. To allow additional tracking parameters without breaking the signature, pass an array to the middleware:

php
Route::get('/unsubscribe/{user}', [UnsubscribeController::class, 'handle'])
    ->name('unsubscribe')
    ->middleware('signed:relative');  // relative signature, ignores host

// Or in hasValidSignature:
if ($request->hasValidSignatureWhileIgnoring(['utm_source', 'utm_campaign'])) {
    // ...
}

Common Pitfalls

  1. Storing the link somewhere durable — Signed URLs are fine in emails but do not embed them in database records or public logs; they grant access to whoever sees them.
  2. Forgetting the expiry — signedRoute() never expires. Prefer temporarySignedRoute() for anything sensitive.

Best Practices

  1. Keep windows short — 30 minutes is generous for an unsubscribe link, 5 minutes for a one-time login link.
  2. Scope the URL with parameters — Bind the URL to a user ID so tampering with the path invalidates the signature.

Summary

  • URL::signedRoute() creates tamper-proof URLs signed with APP_KEY.
  • URL::temporarySignedRoute() adds an expiry.
  • The signed middleware rejects invalid or expired requests automatically.
  • Use them for unsubscribe links, magic logins, and short-lived download tokens.

Code Examples

php
use Illuminate\Support\Facades\URL;

// Generate a 30-minute magic-login link
$url = URL::temporarySignedRoute(
    'login.magic',
    now()->addMinutes(30),
    ['user' => $user->id]
);

// The protected route
Route::get('/magic-login/{user}', function (User $user) {
    auth()->login($user);
    return redirect('/dashboard');
})->name('login.magic')->middleware('signed');
✓ Completed