Whether you are a developer, a security expert or a product owner trying to improve your application security as best as possible then you might be already familiar with uuids since they became quite popular in the last few years. If you are new to this topic, this could still be something interesting for you.
The concept
Similar to the id which is an auto incrementing integer in your database, uuid stands for “Universally Unique Identifier”, a 128-bit label used widely in computer systems. In web applications uuids are used to obfuscate or to hide the original identity of the database record.
When you are exposing your database records or models, as they are called in laravel, to the outside world, there will be cases where you will have to expose your model id.
Attackers can easily iterate through all your models and try to get access to sensible data of your customers. That’s where uuids come in handy since they are a complex string containing numbers and characters, the probability of an attacker to find a real record is much lower.
Implementation difficulty
When it comes to implementation, there are some approaches with which you might already be familiar with like:
- Swapping your id column completely with an uuid
- Adding an extra uuid column to the existing id column
Only that their implementation in actual code can be quite challenging. What if your project is already in production, with very complex business logic using jobs and services? For those who are just at the beginning of a project, performance is still quite a big deal when switching an id column from integer to string and also relationships are harder to maintain. For these reasons we are going to look at a different approach.
The solution
Since uuids are mainly used in routing, we could make use of a proxy.
That would be an intermediate table similar to a hash table, which is going to point to the model that we want to expose to the outside world.
So create a migration which is going to content the following columns:
Schema::create('public_models', function (Blueprint $table) {
$table->uuid()->primary();
$table->morphs('model');
$table->timestamps();
$table->softDeletes();
});
The relationship is going to be a 1 to 1.
Then, create a trait which is going to fulfill 3 tasks:
- Listen to created and deleted events in order to create or delete an associated PublicModel
- Override the default functionality of resolveRouteBinding($value, $field = null) to resolve the PublicModel
- Expose a publicModel() relationship to any model which uses this trait
This is how the trait is going to look like:
trait ExposesUuids
{
/**
* @return void
*/
public static function bootExposessUuids(): void
{
static::created(function (Model $model) {
if ($model->isRelation('publicModel')) {
$model->publicModel()->firstOrCreate();
}
});
static::deleted(function (Model $model) {
if ($model->isRelation('publicModel')) {
$model->publicModel()->delete();
}
});
}
/**
* Retrieve the model for a bound value.
*
* @param mixed $value
* @param string|null $field
* @return Model|null
*/
public function resolveRouteBinding($value, $field = null): ?Model
{
/**
* @var PublicModel|null $publicModel
*/
$publicModel = PublicModel::query()->find($value);
return $publicModel?->model;
}
/**
* @return MorphOne
*/
public function publicModel(): MorphOne
{
return $this->morphOne(PublicModel::class, 'model');
}
}
Pretty straightforward right? Lastly, let’s configure our PublicModel a little bit.
class PublicModel extends Model
{
use HasFactory, SoftDeletes, HasUuids;
protected $primaryKey = 'uuid';
protected $keyType = 'string';
protected $guarded = ['uuid'];
protected $with = ['model'];
/**
* @return MorphTo
*/
public function model(): MorphTo
{
return $this->morphTo();
}
}
Now let’s try this in a clean project setup.
We will register our new ExposesUuids trait in a Post.php model:
class Post extends Model
{
use HasFactory, SoftDeletes, ExposesUuids;
protected $guarded = ['id'];
/**
* @return BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
And create the test routes:
Route::prefix('/posts')->group(function () {
Route::get('/form', [PostController::class, 'form']);
Route::post('/create', [PostController::class, 'create']);
Route::get('/', [PostController::class, 'index']);
Route::get('/{post}', [PostController::class, 'show']);
});
Since I installed laravel breeze starter kit for authentication, for demonstration purposes I have added them in the web.php file.
The /form & /create routes are going to be used to create a user post.
The /posts route is going to list all user posts and / {post} is going to show a user’s post.
We are going to also create a PostResource using php artisan make:resource Post command.
class PostResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->publicModel->uuid,
'title' => $this->title,
'body' => $this->body
];
}
}
It is going to expose the id, title and the body columns of the Post model using our new publicModel->uuid.
Here is the controller:
class PostController extends Controller
{
/**
* @return Factory|View|Application
*/
public function form(): Factory|View|Application
{
return view('form');
}
/**
* @param Request $request
* @return PostResource
*/
public function create(Request $request): PostResource
{
$post = new Post([
'title' => $request->post('title'),
'body' => $request->post('body')
]);
$request->user()->posts()->save($post);
return PostResource::make($post);
}
/**
* @param Request $request
* @return AnonymousResourceCollection
*/
public function index(Request $request): AnonymousResourceCollection
{
return PostResource::collection($request->user()->posts);
}
/**
* @param Post $post
* @return PostResource
*/
public function show(Post $post): PostResource
{
return PostResource::make($post);
}
}
and the view:
We are going to boot our application using php artisan serve and we are ready to go!
I have created a new user and logged in and this is how the post form looks like:
After submitting the query we can see the response:
Perfect, now lets list all our posts:
As you can see, our user has 3 posts and lets access our latest post using the uuid:
Awesome, let’s give it another shot and see the very first post of my user:
Hurray, everything works as expected!
Awesome I’m ready to code but hey what is this?
Now don’t just leave yet, I know you want to try this in your awesome project 😀 but there are some things to consider.
The advantages / disadvantages
The benefits of this approach using uuids are that
- You have one simple hash table which points out to the associated model
- No need to change all your migrations to add an uuid or swap an existing id column
- No breaking change and your business logic is going to stay pretty much the same
On the other hand the disadvantages are that
- For applications in production, there is some maintenance work which needs to be done when registering the trait ExposesUuids to a new eloquent model. You will have to create a PublicModel for all existent database models.
Things to keep in mind
- Don’t forget to authorize any user actions against an eloquent model. Use Laravel’s Gates & Policies authorization systems. Without them, as an example, a user could simply view another user’s bank account. That sounds terrifying right?
- Speaking about authorization. if your application expects model ids from the front-end, we recommend authorizing those models too. Use model policies to be sure that the user cannot access in any way other users data.
Conclusion
Uuids are a good security measure to protect your users data. I think that the hash map approach is definitely worth a shot and let me know what you think about this in the comments. Happy coding 🎉.
Useful links:
Other uuid approaches:
We have helped 20+ companies in industries like Finance, Transportation, Health, Tourism, Events, Education, Sports.