Weave 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

Expiring Url Bundle Laravel Package

ticketpark/expiring-url-bundle

A Symfony bundle for generating expiring URLs, letting you create time-limited, signed links to protect routes and actions. Configure expiration windows and validate signatures before granting access.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the key fact: this is a Symfony bundle, not a Laravel package. There is no service provider, no config publish tag, no facade. If you're in a Laravel codebase you will not composer require this and get anything useful — Laravel already ships URL::temporarySignedRoute() / URL::signedRoute() and the signed middleware, which cover the same ground. Read this package as a reference implementation of the algorithm and a drop-in for the Symfony services you maintain, then port the ~40 lines of hashing logic if you actually need it in Laravel.

Assuming a Symfony app:

composer require ticketpark/expiring-url-bundle

Register the bundle in config/bundles.php (Symfony Flex usually does this), then configure the secret — do not let it default to something derivable:

# config/packages/ticketpark_expiring_url.yaml
ticketpark_expiring_url:
    secret: '%env(EXPIRING_URL_SECRET)%'

Generate a real secret (php -r "echo bin2hex(random_bytes(32));") and store it outside the repo. The bundle's whole security model is "HMAC over the URL + expiry timestamp with this secret."

Where to look first: run bin/console debug:container ticketpark to get the actual service ID, and read the generator + validator classes in vendor/ticketpark/expiring-url-bundle/. They are short. Confirm the exact method names before writing code — the public surface is essentially two calls (create a URL with a TTL, validate an incoming URL), and the signatures have shifted between versions.

First use case: a time-limited download link. A controller action mints a URL for /files/42/download valid for 30 minutes, emails it, and a second action validates before streaming the file. Everything else (invites, password resets, preview links, signed webhooks) is the same shape.

Implementation Patterns

Generate at the edge, validate at the boundary. Inject the service where the URL is produced, not where it's consumed, and keep the validator in a single place — a listener, a middleware-equivalent kernel.request subscriber, or an explicit check at the top of the protected action.

// generating
public function share(ExpiringUrlService $expiringUrl): Response
{
    $url = $expiringUrl->generate(
        $this->router->generate('file_download', ['id' => 42], RouterInterface::ABSOLUTE_URL),
        new \DateInterval('PT30M')
    );

    // mail it, return it as JSON, whatever
}
// validating — explicit, at the top of the protected action
public function download(Request $request)
{
    if (!$this->expiringUrl->isValid($request->getUri())) {
        throw new GoneHttpException('This link has expired.');
    }
    // stream the file
}

Return 410 Gone for expired links rather than 403 — it's semantically correct, it stops browsers and mail scanners from retrying, and it makes logs readable.

Prefer a request subscriber for anything with more than one protected route. A single subscriber that matches a route attribute or a path prefix beats copy-pasting the check into five controllers, and it means a forgotten controller fails closed only if you invert the logic (deny by default, allow-list the exceptions).

Bind identity into the URL, not the session. The signature covers the URL and the expiry only. If a link must belong to a specific user or file, put the user/file ID in the path so it's inside the signed payload, and re-check authorization after validating — a valid signature proves the link was minted by you, not that the current visitor is entitled to it.

Laravel equivalent, for comparison:

$url = URL::temporarySignedRoute('files.download', now()->addMinutes(30), ['file' => 42]);

// in the route
Route::get('/files/{file}/download', ...)->middleware('signed');

If you have a mixed estate, standardize on this rather than porting the bundle.

Twig/Blade helper: wrap generation in a small service method so templates never construct TTLs inline; a link_expiring(path, ttl) function keeps the 30-minute policy in one place when legal asks you to change it to 15.

Gotchas and Tips

  • The URL must be byte-identical at validation time. Trailing slashes, query-parameter ordering, http vs https, and www vs apex all break the hash. Generate with ABSOLUTE_URL and validate against $request->getUri(), and make sure your reverse proxy passes the original scheme/host through (trusted_proxies + X-Forwarded-* in Symfony) or you'll generate links that never validate in production while working fine locally.
  • Secret rotation invalidates every outstanding link at once. If you rotate EXPIRING_URL_SECRET you need a grace period with two secrets (validate against both, sign with the new one) or you'll break every in-flight email. Plan it.
  • Clock skew. Validation compares against the server's own clock, so a load-balanced fleet with drifting NTP will produce phantom expiries. Keep time in sync; keep TTLs comfortably longer than your tolerance.
  • Don't cache signed URLs at the CDN or in a response cache — the expiry is baked into the string, and a cached redirect will happily serve a dead link. Also exclude the generation endpoint from any page cache or you'll hand the same URL to everyone.
  • The signature is over the URL, not the response. It's not encryption: anyone holding the link can read the IDs in it. Never put an email address, a token, or anything else sensitive in the path.
  • Use constant-time comparison. If you find yourself reimplementing the validator, compare with hash_equals(), never ===. The bundle should already do this — verify it before trusting it.
  • Low stars, zero dependents. Treat this as unmaintained-adjacent. Vendor the source or reimplement; the algorithm is small and the maintenance risk of a transitive dependency you can't patch is worse than 40 lines of your own HMAC code with a test suite.
  • Debugging: if validation fails unexpectedly, dump the exact string you signed and the exact string you validated — nine times out of ten they differ invisibly (encoding, trailing ?, port number). bin/console debug:router to confirm the route actually produces the path you think it does.
  • Extension point: the cleanest customization is replacing the service with your own implementation that wraps it, adding whatever claims you need (user ID, purpose string) into the signed payload rather than bolting them on as unsigned query params.
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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr