Bespoke signed URL's in Laravel

Laravel introduces a valuable feature called signed URLs, which empowers you to generate URLs capable of being validated for integrity each time a user accesses them. Although this feature is robust in its standard form, my unique use case demanded an extension of its capabilities across multiple projects and domains.

Within this blog post, I will walk you through the process of establishing your own clandestine URL system, building upon the groundwork laid by Laravel's signed URLs.

Crafting the URL

$url = "https://craiglovelock.co.uk/super-secret-page?account_id=12345";

// config('settings.secret_url_key') is a key stored in our .env file - 12345
$secret = hash_hmac('sha256', $url, config('settings.secret_url_key'));

$secureUrl = "{$url}&secret={$secret}";

We initiate with our 'base URL', the destination we intend the user to visit once validated. Subsequently, this URL undergoes hashing via the sha256 method.

The third parameter of the hash_hmac function accepts our 'hash key'. This is a pivotal aspect, enabling the use of the secret URL across different domains (more on that later).

The resulting URL of this code is the following:

https://craiglovelock.co.uk/super-secret-page?account_id=12345&secret=820705e989a44a62ffe94bb9e8f4569df2c71cd2ad20abdc0c08a7838dafdbcd

Validating the URL on another domain

Begin by creating a new middleware and subsequently adding it to our kernel:

php artisan make:middleware ValidateSecretURL
// 
protected $routeMiddleware = [
    // ...
    'validate-secret-url' => \App\Http\Middleware\ValidateSecretURL::class
]
public function handle(Request $request, Closure $next): Response|RedirectResponse
{
    $secretKey = $request->query('secret');

    abort_unless($secretKey, 403);

    $intendedUrl = rtrim(preg_replace('~(\?|&)secret=[^&]*~', '$1', $request->fullUrl()), '&');

    $secret = hash_hmac('sha256', $intendedUrl, config('settings.secret_url_key'));
    
    abort_unless($secret === $secretKey, 403);

    return $next($request);
}

Within our new middleware, we extract the secret value from the query string and employ the abort_unless() method for simple validation.

The essence of validating the actual key lies in attempting to hash the URL using the same key it was generated with. If the hash output matches the incoming URL, it's deemed valid. Once more, we rely on the abort_unless() method to yield the appropriate response.

This particular line:

$intendedUrl = rtrim(preg_replace('~(\?|&)secret=[^&]*~', '$1', $request->fullUrl()), '&');

Strips away the secret query string parameter, facilitating retrieval of the same URL initially subjected to hashing.

Closing notes

It's crucial to ensure consistent configuration values across all servers. While the key can be of your choosing, for our projects, we generate a similar key style as the Laravel app key found here.

This post provides a swift method to establish this system. In the future, I might explore another blog post detailing the addition of link expiration and enhancing the flexibility of URL construction methods.