Laravel Passport: Assigning a UUID to your OAuth Clients
The base Laravel Passport package comes with some amazing functionality out of the box. My only criticism is the design of the OAuth Clients; having incremental IDs over something like a UUID. Some see this as a security concern and personally it didn’t sit right with me.
So I’ve taken a little bit of time to see how I can change this and what bits need tweaking in order to make this work, and this experiment turned out to be a success!
Overriding the source
Updating the migrations
Looking through the Laravel Passport docs, it appears that you can publish the migrations that it uses. This means that we can override them and update the migrations to use a UUID field instead of an incremental one.
So first we will publish the Laravel Passport migrations:
php artisan vendor:publish --tag=passport-migrations
Great! Now we should have a number of migrations added to our database/migrations
directory:
./2016_06_01_000001_create_oauth_auth_codes_table.php ./2016_06_01_000002_create_oauth_access_tokens_table.php ./2016_06_01_000003_create_oauth_refresh_tokens_table.php ./2016_06_01_000004_create_oauth_clients_table.php ./2016_06_01_000005_create_oauth_personal_access_clients_table.php
Now we will go through these in order and update the database structure as necessary to accomodate for our UUIDs.
First up is the oauth_auth_codes
table, auth_codes
are connected to oauth_clients
so we will need to update the migration to look a little like this:
/** database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php */
public function up()
{
Schema::create('oauth_auth_codes', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->integer('user_id');
$table->uuid('client_id');
$table->text('scopes')->nullable();
$table->boolean('revoked');
$table->dateTime('expires_at')->nullable();
});
}
Next up is the oauth_access_tokens
table, access_tokens
are also linked to specific clients so we can update that column:
/** database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php */
public function up()
{
Schema::create('oauth_access_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->integer('user_id')->index()->nullable();
$table->uuid('client_id');
$table->string('name')->nullable();
$table->text('scopes')->nullable();
$table->boolean('revoked');
$table->timestamps();
$table->dateTime('expires_at')->nullable();
});
}
The oauth_clients
table is after that, this will be the time we override the id
column to be a UUID and primary rather than incrementing:
/** database/migrations/2016_06_01_000004_create_oauth_clients_table.php */
public function up()
{
Schema::create('oauth_clients', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->integer('user_id')->index()->nullable();
$table->string('name');
$table->string('secret', 100);
$table->text('redirect');
$table->boolean('personal_access_client');
$table->boolean('password_client');
$table->boolean('revoked');
$table->timestamps();
});
}
Finally, the ouath_personal_access_clients
table needs the client_id
column changed:
/** database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php */
public function up()
{
Schema::create('oauth_personal_access_clients', function (Blueprint $table) {
$table->increments('id');
$table->uuid('client_id')->index();
$table->timestamps();
});
}
Now that all of our custom migrations are done, we need to tell Laravel to not run the Passport migrations that are in our vendor/
directory. To do this we need to open our AppServiceProvider
where we can add this line to the register
method:
use Laravel\Passport\Passport;public function register()
{
Passport::ignoreMigrations();
}
The first step of this problem has now been overcome, once we migrate our tables we will have UUID type columns in place of the integer ones that used to be there.
Trying to install Passport
Our next step is to try to install Passport so that it can create our Personal Access client and Password client for us. To do this we run the artisan command php artisan passport:install
.
Here is where we see our first issue with this change crop up. As the default Passport configuration is expecting to not need to set an id
field, it doesn't set one and we end up with a duplicated Primary column id
error. So let's take a look at the source and see what we can do.
Making a custom OAuth Client Model
The OAuth Client model isn’t expecting to have to set it’s own ID, by default it allows the database assign it an auto-incrementing ID. So how do we change this?
Well, because Eloquent Models defaults to having an integer identifier, the $incrementing
property is set to true and will therefore cast the id field as an integer when it is accessed. This will become an issue when going through the Authorization Code Flow in production.
This means that we need to have our own Client model that interacts with the oauth_clients
table in the same way that the Laravel Passport model does. We then need to tell Laravel to use our version instead of its.
After looking through the Laravel Passport source code, I can see that the OAuth Client model is retrieved by a static method from the Laravel\Passport\Passport
class. This means that we can overwrite this with relative ease if you know how static properties on classes work (If you don't, time for a quick lesson).
A static property defined on a class cannot be directly overwritten by a simple assignment, but it can be overwritten by a static method on the same class. All instances of this class will share this static property, so only update them when absolutely necessary.
Looking at the Laravel\Passport\Passport
class, we can find the following static property and method that will allow us to take over:
/**
* The client model class name.
*
* @var string
*/
public static $clientModel = 'Laravel\Passport\Client'; /**
* Set the client model class name.
*
* @param string $clientModel
* @return void
*/
public static function useClientModel($clientModel)
{
static::$clientModel = $clientModel;
}
This method allows us to provide a path to the model that Passport will use when trying to query OAuth Clients.
So, first we will need to make a new Client model in our app/Models/Passport
directory that extends the Laravel version to not interfere with other functionality.
We need to add two things to it, tell the Model not to increment IDs and in turn to not treat IDs as integers, and a way to attach a UUID on creation of a Client.
To do this we can set the $incrementing
property to false
which takes care of the first point.
To intercept a model that is about to be created and have the ability to modify its attributes we need to create a boot method on the model. The boot method allows us to hook into the Eloquent Model events that get dispatched when an action is about to take place or has taken place. In this instance we will hook into the creating
event and set a UUID on the model before it gets saved to the database.
Our model class should look a little like the below:
namespace App\Models\Passport;use Webpatser\Uuid\Uuid;
use Laravel\Passport\Client as OAuthClient; class Client extends OAuthClient
{
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = false; public static function boot()
{
static::creating(function ($model) {
$model->uuid = Uuid::generate()->string;
});
}
}
Now that we have this, we need to tell Laravel to use this instead of its version. To do this we go to our AppServiceProvider
and then go to the register
method just like before and then we call that Passport::useClientModel()
method that we looked at earlier, passing in the namespace of our custom model:
public function register()
{
Passport::ignoreMigrations();
Passport::useClientModel('App\Passport\Client');
}
We’re done!
That’s all we need to do, we’ve overwritten Laravel Passport’s default OAuth Client behaviour with our own and we now have UUIDs in place of the typical incremental IDs with no adverse effects.
Originally published at toms.dev.