spatie/laravel-url-signer
Sign and validate any URL in Laravel with an expiring signature. Works across apps, uses a configurable secret (not the app key), and includes middleware to protect routes. Generate time-limited links in one call and verify them anywhere.
Installation:
composer require spatie/laravel-url-signer
Publish the config (optional):
php artisan vendor:publish --provider="Spatie\UrlSigner\UrlSignerServiceProvider"
First Use Case: Sign a URL with a 30-day expiry:
use Spatie\UrlSigner\Laravel\Facades\UrlSigner;
$signedUrl = UrlSigner::sign('https://example.com/protected', now()->addDays(30));
Output:
https://example.com/protected?expires=1735689600&signature=abc123...
Validation:
$isValid = UrlSigner::validate($signedUrl); // Returns bool
config/urlsigner.php (for custom signing keys, expiry defaults, etc.).UrlSigner facade for quick usage.ValidateSignedUrl middleware for route protection.Signing URLs:
// Basic signing
$url = UrlSigner::sign('https://example.com/download', now()->addHours(1));
// Sign with custom key (from config)
$url = UrlSigner::sign('https://example.com/secret', now()->addDays(7), 'custom_key');
Route Protection: Add middleware to a route:
Route::get('/download', function () {
return response()->download('file.pdf');
})->middleware('signed-url');
Or globally in app/Http/Kernel.php:
protected $middleware = [
// ...
\Spatie\UrlSigner\Middleware\ValidateSignedUrl::class,
];
Dynamic Expiry:
$expiry = now()->addMinutes(config('urlsigner.default_expiry_minutes'));
$url = UrlSigner::sign('/protected', $expiry);
Signing Non-Route URLs:
// External API URLs, S3 presigned URLs, etc.
$s3Url = UrlSigner::sign('https://my-bucket.s3.amazonaws.com/private-file', now()->addHours(2));
return response()->json(['download_url' => UrlSigner::sign('/download', now()->addHours(1))]);
$job = new SendEmailWithAttachment($user, UrlSigner::sign('/attachments/123', now()->addHours(1)));
dispatch($job);
<a href="{{ Spatie\UrlSigner\Laravel\Facades\UrlSigner::sign('/profile', now()->addDays(1)) }}">
Secure Link
</a>
Clock Skew:
$expiry = now()->addMinutes(30)->addSeconds(30); // Extra buffer
Key Management:
config('urlsigner.key'). If not set, it falls back to APP_KEY.'key' => env('URL_SIGNER_KEY', 'default_key_here'),
URL Length Limits:
Middleware Scope:
signed-url middleware validates all routes by default. Use route-specific middleware to avoid unintended blocks:
Route::get('/public', function () {})->withoutMiddleware('signed-url');
Validation Failures:
expires timestamp is in the future (not past).signature matches the expected hash (use dd() to inspect the URL parts):
$parts = parse_url($signedUrl);
parse_str($parts['query'], $query);
dd($query); // Inspect expires/signature
Log Expiry Warnings: Add a custom validator to log near-expiry URLs:
UrlSigner::extend(function ($url, $expiry) {
if ($expiry->diffInMinutes() < 5) {
Log::warning("URL near expiry: {$url}");
}
});
Custom Signing Logic: Override the default HMAC signing:
UrlSigner::extend(function ($url, $expiry, $key) {
return "custom-signed-{$url}?expires={$expiry->timestamp}";
});
Additional Query Parameters: Append extra data to URLs (e.g., user IDs):
$url = UrlSigner::sign(
'https://example.com/file',
now()->addHours(1),
'key',
['user_id' => 123]
);
Note: These params are not signed by default. Use with caution.
Rate Limiting: Combine with Laravel's rate limiting to prevent abuse:
Route::get('/download', function () {
return response()->download('file.pdf');
})->middleware(['signed-url', 'throttle:10,1']);
Testing:
Mock the UrlSigner facade in tests:
UrlSigner::shouldReceive('validate')
->once()
->with('https://example.com/protected?expires=...')
->andReturn(true);
How can I help you explore Laravel packages today?