Signed URLs with Laravel
Cosme Escobedo
Posted on April 3, 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.
Posted on April 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.