Signed URLs with Laravel

Apr 2, 2022

In a project, you might need to generate unique URLs that perform some code where you need to make sure that the user didn't change the URL in any way. For example, unsubscribing a user from a newsletter, or maybe creating a link for a user to signup without a password, you might also want to offer a discount for your email subscribers only.

Whatever your use case might be, Laravel offers features to allow us to work with signed URLs.

Generating a signed URL

To generate a signed URL, the first thing we need to do is create a named route. For example, I'm going to create a named route to unsubscribe a user when they click on the link.

// routes/web.php
Route::get('/unsubscribe/{user}', [SubscriptionsController::class, 'unsubscribe'])
       ->name('unsubscribe');

Now, to generate a signed URL, we can use the URL facade:

use Illuminate\Support\Facades\URL;

return URL::signedRoute('unsubscribe', ['user' => 1]);

The first parameter to the signedRoute method is the name of the route. Here I'm using unsubscribed since that's the name that we gave the route above and the second parameter are the route parameters that we want to pass to the route, in this case, the user ID. This method will return a URL like this one:

https://yoursite.com/unsubscribe/1?signature=7594beb1a7af889d997eedc50f68eadb8b2e8d4097ba3a9ff1c600ffa8a02e49

As you can see, Laravel attached a signature parameter to the URL with a hash, this has represented the URL so if the user modifies any part of that URL the hash will be invalid.

Temporally signed URLs

You may also want to create a URL that expires after some time has passed, you can also create this type of URL with the temporarySignedRoute method

return URL::temporarySignedRoute(
    'unsubscribe', now()->addMinutes(30), ['user' => 1]
);

The second parameter to this method is the time at which the URL should expire, using this method an expires parameter will be added to the URL, and Laravel will report this URL as invalid.

Verifying the URL

We've now created the signed URL, but up to this point, we aren't verifying the hash. To do that, we can use the built-in middleware signed in our route. To do this, just add the middleware in your route declaration.

// routes/web.php
Route::get('/unsubscribe/{user}', [SubscriptionsController::class, 'unsubscribe'])
       ->name('unsubscribe')
       ->middleware('signed');

By adding this middleware, Laravel will automatically check the hash and throw a 403 HTTP code.

Personalizing the error page.

You may also want to customize the error that the user sees when the URL is invalid. That can be achieved by defining a custom renderable closure for the InvalidSignatureException:

// app\Exceptions\Handler.php
use Illuminate\Routing\Exceptions\InvalidSignatureException;
 
/**
 * Register the exception handling callbacks for the application.
 *
 * @return void
 */
public function register()
{
    $this->renderable(function (InvalidSignatureException $e) {
        return response()->view('error.link-expired', [], 403);
    });
}

Inside this closure, we can define what the user will see when visiting an invalid singed URL.

Verifying the URL without middleware

In some cases, you might want to verify the URL in your controller. You can do this by removing the singed middleware from your route definition and, instead, using the hasValidSignature in the request.

public function unsubscribe(Request $request)
{
    if (!$request->hasValidSignature()) {
        abort(401);
    }
    
    //...
}

This allows a little more flexibility if we need a custom experience.