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\Route\Collection;
use Infocyph\Webrick\Router\Url\SignedUrlConfig;
use Psr\Log\NullLogger;
$signKey = $_ENV['WEBRICK_SIGN_KEY'] ?? 'change-me';
$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';
},
routeCache: __DIR__ . '/../.route-cache',
registrarOptions: [
'exposeUrlServices' => true,
'signKey' => $signKey,
'signedDefaultTtl' => 900,
'signedUrlConfig' => $signedUrls,
'urlBaseUri' => $baseUri,
],
bindUrlServices: static function (Collection $routes) use ($signKey, $signedUrls, $baseUri): void {
Route::bindUrlServices($routes, $signKey, 900, $signedUrls, $baseUri);
},
fallbackAliasesFromRegistrar: true,
);
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
_sigand_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
urlBaseUriand 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(...)ortemporaryUrlUntil(...)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
ignoredQueryParamson the verifier profile.