Signed URLs with Laravel
Tags: laravel
Published: 2022-04-03 02:12:19

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.

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

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

```php
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

```php
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. 

```php
// 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`:

```php
// 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.

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

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