
September 07, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
You need to give a client a download link that works for 48 hours without creating an account. You need an invoice URL that expires after payment. You need a document-review link that cannot be guessed or altered. Laravel signed URLs for temporary access solve exactly that: cryptographically signed routes that expire on a schedule and fail closed if someone tampers with the query string. On production Laravel API and portal projects, I treat signed URLs as the default pattern whenever access is narrow, time-bound, and does not warrant a full session. This guide covers generation, validation, middleware, security, and the patterns I use on legal-tech and eCommerce builds running Laravel 12 or 13 on PHP 8.3+.
expires timestamp and signature hash. Use URL::temporarySignedRoute() to generate them and the signed middleware (or hasValidSignature) to validate. Invalid or expired links return HTTP 403.What Are Laravel Signed URLs and When Should You Use Them?
A signed URL is a normal named route with two extra query parameters: expires (Unix timestamp) and signature (HMAC hash of the URL using your APP_KEY). Laravel recomputes the hash on every request. If the path, query string, or expiration changes by even one character, validation fails.
Use signed URLs when the access scope is:
- Single action — download one file, approve one quote, view one invoice.
- Time-limited — link should die after hours or days, not live forever.
- Delivered out-of-band — sent by email, SMS, or chat where the user may not be logged in.
- Low friction — forcing registration for a one-time PDF is bad UX on client portals.
Do not use signed URLs as a substitute for proper authorization on sensitive admin actions. They are shareable by design. Anyone who receives the link gets access until expiry. For ongoing access, use Laravel policies, gates, and session auth instead.
On a legal-tech portal I built, signed URLs power time-limited document downloads for clients who receive files by email but do not maintain daily logins. The same pattern appears on client portal projects with document sharing where the business rule is: access this one file for seven days, then the link dies.
How Do You Generate Laravel Signed URLs for Temporary Access?
Laravel exposes two helpers on the URL facade. For time-limited links — the usual case — use temporarySignedRoute. For links that never expire until you rotate APP_KEY, use signedRoute.
Step 1: Define a named route
Every signed URL targets a named route. Anonymous routes work poorly because you cannot regenerate the same URL reliably from notifications or queued jobs.
// routes/web.php
use App\Http\Controllers\DocumentDownloadController;
Route::get('/documents/{document}/download', [DocumentDownloadController::class, 'show'])
->name('documents.download')
->middleware('signed'); Step 2: Generate the signed URL
use Illuminate\Support\Facades\URL;
use App\Models\Document;
$document = Document::findOrFail($id);
$url = URL::temporarySignedRoute(
'documents.download',
now()->addHours(48),
['document' => $document->id]
);
// Example output:
// https://example.com/documents/42/download?expires=1757200000&signature=abc123... The expiration argument accepts any DateTimeInterface or DateInterval. Passing now()->addDays(7) is common for client deliverables. Passing now()->addMinutes(15) suits one-time payment confirmations.
Step 3: Attach middleware on the route
Laravel 11 and 12 register the signed alias automatically. It maps to ValidateSignature middleware, which calls $request->hasValidSignature() internally. If validation fails, the user gets a 403 response — no redirect, no soft failure.
// Alternative: validate manually inside a controller
public function show(Request $request, Document $document)
{
if (! $request->hasValidSignature()) {
abort(403, 'This link has expired or is invalid.');
}
return Storage::download($document->path, $document->filename);
} Step 4: Send the URL from mail or notifications
// app/Notifications/DocumentReadyNotification.php
public function toMail(object $notifiable): MailMessage
{
$url = URL::temporarySignedRoute(
'documents.download',
now()->addDays(3),
['document' => $this->document->id]
);
return (new MailMessage)
->subject('Your document is ready')
->action('Download Document', $url);
} If you queue the notification, generate the URL inside toMail() or the job handler — not hours earlier in a controller — so the expiration clock starts close to delivery. Stale URLs generated at dispatch time but delivered late confuse clients.
How Do You Validate and Customize Signed URL Behavior in Laravel?
Validation is symmetric: Laravel strips the signature parameter, recomputes the HMAC over the remaining URL including expires, and compares with hash_equals() to prevent timing attacks. If expires is in the past, validation fails regardless of signature correctness.
Ignoring specific query parameters
Marketing teams append UTM parameters after the link is sent. Those break the signature unless you whitelist them. Pass ignored parameter names to the middleware:
Route::get('/reports/{report}', [ReportController::class, 'show'])
->name('reports.show')
->middleware('signed:utm_source,utm_medium,utm_campaign'); Or call hasValidSignatureWhileIgnoring() in controller code. Only ignore parameters that do not affect authorization logic.
Relative vs absolute URLs
// Relative path only — useful in SPA or API contexts
URL::temporarySignedRoute('documents.download', now()->addHour(), [
'document' => 1,
], absolute: false);
// Force a specific domain — useful behind load balancers
URL::temporarySignedRoute('documents.download', now()->addHour(), [
'document' => 1,
], absolute: true); Behind reverse proxies, ensure APP_URL matches the public-facing domain. Mismatched scheme or host between generation and validation is a common production bug. I have seen signed links work in staging but fail in production because APP_URL still pointed at an internal hostname. Fix trust proxies in bootstrap/app.php and set APP_URL to the canonical HTTPS domain.
Custom responses for expired links
Publish a friendly Blade view instead of the default 403. Override the exception handler or use a dedicated route that catches InvalidSignatureException:
// bootstrap/app.php (Laravel 11+)
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(function (InvalidSignatureException $e, Request $request) {
return response()->view('errors.link-expired', [], 403);
});
}) On client-facing portals, a clear "This link expired — request a new one" message reduces support tickets compared to a bare forbidden page.
Testing signed URLs
// tests/Feature/DocumentDownloadTest.php
public function test_valid_signed_url_allows_download(): void
{
$document = Document::factory()->create();
$url = URL::temporarySignedRoute(
'documents.download',
now()->addHour(),
['document' => $document->id]
);
$this->get($url)->assertOk();
}
public function test_expired_signed_url_returns_403(): void
{
$document = Document::factory()->create();
$url = URL::temporarySignedRoute(
'documents.download',
now()->subMinute(),
['document' => $document->id]
);
$this->get($url)->assertForbidden();
} Use Laravel's withoutMiddleware sparingly in tests — validating the full signed middleware stack catches real integration bugs. For time-dependent tests, Carbon::setTestNow() keeps expiration logic deterministic.
What Are the Best Production Use Cases for Laravel Signed URLs?
Signed URLs fit narrow, time-boxed workflows. These are the patterns I reach for most often on production Laravel 12 and 13 applications.
- Document and media downloads — legal contracts, notarized scans, export CSVs. Pair with storage authorization inside the controller: verify the signed URL, then confirm the file belongs to the route parameter ID.
- Email verification and magic links — Laravel's built-in email verification already uses signed URLs internally. Custom onboarding flows can follow the same model.
- Invoice and receipt viewing — eCommerce clients on Laravel eCommerce platforms often need a shareable order summary link that does not expose the full account dashboard.
- Unsubscribe and preference links — one-click unsubscribe from newsletters without login, with a 30-day expiry window.
- Webhook-style callbacks to browsers — payment gateway return URLs where you want tamper resistance on status pages shown to the customer.
- Admin approval links — "Approve this refund" sent to a manager's phone. Keep the TTL short — 15 to 60 minutes — and log every use.
On notary service portals, I use 72-hour signed download links for completed document packages. The business rule is explicit: after three days, the client must log in or request a reissue. That balances convenience against the risk of forwarded links sitting in inboxes indefinitely.
For API-only systems, consider whether Sanctum tokens or Passport fit better. Signed URLs excel at browser GET actions. They are awkward for JSON POST APIs where you need request bodies and rate limiting per user.
| Approach | Best for | Expiry | Shareable risk | Revocable before expiry |
|---|---|---|---|---|
| Signed URL | One-time downloads, email links | Built-in via expires | High — anyone with link has access | No — unless you add server-side blocklist |
| Sanctum API token | Mobile apps, SPAs, REST clients | Configurable or none | Medium — token in header | Yes — delete token record |
| Session auth | Logged-in dashboards | Session lifetime | Low — cookie-bound | Yes — logout invalidates |
| Opaque DB token | Password reset, invite codes | Per-row expires_at | High — same as signed URL | Yes — delete or mark used |
Many teams combine patterns: a signed URL lands the user on a page, then optional login unlocks broader access. That is fine — just do not assume the signed URL alone proves identity. It proves possession of the link.
How Do You Secure Laravel Signed URLs Against Common Mistakes?
Signed URLs are safe when implemented with clear threat assumptions. They are not magic access control.
Protect APP_KEY like a root password
The signature derives from APP_KEY. Anyone with your key can forge URLs for any route. Never commit .env to Git, rotate the key if leaked, and understand that rotation invalidates every outstanding signed link instantly. Document that for clients before emergency key rotation during an incident.
Keep TTL as short as business rules allow
A 30-day signed URL for a sensitive contract is a 30-day window for forwarding, scraping, and inbox compromise. Default to hours or days. Use the password generator tool mindset here: shorter exposure windows beat clever cryptography every time.
Authorize inside the controller, not only on the signature
The signature proves the URL was issued by your app. It does not prove the recipient should access resource ID 42. Always load the model and apply business rules:
public function download(Request $request, Document $document)
{
// Middleware already validated signature
if ($document->is_revoked) {
abort(410, 'This document is no longer available.');
}
if ($document->download_count >= $document->max_downloads) {
abort(403, 'Download limit reached.');
}
$document->increment('download_count');
return Storage::download($document->path);
} Log access for audit trails
On legal-tech and financial workflows, log IP, user agent, and timestamp for every signed URL hit. Spatie Activity Log or a simple access_logs table works. Auditors ask who opened a contract link and when — signed URLs make that harder unless you instrument it.
Do not put secrets in query strings beyond signature
The entire query string participates in signing, but URLs appear in server logs, browser history, and Referer headers. Never append API keys or PII as extra query params. Route parameters and opaque IDs only.
HTTPS only in production
Sign URLs with HTTPS domains. Mixed content or HTTP downgrade exposes links in transit. Pair with HSTS on production servers — a topic covered in Linux system administration for Laravel hosting.
The official Laravel documentation on URL generation and signed URLs is the authoritative reference for method signatures. For HMAC background, the RFC 2104 HMAC specification explains why keyed hashing prevents tampering without revealing the secret.
How Do Signed URLs Fit Into Broader Laravel Architecture?
Signed URLs sit at the edge of your authorization model — not at the center. A sensible Laravel 13 application on PHP 8.3 still uses policies for resource ownership, Sanctum for API consumers, and queues for delivery.
When building REST APIs with Laravel, expose signed URLs in JSON responses for actions that must open in a browser — for example, "download_url": "https://..." with a 15-minute TTL. Document the expiry in your OpenAPI spec so mobile clients refresh links before presenting them. See API documentation with Scribe for keeping those contracts accurate.
For payment flows, signed return URLs complement gateway callbacks. The gateway webhook confirms payment server-to-server; the signed browser URL shows a receipt without exposing admin routes. Patterns overlap with Laravel payment integrations for Khalti, Stripe, and eSewa.
Deploy through your normal pipeline — signed URL logic has no special CI requirements beyond keeping APP_KEY consistent across release directories. On Deployer-style zero-downtime deploys, the shared .env persists the key while code rotates. Mismatched keys between web and queue workers break notifications silently; verify workers read the same environment as PHP-FPM.
If you are modernizing a legacy app, signed URLs are a low-risk incremental win. You can add a download route with middleware today without rewriting the entire auth stack — a pragmatic approach aligned with modern Laravel architecture principles.
Key Takeaways
- Generate time-limited links with
URL::temporarySignedRoute()and protect routes using thesignedmiddleware alias. - Treat signed URLs as shareable credentials — keep TTL short and add controller-level checks for revocation and download limits.
- Generate URLs at send time inside notifications or jobs, not at queue dispatch, so expiration matches delivery.
- Set
APP_URLto your public HTTPS domain and configure trusted proxies so signatures validate behind load balancers. - Use signed URLs for one-off GET actions; use Sanctum tokens or sessions for ongoing dashboard and API access.
- Log every signed URL access on sensitive document workflows so you have an audit trail when clients ask who viewed a file.
People Also Ask
What is the difference between signedRoute and temporarySignedRoute in Laravel?
URL::signedRoute() creates a URL with a valid signature but no expiration parameter — it works until you change APP_KEY. URL::temporarySignedRoute() adds an expires timestamp and is the right choice for temporary access. Always prefer the temporary variant unless you have a specific reason to issue non-expiring links.
What happens when a Laravel signed URL expires?
The signed middleware calls hasValidSignature(), which checks the current time against the expires query parameter. If the timestamp is in the past, validation fails and Laravel throws InvalidSignatureException, returning HTTP 403 by default. Customize the response in your exception handler for a better user experience.
Can Laravel signed URLs be used for POST requests?
Signed URLs are designed for GET requests where the full URL including query string is signed. POST bodies are not part of the signature. For state-changing actions from email links, use a signed GET that displays a confirmation form, then POST with a CSRF token — or use a one-time opaque token stored in the database instead.
Do signed URLs work with Laravel queues and Horizon?
Yes. Generate the URL inside the queued job or notification at execution time, not when the job is serialized. Ensure queue workers share the same APP_KEY and APP_URL as the web process. After APP_KEY rotation, flush pending jobs that embed old signed URLs or regenerate them before retry.
Ship Temporary Access Without Cutting Security Corners
Laravel signed URLs for temporary access give you a built-in, battle-tested pattern for time-limited links without bolting on a separate token service. They work on Laravel 12 and 13 with minimal code: a named route, one facade call, one middleware alias. The engineering discipline is in the edges — short TTLs, controller authorization, audit logging, and honest communication that forwarded links are outside your control.
If you are building a client portal, document workflow, or eCommerce order system that needs secure temporary links, I can help architect and implement it on your stack. See custom software development services or contact us to discuss your project. For related reading, explore building RESTful APIs with Laravel, Laravel Livewire for interactive portals, and CI/CD pipelines for Laravel deployments.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

