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

Url Signer Bundle Laravel Package

tilleuls/url-signer-bundle

Symfony bundle for generating and validating signed URLs. Add tamper-proof, expiring links to routes, downloads, and API endpoints, protect resources from unauthorized access, and verify signatures through a simple, configurable service.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the bundle with Composer (Flex auto-registers it in a standard Symfony app):

composer require tilleuls/url-signer-bundle

Generate a strong signing key and expose it as an environment variable:

php -r "echo bin2hex(random_bytes(32)), PHP_EOL;"
# or: openssl rand -hex 32

Configure it in config/packages/url_signer.yaml:

url_signer:
    signature_key: '%env(URL_SIGNER_KEY)%'
    # optional: default_expiration, profiles, etc. depending on version

Then inject the CoopTilleuls\UrlSignerBundle\UrlSigner\UrlSignerInterface service wherever you need to mint or verify URLs. The two methods you'll live in are sign() and validate():

use CoopTilleuls\UrlSignerBundle\UrlSigner\UrlSignerInterface;

class DownloadController
{
    public function __construct(private UrlSignerInterface $urlSigner) {}

    public function createLink(): Response
    {
        $url = $this->urlSigner->sign('https://app.example.com/files/report.pdf', 3600);
        // https://app.example.com/files/report.pdf?expires=1700003600&signature=...
        return new Response($url);
    }

    public function download(Request $request): Response
    {
        if (!$this->urlSigner->validate($request->getUri())) {
            throw $this->createAccessDeniedException();
        }
        // serve file
    }
}

First concrete use case: mint a short‑lived link for a password reset, email verification, or file download, and validate the signature at the entry point of the corresponding controller.

Implementation Patterns

Signing URLs you generate with the router. The most common pattern is signing an internal absolute URL you produced with UrlGeneratorInterface:

$url = $urlGenerator->generate('user_verify', ['token' => $token], UrlGeneratorInterface::ABSOLUTE_URL);
$signed = $this->urlSigner->sign($url, 86400); // valid 24h

Pass ABSOLUTE_URL so the signed URL includes scheme + host — signing a relative URL produces a signature that only validates if the exact scheme/host match at verification time.

Validation in a controller or event subscriber. For a handful of actions, validate inline in the controller. For broader coverage (e.g., a whole /downloads/* area), add a kernel.request subscriber:

public function onKernelRequest(RequestEvent $event): void
{
    $request = $event->getRequest();
    if (!$request->attributes->getBoolean('_signed_url')) {
        return;
    }
    if (!$this->urlSigner->validate($request->getUri())) {
        $event->setResponse(new Response('', 403));
    }
}

Tag protected routes with _signed_url: true in the route defaults.

Minting links inside transactional emails. Build the signed URL before dispatching the message to the mailer, since the signer is synchronous and the URL must be stable. Store the plaintext URL (or the token) in the email, not a callback that re-signs.

Profile-based TTLs. For multi-tenant or multi-purpose cases, use distinct signature keys/profiles (if your bundle version exposes profiles config) so a leaked key for “public marketing links” can’t forge “admin password reset” links. If profiles aren’t available, namespace the purpose inside the signed URL path or a query parameter:

$url = $this->urlSigner->sign($baseUrl . '?purpose=reset', 3600);

Integration with API Platform / REST. Don’t sign API responses with content that changes on every request (paginated lists, timestamps). Instead sign a stable resource URL (e.g., /api/invoices/42/pdf) and let the API do normal authorization on top.

Idempotent verification. Treat validate() as a pure check. If it returns false, return a 403 (or throw InvalidSignatureException if your version does that). Don’t try to “repair” the URL — the signature covers the raw URL string.

Testing. In KernelTestCase/WebTestCase, pull UrlSignerInterface from the container and call sign() with a deterministic clock-free TTL. Because the signature includes an expires timestamp, you’ll need to freeze time (e.g., with symfony/clock + ClockMock) to assert on exact URLs; otherwise assert on the shape and just call validate() on what you generated.

Key rotation. Rotate the env var on a schedule. Rotation invalidates every outstanding signed URL, so pair rotation windows with the longest TTL you issue. Keep the old key active for a grace period if the bundle version supports multiple keys.

Gotchas and Tips

Never sign a URL that already has a signature or expires parameter. The signer appends its own; pre-existing values cause ambiguous parsing on validation. Strip them first.

Query-string ordering matters. The signature is computed over the URL string including the query string. If anything downstream reorders or re-encodes parameters (a proxy, an HTTP gateway, Symfony router normalization, trailing-slash redirects), validation fails. Test your exact production URL shape.

Behind a reverse proxy, prefer $request->getUri() over guessing the scheme. Use framework.trusted_proxies and framework.trusted_hosts so getSchemeAndHttpHost() reflects reality. Signing http://your-app/... when the browser reaches https://... is the #1 cause of “works locally, 403 in prod.”

Don’t append tracking or auth params after signing. Adding ?utm_source=email post-signature invalidates it. Decide the query string before signing, then never touch the URL again.

Expiration is a Unix timestamp in the URL — not a duration. If you set default_expiration, remember it applies to every sign() call without an explicit TTL. Audit it before shipping: an unintended default_expiration: null produces links that never expire.

Clock skew breaks trust. If you have multiple app servers, NTP-drift means one node may consider a URL expired while another doesn’t. Validate on the same tier (or with a small tolerance) — and never rely on expires alone for single-use semantics; a signed URL is replayable until it expires.

Signed URLs are bearer credentials. Anyone with the URL can use it: log aggregators, email forwarding, browser history, Referer headers. Don’t put sensitive data in the path; put the resource ID in the path and let authorization run after signature validation.

Keep the key out of git and out of var/cache. Use %env(URL_SIGNER_KEY)% with a sealed .env.local / vault secret. When the key changes, clear the container cache (bin/console cache:clear) so the rotated key is picked up.

Fail closed on missing configuration. In CI, run a smoke test that boots the kernel and asserts sign() produces a URL that validate() accepts; this catches a missing/misconfigured signature_key before deployment.

Extension point: wrap the signer. If you need per‑tenant keys or an audit log, decorate UrlSignerInterface with your own service that dispatches a Symfony event (e.g., SignedUrlIssuedEvent) or delegates to a profile based on the current tenant — the interface is small and stable enough to decorate safely.

Exception type vs. boolean. Depending on version, validate() may return bool or throw. Wrap it with a small helper (isValid(string $url): bool) in your own service so a package upgrade that switches semantics doesn’t ripple across controllers.

Don’t cache signed responses. Because the URL itself is the credential and the response may vary by signature, mark signed endpoints Cache-Control: private, no-store to avoid a CDN serving one user’s signed resource to another.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin