dsentker/url-signature
Laravel/PHP package to create and verify signed URLs. Add a signature to query strings to protect routes and parameters from tampering, with simple helpers for generating signatures and validating incoming requests, including optional expiry support.
Installation:
composer require dsentker/url-signature
Add the service provider to config/app.php under providers:
Dsentker\UrlSignature\UrlSignatureServiceProvider::class,
Publish Config (optional):
php artisan vendor:publish --provider="Dsentker\UrlSignature\UrlSignatureServiceProvider"
This generates config/url-signature.php with default settings.
First Use Case: Generate a signed URL in a controller or blade template:
use Dsentker\UrlSignature\Facades\UrlSignature;
$signedUrl = UrlSignature::generate(
url('/payment?amount=100¤cy=USD'),
['secret' => config('services.payment.secret_key')]
);
Outputs a URL like:
https://example.com/payment?amount=100¤cy=USD&signature=abc123xyz
Generating Signed URLs:
// In a controller
$url = route('checkout', ['id' => 123]);
$signedUrl = UrlSignature::generate($url, ['secret' => 'your_key']);
// In Blade
@php
$signedUrl = UrlSignature::generate(
route('download', ['file' => 'report.pdf']),
['secret' => config('app.signature_key')]
);
@endphp
<a href="{{ $signedUrl }}">Download Report</a>
Validation Middleware:
Add to app/Http/Kernel.php:
protected $routeMiddleware = [
'validate.signature' => \Dsentker\UrlSignature\Middleware\ValidateSignature::class,
];
Apply to routes:
Route::get('/secure-endpoint', function () {
// Logic for validated requests
})->middleware('validate.signature');
Dynamic Secret Keys: Use closures for runtime secrets:
$signedUrl = UrlSignature::generate(
url('/admin'),
['secret' => fn() => app('auth')->user()->apiKey]
);
Route::get('/signed-route', function () {
return UrlSignature::generate(
route('target'),
['secret' => 'dynamic_key']
);
});
return response()->json([
'data' => $data,
'download_url' => UrlSignature::generate(
route('download', $file),
['secret' => $user->secret]
)
]);
Dispatch(new ProcessPayment($order))
->delay(now()->addMinutes(5))
->onQueue('high');
// In job handle():
$signedUrl = UrlSignature::generate(
route('payment.webhook'),
['secret' => $order->webhookSecret]
);
Secret Management:
config/services.php or .env.Query String Order Sensitivity (Critical in 1.1.0):
QueryString helper class. This is now the default behavior and cannot be disabled._token, page), use the exclude option to avoid unintended sorting:
UrlSignature::generate($url, [
'secret' => $secret,
'exclude' => ['_token', 'page'] // Explicitly exclude unsorted params
]);
Middleware Timing:
ValidateSignature middleware runs after route resolution. If your route requires signed parameters (e.g., ?id=123&signature=...), validate the signature before processing the ID.ValidateSignature to reorder logic.Case Sensitivity: The default HMAC algorithm (SHA256) is case-sensitive. Ensure secrets and generated signatures match exactly.
Signature Mismatches: Compare the generated signature with the received one:
$expected = UrlSignature::generate($url, ['secret' => $secret]);
$received = request()->query('signature');
dd(hash_equals($expected, $received)); // Prevent timing attacks
\Log::debug('Generated URL Params', [
'sorted_params' => UrlSignature::getSortedQueryString($url),
'raw_params' => request()->query()
]);
Log Raw Inputs:
Add to ValidateSignature handler:
\Log::debug('Signed URL Validation', [
'url' => $request->fullUrl(),
'query_params' => $request->query(),
'signature' => $request->query('signature'),
'sorted_params' => UrlSignature::getSortedQueryString($request->fullUrl())
]);
Custom Algorithms:
Extend the Dsentker\UrlSignature\Contracts\SignatureGenerator contract:
class CustomGenerator implements SignatureGenerator {
public function generate(string $url, string $secret): string {
return base64_encode(hash_hmac('sha1', $url, $secret, true));
}
}
Bind in AppServiceProvider:
$this->app->bind(SignatureGenerator::class, function () {
return new CustomGenerator();
});
Parameter Whitelisting:
Use the exclude parameter to control included parameters (now more reliable in 1.1.0):
UrlSignature::generate($url, [
'secret' => $secret,
'exclude' => ['_token', 'page', 'sort'] // Ignore these params
]);
Expiry Support:
Add a expires parameter to URLs:
$url = UrlSignature::generate(
url('/expire-soon'),
[
'secret' => $secret,
'expires' => now()->addMinutes(10)->timestamp
]
);
Validate in middleware:
if (request()->query('expires') < time()) {
abort(403, 'URL expired');
}
Custom Query String Handling (Advanced):
If you must deviate from alphabetical sorting (e.g., for legacy systems), override the QueryString helper:
use Dsentker\UrlSignature\Support\QueryString;
class CustomQueryString extends QueryString {
protected function sortParams(array $params): array {
// Implement custom sorting logic (e.g., preserve original order)
return $params;
}
}
Bind the custom class in AppServiceProvider:
$this->app->bind(\Dsentker\UrlSignature\Contracts\QueryString::class, function () {
return new CustomQueryString();
});
Testing: Add tests to verify signature generation/validation with sorted parameters:
public function test_sorted_query_string()
{
$url = url('/test?b=2&a=1');
$signedUrl = UrlSignature::generate($url, ['secret' => 'test']);
$this->assertStringContainsString('a=1&b=2', $signedUrl);
$this->assertTrue(UrlSignature::validate($signedUrl, ['secret' => 'test']));
}
How can I help you explore Laravel packages today?