Signed & Temporary URLs#

Webrick signed URLs are generated by Route facade helpers and verified by VerifySignedUrlMiddleware.

Verification failures throw framework HTTP exceptions internally. The kernel’s ErrorHandler converts them into the final 400/403/410 responses just before emission.

Current capabilities:

  • plain named-route URLs via Route::urlFor(...)

  • permanent signed URLs via Route::signedUrlFor(...)

  • TTL-based temporary URLs via Route::temporaryUrlFor(...)

  • explicit-expiry URLs via Route::temporaryUrlUntil(...)

  • relative or absolute payload signing

  • configurable signature and expiry parameter names

  • configurable HMAC algorithm

  • ignored query parameters during verification

  • multiple verification keys for key rotation

1. Boot-time setup#

use Infocyph\Webrick\Middleware\VerifySignedUrlMiddleware;
use Infocyph\Webrick\Router\Definition\Registrar;
use Infocyph\Webrick\Router\Dispatch\MiddlewareAliases;
use Infocyph\Webrick\Router\Facade\Router as Route;
use Infocyph\Webrick\Router\Kernel\RouterKernel;
use Infocyph\Webrick\Router\Matching\ShardedMatcher;
use Infocyph\Webrick\Router\Url\SignedUrlConfig;
use Psr\Log\NullLogger;

$signKey = $_ENV['WEBRICK_SIGN_KEY']
    ?? throw new RuntimeException('WEBRICK_SIGN_KEY is required');
$baseUri = $_ENV['WEBRICK_URL_BASE_URI'] ?? 'http://localhost';
$signedUrls = new SignedUrlConfig(
    generationKey: $signKey,
    verificationKeys: [$signKey],
    defaultTtl: 900,
);

MiddlewareAliases::register(
    'verifySignedUrl',
    static fn() => new VerifySignedUrlMiddleware($signKey, 5),
);
MiddlewareAliases::register(
    'verifySignedUrlAbsolute',
    static fn() => new VerifySignedUrlMiddleware(new SignedUrlConfig(
        verificationKeys: [$signKey],
        payloadMode: SignedUrlConfig::MODE_ABSOLUTE,
        ignoredQueryParams: ['preview'],
        leeway: 5,
    )),
);

$kernel = RouterKernel::bootWithRegistrar(
    log: new NullLogger(),
    matcher: ShardedMatcher::make(),
    register: static function (Registrar $registrar): void {
        unset($registrar);
        require __DIR__ . '/../routes.php';
    },
    registrarOptions: [
        'exposeUrlServices' => true,
        'signKey' => $signKey,
        'signedDefaultTtl' => 900,
        'signedUrlConfig' => $signedUrls,
        'urlBaseUri' => $baseUri,
    ],
    invoker: $invoker,
);

signKey, signedDefaultTtl, signedUrlConfig and urlBaseUri remain part of kernel configuration. When booting from cache, Webrick defers alias loading and UrlGenerator construction until the first URL helper call. Provide bindUrlServices only to replace the default binding behavior.

If you do not want aliases, you can attach new VerifySignedUrlMiddleware(...) directly in a route’s middleware option.

2. Define protected routes#

use Infocyph\Webrick\Request\Request;
use Infocyph\Webrick\Response\Response;
use Infocyph\Webrick\Router\Facade\Router as Route;

Route::get('/download/{file}', fn(string $file) => Response::attachment(__DIR__ . '/../files/' . $file, $file), [
    'as' => 'file.download',
    'middleware' => ['verifySignedUrl'],
]);

Route::get('/secure-absolute/{id:int}', fn(Request $request, string $id) => Response::json([
    'id' => (int) $id,
    'preview' => $request->getQueryParams()['preview'] ?? null,
]), [
    'as' => 'secure.absolute',
    'middleware' => ['verifySignedUrlAbsolute'],
]);

3. Generate URLs#

use Infocyph\Webrick\Router\Facade\Router as Route;
use Infocyph\Webrick\Router\Url\SignedUrlConfig;

$plain = Route::urlFor('file.download', ['file' => 'report.pdf']);
$signed = Route::signedUrlFor('file.download', ['file' => 'report.pdf']);
$temp = Route::temporaryUrlFor('file.download', ['file' => 'report.pdf'], ttl: 900);
$until = Route::temporaryUrlUntil('file.download', new DateTimeImmutable('+15 minutes'), ['file' => 'report.pdf']);

$absolute = Route::signedUrlFor('file.download', ['file' => 'report.pdf'], absolute: true);
$absolutePayload = Route::signedUrlFor(
    'secure.absolute',
    ['id' => 42],
    ['dl' => 1],
    absolute: true,
    payloadMode: SignedUrlConfig::MODE_ABSOLUTE,
);

4. Signed URL profiles with richer verification#

use Infocyph\Webrick\Router\Url\SignedUrlConfig;

$profile = new SignedUrlConfig(
    generationKey: 'active-signing-key',
    verificationKeys: ['active-signing-key', 'previous-signing-key'],
    defaultTtl: 900,
    signatureParam: '_sig',
    expiryParam: '_exp',
    algorithm: 'sha3-256',
    payloadMode: SignedUrlConfig::MODE_ABSOLUTE,
    ignoredQueryParams: ['preview', 'utm_source'],
    leeway: 10,
);

Use this when you need:

  • key rotation

  • absolute-payload verification

  • tracking parameters that should not break signatures

  • custom parameter names or algorithm choices

5. Operational notes#

  • Default reserved query params are _sig and _exp.

  • Signed URL helpers reject attempts to inject reserved params from user-supplied query arrays.

  • temporaryUrlFor(...) uses the helper TTL argument when provided, otherwise the configured default TTL.

  • temporaryUrlUntil(...) is the better choice when expiration must align to a fixed timestamp.

  • Absolute payload signing depends on a stable urlBaseUri and should be used intentionally.

6. Troubleshooting#

Missing signature#

Usually the proxy stripped or failed to forward the query string.

Nginx

location / {
    try_files $uri /index.php?$query_string;
}

Apache

RewriteRule ^ index.php [QSA,L]

Invalid signature#

Common causes:

  • generation and verification use different keys

  • query params were added or mutated after generation

  • absolute payload mode was used during generation but not verification, or the reverse

  • base URI or host does not match what the generator signed

Expired URL#

  • TTL already elapsed

  • explicit expiry timestamp is in the past

  • clocks differ too much between generators and verifiers

Use leeway when you need a small clock-skew buffer.

7. Recommendations#

  • Use permanent signed URLs only for low-risk links.

  • Use temporaryUrlFor(...) or temporaryUrlUntil(...) for downloads, callbacks and sensitive actions.

  • Prefer multi-key verification during signing-key rotations.

  • If your application appends analytics params after generation, add them to ignoredQueryParams on the verifier profile.