Skip to content
All posts
LaravelAPISecurity

Laravel Sanctum with custom expiry time

February 10, 2022·Read on Medium·
source: laravelnews

Laravel Sanctum provides a simple token based authentication system suitable for any SPAs, mobile application integration and APIs. Sanctum allows each user of your application to generate multiple API tokens for their account. These tokens may be granted abilities / scopes which specify which actions the tokens are allowed to perform.

Sanctum also provide expiration for token which can be set in sanctum config. Once applied, the expiration affected for all token and compared with creation date of the token.

This’s great but, what if we want to differentiate expiration time for each of our guard? Current sanctum config is restricted and applied for all guards.

The Problem

The current situation is when we set 120 minutes in sanctum config

Guard A - 120 Minutes
Guard B - 120 Minutes
Guard C - 120 Minutes

What we want is like below which is dynamic

Guard A - 60 Minutes
Guard B - 120 Minutes
Guard C - 60 * 24 * 30 Minutes (1 month)

The Solution

The solution is quite easy with some manipulation on the current sanctum flow. Here is the idea,

  1. Add expiry date column in sanctum migration or create new migration to add the expiry date column
  2. Overwrite “createToken” method in “HasApiTokens” traits to accept new column
  3. Register new token model with extend of current PersonalAccessToken model and put expiry date column in fillable
  4. Add sanctum authenticate callback to validate the date expiration with the new column

Lets get started

Step 1

For the first timer, you may install Laravel Sanctum via the Composer package manager. If already install you may skip this step

composer require laravel/sanctum

Next, publish the Sanctum configuration and migration files.

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

Don’t run migration because we need to modify it

Step 2

New migration

You might notice the migration file for personal access token. Open the file

Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->bigIncrements('id');
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});

Add another column name “expired_at” after “last_used_at” column. Make it as nullable because expiry in sanctum is optional

....
$table->timestamp('expired_at')->nullable();
....

Next, run the migration command

Existing migration

Don’t worry. For those who already migrated, we can create a new migration file to add the expiry column. Just run a make:migration command

php artisan make:migration alter_personal_access_tokens_table

Edit the migration and add column name “expired_at” after “last_used_at” column with nullable type.

Schema::table('personal_access_tokens', function (Blueprint $table) {
$table->timestamp('expired_at')->after('last_used_at')->nullable();
});

Next, run the migration command

Step 3

Now, we need to overwrite “HasApiToken” traits method. If we go through trait code, we will notice that the creation of the token happen in “createToken” method.

public function createToken(string $name, array $abilities = ['*'])
{
$token = $this->tokens()->create([
'name' => $name,
'token' => REDACTED,
'abilities' => $abilities,
]);


.....
}

Here is the part where we need to overwrite and put our new column includes in the token creation.

Let’s say we have model named User using Sanctum trait

class User extends Authenticatable
{
use HasApiTokens;
}

Overwrite the createToken method inside User model and put the “expired_at” field with value of expiration in 3 hours for example

class User extends Authenticatable
{
use HasApiTokens; ...
... public function createToken(string $name, $abilities = ['*'])
{
$token = $this->tokens()->create([
'name' => $name,
'token' => REDACTED,
'abilities' => $abilities,
'expired_at' => now()->addHours(3)
]);

return .....;
}
}

Step 4

Next, because of we added new column in personal_access_token table, we need to overwrite Sanctum Personal Access Token model to add expired_at column in mass assignment fillable.

Create a model with the same name of simplicity and easy to remember

php artisan make:model PersonalAccessToken

After model created, extends the model with the Sanctum Personal Access Token model. Overwrite the fillable and casts variable. Add expired_at column into it like below

use Laravel\Sanctum\PersonalAccessToken as Model;

class
PersonalAccessToken extends Model
{
protected
$casts = [
'abilities' => 'json',
'last_used_at' => 'datetime',
'expired_at' => 'datetime'
];

protected
$fillable = [
'name',
'token',
'abilities',
'expired_at'
];
}

Then, based on Sanctum documentation, if you want to use your own Personal Access Token model, you need to register is into Sanctum inside AuthServiceProvider.php at boot method

Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);

So, the Sanctum will now use your model instead of default model

Step 5

Finally, register the callback for sanctum authentication. Why? because current Sanctum guard only validate the hash of the token and scopes. Luckily, Sanctum provides callback where we can put our own logic for validation. Here is the snippet of current Sanctum guard

protected function isValidAccessToken($accessToken): bool
{
if (! $accessToken) {
return false;
}

$isValid =
(! $this->expiration || $accessToken->created_at->gt(now()->subMinutes($this->expiration)))
&& $this->hasValidProvider($accessToken->tokenable);

if (is_callable(Sanctum::$accessTokenAuthenticationCallback)) {
$isValid = (bool) (Sanctum::$accessTokenAuthenticationCallback)($accessToken, $isValid);
}


return $isValid;
}

It’s show the valid of access token depends on this method. If a callback is assigned, the valid of the token depends on it. Here where we can put our logic to validate the expiration date with expired_at column

Edit the AuthServiceProvider.php class. On boot method, add after model registration

Sanctum::authenticateAccessTokensUsing(
static function (PersonalAccessToken $accessToken, bool $is_valid) {
// your logic here
}
);

Because of the add custom callback, it will affect all the usage whom using sanctum authentication. So, to overcome this, the logic is where if expired_at is not null, check with validity of the token and check if expired_at is past or not, else just return the actual validation value.

return $accessToken->expired_at ? $is_valid && !$accessToken->expired_at->isPast() : $is_valid;

Now we finish all the steps. Let’s test it

Testing

Create a simple api controller where to request a token

php artisan make:controller ApiAuthController

Add a method inside controller. You may follow the Laravel doc for token creation

class ApiAuthController extends Controller
{
public function token(Request $request)
{
if (!Auth::attempt($request->only(['email', 'password']))) {
abort(403);
}

$token = auth()->user()->createToken('Our Token');
return response()->json([
'token' => $token->plainTextToken,
'expired_at' =>
$token->accessToken->expired_at
]);

}
}

Here is the sample output if you succeed.

You might notice that, my sample output token is longer. It’s because of since we can overwrite createToken method, i decide to make it my own token style.

Conclusion

This solution doesn’t applied for one model only. You may applied for other model that you want to use Sanctum to authenticate. All you need is to overwrite with the thing as we learn.

It might not be the best practice since there is no other way we can do this. So, cheers 🍺

References

  • Laravel Documentation
  • Laravel API

Found this helpful?

If this article saved you time or solved a problem, consider supporting — it helps keep the writing going.

Originally published on Medium.

View on Medium
Laravel Sanctum with custom expiry time — Hafiq Iqmal — Hafiq Iqmal