Obfuscating IDs in Laravel using Hashids

Florian Domgjoni
4 min readMar 29, 2022

--

Photo by Tai Bui on Unsplash

Introduction

Auto incremented IDs are the most used, preferred, and efficient form of storing primary keys in SQL databases. However displaying these IDs plainly in your API URL, may leak unintended data to your end users/public.

Consider the following scenarios in which auto-incremented primary keys are plainly shown in the URL:

  • We have a GET /api/users/:id endpoint. Anyone will be able to guess how many users we have right now by creating a new account and simply viewing their user id.
  • We have a GET /api/products/:id/purchase/:id endpoint which allows users to buy a product. A competing business can purchase a product from your platform, after a given time such as a day they make another purchase. By simply looking at the difference between the IDs, they can find out how many products your business has sold within the given time frame.

In short by doing this you may be providing your users with information they don’t need to have.

An additional reason to avoid using auto-incremented IDs in the URL is that they, in my opinion at least, just do not look good:

/api/posts/19402425483looks uglier than/api/posts/zd4fTddF

There are two possible solutions to the aforementioned problems:

  • Stop using auto-incremented IDs altogether, use a different primary key such as UUID or nanoID, although this solution comes with its caveats.
  • Keep using auto-incremented IDs internally, but disguise, or obfuscate them when they are displayed to the public as well as receive hashids instead of plain ids from client requests.

In this article, we’ll take the second approach and we’ll implement a solution in Laravel using the Hashids package.

Import to note
The term obfuscation used in this article’s title and on the official docs of the hashids docs is not unintended. It’s mentioned in the docs that hashids are not meant to be very secure and there are ways to decode them even without a salt. Despite this, they are certainly more secure than showing plain IDs and they make it way harder to find out the real IDs.

Implementation in Laravel

First, we need to install the Hashids library:

composer require hashids/hashids

Now we can use it to obfuscate our IDs. To make it easier for us to use this package, we will create a simple wrapper class. I like putting most custom and shared logic inside a services folder, thus we’ll create the following class:

With this class in place, we can encode and decode our IDs. However, doing these actions repeatedly every time we need to access or return an ID would be extremely cumbersome( I’ve seen this done in real projects 😐). After all one of the most basic principles of programming is avoiding manual repetition and automating processes.

Since we want to get hashids as route parameters instead of plain IDs, we need to decode them every time we want to interact with our database. We’ll make this process painless, by using route binding in the RouteServiceProvider.php file:

Now every time we receive a hashid as a route parameter it will automatically be decoded, then we can use it as a regular id in our controllers.

The next step is to encode the id to its hashid counterpart when we need to return a response.

The API resource approach

You can use API resources to handle the encoding of IDs very smoothly. For each model, you should create an API resource and transform the plain ID. Here’s an example:

Finally, your routes will look as such:

Using API resources in this case and in general is very convenient and neat, however, in a real-world project, there are cases when you may need to return custom or complex responses, which may not be based on resources. Follow along…

The custom cast approach

We will create a custom cast to handle setting and getting our IDs:

Using this custom cast, every time you retrieve a model the id will automatically be converted to its hashid counterpart. Similarly, if we want to set the id it will try to decode the hashid and set the raw id value. The last step is to add our cast to the id field:

Great, now all our ids are converted to hashids when accessed, but what if we need to access the raw original id? After all, that is the id we will use to store data in the database. Worry not Laravel has this covered by providing us with the very convenient getRawOriginal and setRawAttributes methods, which allow you to skip casts, mutators, and accessors.

Lastly, here’s how our routes look like:

Before:

After:

--

--