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 appendedsignaturequery parameter derived from the route, parameters, and yourAPP_KEY.URL::temporarySignedRoute(): Same as above but bakes in an expiry.signedmiddleware: 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
phpuse 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:
phpRoute::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:
phpRoute::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
- 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.
- Forgetting the expiry —
signedRoute()never expires. PrefertemporarySignedRoute()for anything sensitive.
Best Practices
- Keep windows short — 30 minutes is generous for an unsubscribe link, 5 minutes for a one-time login link.
- 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 withAPP_KEY.URL::temporarySignedRoute()adds an expiry.- The
signedmiddleware rejects invalid or expired requests automatically. - Use them for unsubscribe links, magic logins, and short-lived download tokens.
Code Examples
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');