Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Url Signer Laravel Package

spatie/url-signer

Generate and verify signed URLs with expiration timestamps using a shared secret. spatie/url-signer appends expires and signature parameters, letting you safely share time-limited links (e.g., in emails) and validate them server-side with a simple API.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require spatie/url-signer
    

    Register the service provider in config/app.php (if not auto-discovered):

    Spatie\UrlSigner\UrlSignerServiceProvider::class,
    
  2. Configuration: Publish the config file:

    php artisan vendor:publish --provider="Spatie\UrlSigner\UrlSignerServiceProvider"
    

    Update config/url-signer.php with your signing key (e.g., APP_KEY or a custom string).

  3. First Use Case: Generate a signed URL for a download link (e.g., a PDF):

    use Spatie\UrlSigner\Facades\UrlSigner;
    
    $signedUrl = UrlSigner::sign(
        route('download.pdf', ['id' => 123]),
        now()->addMinutes(15) // Expires in 15 minutes
    );
    

    Output: https://app.com/download/123?expires=1705000000&signature=abc123...


Implementation Patterns

Common Workflows

  1. Signed Routes: Use named routes for clarity and maintainability:

    $signedUrl = UrlSigner::sign(
        route('admin.export', ['user' => $user->id]),
        now()->addHours(1)
    );
    
  2. Dynamic Expiry Logic: Tie expiry to user roles or actions:

    $expiry = $user->isAdmin() ? now()->addDays(7) : now()->addHours(1);
    $signedUrl = UrlSigner::sign(route('user.profile'), $expiry);
    
  3. Validation Middleware: Protect routes with a middleware:

    namespace App\Http\Middleware;
    
    use Closure;
    use Spatie\UrlSigner\Facades\UrlSigner;
    
    class ValidateSignedUrl
    {
        public function handle($request, Closure $next)
        {
            if (!$request->has('signature') || !$request->has('expires')) {
                abort(403);
            }
    
            if (!UrlSigner::validate($request->fullUrl())) {
                abort(403);
            }
    
            return $next($request);
        }
    }
    

    Register in app/Http/Kernel.php:

    protected $routeMiddleware = [
        'signed.url' => \App\Http\Middleware\ValidateSignedUrl::class,
    ];
    

    Usage in routes:

    Route::get('/download/{id}', function () { ... })
        ->middleware('signed.url');
    
  4. Signed URL Storage: Store signed URLs in the database (e.g., for email campaigns):

    $user->signed_urls()->create([
        'url' => route('user.invoice', ['id' => $invoice->id]),
        'expires_at' => now()->addDays(3),
        'signature' => UrlSigner::sign(
            route('user.invoice', ['id' => $invoice->id]),
            now()->addDays(3)
        ),
    ]);
    
  5. Custom Signers: Extend for custom logic (e.g., HMAC-SHA1):

    use Spatie\UrlSigner\Signers\Signer;
    
    class CustomSigner implements Signer
    {
        public function sign(string $url, string $key, string $expiry): string
        {
            return hash_hmac('sha1', $url . $expiry, $key);
        }
    }
    

    Bind in AppServiceProvider:

    UrlSigner::extend('custom', function () {
        return new CustomSigner();
    });
    

    Usage:

    $signedUrl = UrlSigner::signWith('custom', $url, $expiry);
    

Integration Tips

  • Laravel Notifications: Attach signed URLs to emails:

    $email = new DownloadNotification($user, $signedUrl);
    Mail::to($user)->send($email);
    
  • Queue Jobs: Generate signed URLs asynchronously:

    GenerateSignedUrl::dispatch($user, $expiry);
    

    Job:

    public function handle()
    {
        $signedUrl = UrlSigner::sign(route('download'), $this->expiry);
        // Send via email/notification
    }
    
  • API Tokens: Combine with Sanctum/Passport for API access:

    $token = $user->createToken('Signed URL Access')->plainTextToken;
    $signedUrl = UrlSigner::sign(
        "https://api.app.com/protected?token={$token}",
        now()->addMinutes(5)
    );
    

Gotchas and Tips

Pitfalls

  1. Clock Skew:

    • URLs may fail validation if the server clock drifts. Use NTP or sync clocks.
    • Fix: Add a small buffer (e.g., 2 minutes) when validating expiry:
      if (strtotime($request->expires) > now()->addMinutes(2)->timestamp) {
          // Valid
      }
      
  2. URL Length Limits:

    • Long signatures may exceed URL limits (e.g., 2048 chars). Use shorter keys or base64-encode.
    • Fix: Truncate or encode the key:
      $key = base64_encode(hash('sha256', config('app.key'), true));
      
  3. Query String Collisions:

    • Existing query params may interfere with expires/signature.
    • Fix: Use a route parameter or fragment (#expires=...):
      $url = route('download', [], false) . '?file=123#expires=...';
      
  4. CSRF Protection:

    • Signed URLs bypass CSRF middleware. Explicitly whitelist routes:
      Route::middleware('signed.url')->group(function () {
          Route::get('/download', ...);
      });
      
  5. Key Rotation:

    • Changing the signing key invalidates all existing URLs. Plan rotations carefully.
    • Fix: Use a versioned key (e.g., APP_KEY_v1) and migrate gradually.

Debugging

  1. Validation Failures:

    • Log the raw URL and computed signature for comparison:
      $url = $request->fullUrl();
      $computedSig = UrlSigner::sign($url, config('url-signer.key'), $request->expires);
      logger("Expected: {$request->signature}, Computed: {$computedSig}");
      
  2. Timezone Issues:

    • Ensure expires uses UTC timestamps:
      $expiry = now()->setTimezone('UTC')->addMinutes(10);
      
  3. Signature Mismatches:

    • Verify the URL being signed matches the original (e.g., case sensitivity in routes):
      $originalUrl = route('download', ['id' => 123], false);
      $signedUrl = UrlSigner::sign($originalUrl, $expiry);
      

Extension Points

  1. Custom Query Parameters: Add metadata to signed URLs:

    $signedUrl = UrlSigner::sign(
        route('download'),
        now()->addHours(1),
        ['user_id' => $user->id, 'purpose' => 'invoice']
    );
    

    Extend the UrlSigner class to support custom params.

  2. Rate Limiting: Track signed URL usage to prevent abuse:

    if (SignedUrl::where('signature', $request->signature)->exists()) {
        abort(403, 'URL already used');
    }
    
  3. IP Restriction: Combine with middleware to restrict by IP:

    public function handle($request, Closure $next)
    {
        if (!UrlSigner::validate($request->fullUrl()) ||
            !$request->ip()->is($request->user()->allowed_ips)) {
            abort(403);
        }
        return $next($request);
    }
    
  4. Event Dispatching: Trigger events on URL validation:

    event(new SignedUrlValidated($request->signature));
    

    Listen for analytics or logging:

    SignedUrlValidated::listen(function ($event) {
        Analytics::log('signed_url_used', ['signature' => $event->signature]);
    });
    
  5. Fallback URLs: Redirect expired URLs to a fallback:

    if (!UrlSigner::validate($request->fullUrl())) {
        return redirect()->route('download.fallback');
    }
    
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
milesj/emojibase
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport