Tricks

Masking IDs in URLs using Hashids

Jan 26, 2023
Scott Windon
Admin panel, Integration

By default a URL generated by Filament will contain the ID of a model like this https://yourapp.com/admin/users/1/edit where 1 is the ID of the item. Often this is absolutely fine, but sometimes you might want to hide it (or mask it). The two main use cases for this I've come across so far are:

  • Security - If you show the ID of all of your models, a nefarious user might be able to use it to their advantage.
  • More professional - By masking the ID, you hide the number of items in the database.

What we want to achieve is a URL where the ID is masked like this: https://yourapp.com/admin/users/0l8q7xpnm4k63jo9/edit

You can do this with a library called hashids and a bit of Laravel knowhow.

Install dependencies

Install the hashids package via composer

$ composer require vinkla/hashids

Next, let's publish the configuration since we'll be changing that shortly:

$ php artisan vendor:publish --provider="Vinkla\Hashids\HashidsServiceProvider"

Making connections

In our configuration, found at config/hashids.php, we need to add a connection for each of the models where we'd like to use hashids. By doing this, we can set a different salt for each connection, meaning that the hash generated for User with the ID of 1 is different from Post with the ID of 1.

If we use the same connection for both, they'll both have the same hashid. While there's no technical problem with them both having the same hashid, we may still be revealing information about the app if you can infer what one of the underlying IDs might be.

The salt can be anything you like, as long as it's unique for each item. To keep things simple you might like to use the model name plus the APP_KEY.

We can adjust the starting length for the hashid at this point too, as well as the characters used:

'connections' => [
\App\Models\User::class => [
'salt' => \App\Models\User::class.env('APP_KEY'),
'length' => 16,
'alphabet' => 'abcdefghijklmnopqrstuvwxyz0123456789',
],
],

Setting the route key for our models

With route model binding we can customise the key that's used by overriding the getRouteKey method on the model. By default, this is the ID of our model, but we actually want this to be the hash of the item's ID encoded by the hashids library. Because we already know that we're going to be using this on at least 2 models (User and Product), it makes sense to abstract it and put it into a trait.

Create a new file in app/Http/Traits called Hashidable.php with the following contents:

namespace App\Http\Traits;
 
trait Hashidable
{
public function getRouteKey()
{
return \Hashids::connection(get_called_class())->encode($this->getKey());
}
}

We now need to use this trait on our models. In app/Models/User.php make sure you import the trait and use it:

use App\Http\Traits\Hashidable;
 
class User extends Authenticatable
{
use Hashidable;
 
// ...
}

At this point, any URL which is generated for our User model by our app will contain the hashid instead of the model ID. Perfect. But there's more! We need to tell Laravel what to do with the hashid when it sees it in the URL. At the moment it's going to try to look for the hashid in the ID column of the database, but we need to decode it first.

Decoding and binding

Logically, what we want to do at this point is to decode the hashid back to the model ID and then return the model instance. We can do exactly this by creating a middleware.

Create a new file in app/Http/Middleware called Hashidable.php with the following contents:

namespace App\Http\Middleware;
 
use Closure;
use Illuminate\Http\Request;
 
class Hashidable
{
private $routeModelMapping = [
'filament.resources.users.edit' => \App\Models\User::class,
'filament.resources.users.view' => \App\Models\User::class,
];
 
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if ( in_array($request->route()->getName(), array_keys($this->routeModelMapping)) ) {
$request->route()->setParameter('record', $this->getModelId($this->routeModelMapping[$request->route()->getName()], $request->route('record')));
}
return $next($request);
}
 
private function getModelId($model, $routeKey)
{
return \Hashids::connection($model)->decode($routeKey)[0] ?? $routeKey;
}
 
}

Then add this Middleware to your Kernal.php file within the "web" group:

protected $middlewareGroups = [
'web' => [
...
\App\Http\Middleware\Hashidable::class,
],
];

That's all there is to it! Now if you visit the URL https://yourapp.com/admin/users/0l8q7xpnm4k63jo9/edit, you'll see the user with the ID of 1. Neat!

If you want to add more models, you need to create a new connection in config/hashids.php, add the hashidable trait to your model and extend $routeModelMapping variable in app/Http/Middleware/Hashidable.php.

avatar

Dear sir, i have follow you but i have some problem. In list page ID is masked and worked but view and edit page i get error.

404 NOT FOUND

Not found url with id hash for 2 page. May you help me. I think $routeModelMapping in my code have problem but i can't fix. This post is very useful and helpful so i wana follow this. Pls help me.

avatar

same problem with me

πŸ‘ 1
πŸ‘€ 2
avatar

I've solved it by updating config/hashids.php file.

//......
\App\Models\User::class => [
'salt' => \App\Models\User::class . env('APP_KEY'),
'length' => 16,
// before
'alphabet' => 'abcdefghijklmnopqrstuvwxyz1234567890',
// after
'alphabet' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
],
πŸ’… 2
avatar

Update for Laravel 11/Filament 3. You register the middleware in your panel provider e.g. AdminPanelProvider.php:

return $panel->middleware[Hashidable::class];

I have modified my middleware like so:

class Hashidable
{
public function handle(Request $request, \Closure $next)
{
if (
method_exists($request->route()->getController(), 'getResource') &&
($record = $request->route('record'))
) {
$model = $request->route()->getController()->getResource()::getModel();
$request->route()->setParameter('record', $this->getModelId($model, $record));
}
Β 
return $next($request);
}
Β 
private function getModelId($model, $routeKey)
{
return \Hashids::connection($model)->decode($routeKey)[0] ?? $routeKey;
}
Β 
}

This could be improved further so just a hint to those who's stuck.