Search code examples
phplaravelaccess-tokenlaravel-sanctum

Laravel Sanctum token expiration based on last usage


I have set the sanctum token expiration in the config file, for let's say 24 hours:

/*
|--------------------------------------------------------------------------
| Expiration Minutes
|--------------------------------------------------------------------------
|
| This value controls the number of minutes until an issued token will be
| considered expired. If this value is null, personal access tokens do
| not expire. This won't tweak the lifetime of first-party sessions.
|
*/

'expiration' => 24 * 60,

Doing that, my token gets invalid after this period of time. What I would like to do instead, is to check this expiration against the last_used_at attribute of the token.

For better understanding here is a simple example:

  • User logs in at Monday - 9:00 -> a new token is created with an expiration of 24 hours
  • still at Monday - 13:00 he makes a request -> token's last_used_at value is set to this time
  • Now the next day, at Tuesday - 11:00, the user wants to make a request... Validation fails because it is past 24 hours from the token creation. But it is still in the 24 hour window from the last usage.

After some digging in the source files I found the Guard, which does this check.

vendor/laravel/sanctum/src/Guard.php

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;
}

I think, that changing created_at to last_used_at would do exactly what I need, but the question is how to do it? Of course, I don't want to edit the vendor file.

What I've tried so far:

  • I created a custom middleware which checks the last_used_at value, but in the time the middleware gets called, the value is already set to the current time.
  • I added my custom validation in the AuthServiceProvider to the boot() method, where I did the check against the last_used_at value. This time I get the desired value, but the Guard is executed before this. So first happens the check against the created_at_value from the guard and the token is invalid by the time my custom validation is executed.

Solution

  • You can update sanctum Guard:

    1. Ignore sanctum service provider auto discover. Go to your composer.json file an add Laravel\Sanctum\SanctumServiceProvider to dont-discover array:
    "extra": {
        "laravel": {
            "dont-discover": [
                "Laravel\\Sanctum\\SanctumServiceProvider"
            ]
        }
    },
    
    1. Add your custom, extended sanctum service provider
    php artisan make:provider ExtendedSanctumServiceProvider
    
    1. Extend base SanctumServiceProvider and override createGuard method, adding your custom Guard. That would look like the next example
    
    <?php
    
    namespace App\Providers;
    
    use App\Guards\SanctumGuard;
    use Illuminate\Auth\RequestGuard;
    use Illuminate\Support\Facades\Log;
    use Laravel\Sanctum\SanctumServiceProvider;
    
    class ExtendedSanctumServiceProvider extends SanctumServiceProvider
    {
        /**
         * Register the guard.
         *
         * @param  \Illuminate\Contracts\Auth\Factory  $auth
         * @param  array  $config
         * @return RequestGuard
         */
        protected function createGuard($auth, $config)
        {
            return new RequestGuard(
                new SanctumGuard($auth, config('sanctum.expiration'), $config['provider']),
                request(),
                $auth->createUserProvider($config['provider'] ?? null)
            );
        }
    }
    
    1. Create the custom sanctum guard you defined below, extend base Sanctum Guard and override isValidAccessToken method
    <?php
    
    namespace App\Guards;
    
    use Laravel\Sanctum\Guard as BaseSanctumGuard;
    use Laravel\Sanctum\Sanctum;
    
    class SanctumGuard extends BaseSanctumGuard
    {
        /**
         * Determine if the provided access token is valid.
         *
         * @param  mixed  $accessToken
         * @return bool
         */
        protected function isValidAccessToken($accessToken): bool
        {
            if (! $accessToken) {
                return false;
            }
    
            $last_used_at = $accessToken->last_used_at;
            if(!$last_used_at) $last_used_at = $accessToken->created_at;
    
            $isValid =
                (! $this->expiration || $last_used_at->gt(now()->subMinutes($this->expiration)))
                && $this->hasValidProvider($accessToken->tokenable);
    
            if (is_callable(Sanctum::$accessTokenAuthenticationCallback)) {
                $isValid = (bool) (Sanctum::$accessTokenAuthenticationCallback)($accessToken, $isValid);
            }
    
            return $isValid;
        }
    }
    
    1. Register your Service Provider in app.php > 'providers'
    /*
    * Application Service Providers...
    */
    ...
    App\Providers\AppServiceProvider::class,
    App\Providers\ExtendedSanctumServiceProvider::class,
    .
    .
    .
    

    With all this, you are ready to go