Laravel Passport ACL: Managing Passport scopes via Database

  • July 26, 2024
  • 5 Min Read
Scopes with Laravel Passport

Laravel Passport is an excellent OAuth2 server implementation for any Laravel application that not only has authentication but also provides authorization features. Passport's token scopes act as an authorization permission to allow or stop users from performing actions on a resource.

You define all the token scopes in the Passport::tokensCan() method in the boot() method of your application's App\Providers\AuthServiceProvider class.

For more details on setting up and configuring Laravel Passport, you can refer to the official Laravel Passport documentation.

Problems with the Original Approach

There are many issues with this approach, let's look at them closely.

Manual addition of scopes

For every token scope you create, developers have to manually add the scope name in the Passport::tokensCan() method in App\Providers\AuthServiceProvider class. This leaves room for manual error.

Too many scopes

As your application grows, you will create multiple resources (API) and every resource supporting operations i.e. Create, Read, Update, Delete, etc. You will end up creating hundreds of token scopes. This makes it difficult to manage from a single App\Providers\AuthServiceProvider class.

Hitting HTTP authorization header max length

Although HTTP does not define any limit, most web servers do limit the size of headers they accept. Apache's default limit is 8KB, in IIS it's 16K. The server throws a ‘413 Entity Too Large’ error if the header size exceeds that limit.

Most web servers, like Apache, have limits on HTTP header sizes. You can find more details on the default header limits in Apache.

Slower response

Hundreds of token scopes lead to lengthy HTTP authorization header that leads to bulky headers that increase the time to send and receive HTTP requests and responses.

Any of the above challenges could disrupt the functionality of your operational application.

We came across these challenges while working on a SaaS product with 50k+ users. The solution we opted for is to manage the token scopes via database. It eliminates the first problem of manually adding the scopes in App\Providers\AuthServiceProvider class. As for problem 2, we cannot control the number of scopes because it depends on business demands.

Managing Token Scopes via Database

First Step

Write migrations to set up our database managing the token scopes.

NOTE: Assuming, you already have role_id column in the users table. If that's not the case, code changes are required which will be discussed later in the article.

  1. `role` table
                                            
                                                Schema::create(
                                                    'role', 
                                                    function (Blueprint $table) {
                                                    $table->id();
                                                    $table->string('name');
                                                    $table->string('alias');
                                                    $table->boolean('status')->default(1);
                                                    $table->timestamps();
                                                    $table->softDeletes();
                                                });
                                            
                                            
                                        
  2. `scopes` table

    This table contains all the token scopes our application demands.

                                            
                                                Schema::create(
                                                    'scopes', 
                                                    function (Blueprint $table) {
                                                    $table->id();
                                                    $table->string('name')->unique();
                                                    $table->text('description')->nullable();
                                                    $table->boolean('status')->default(1);
                                                    $table->timestamps();
                                                    $table->softDeletes();
                                                });
                                            
    
                                        
    NOTE: Please ensure the name column is unique.
  3. `role_scopes` table

    This table contains all the token scopes related to a role.

                                            
                                                Schema::create(
                                                    'role_scopes', 
                                                    function (Blueprint $table) {
                                                    $table->id();
                                                    $table->foreignId('role_id')
                                                        ->constrained('roles')
                                                        ->onUpdate('no action')
                                                        ->onDelete('no action');
                                                    $table->foreignId('scope_id')
                                                        ->constrained('scopes')
                                                        ->onUpdate('no action')
                                                        ->onDelete('no action');
                                                    $table->boolean('status')
                                                        ->default(1)
                                                        ->comment('1 = Active, 0 = Inactive');
                                                    $table->timestamps();
                                                    $table->softDeletes();
                                                });
                                            
    
                                        
  4. `user_scopes` table

    This table contains all the extra token scopes related to a specific user.

                                            
                                                Schema::create(
                                                    'user_scopes', 
                                                    function (Blueprint $table) {
                                                    $table->id();
                                                    $table->foreignId('user_id')
                                                        ->constrained('users')
                                                        ->onUpdate('no action')
                                                        ->onDelete('no action');
                                                    $table->foreignId('scope_id')
                                                        ->constrained('scopes')
                                                        ->onUpdate('no action')
                                                        ->onDelete('no action');
                                                    $table->timestamps();
                                                    $table->softDeletes();
                                                });
                                            
    
                                        

Execute php artisan migrate to run all the migrations.

Second Step

Create models for the above migrations.

  1. App\Models\Role
                                                                                        
                                                <?php
                                                    namespace App\Models;
    
                                                    use Illuminate\Database\Eloquent\SoftDeletes;
                                                    use Illuminate\Database\Eloquent\Model;
                                                    use Illuminate\Database\Eloquent\Factories\HasFactory;
    
                                                    class Role extends Model
                                                    {
                                                        use SoftDeletes, HasFactory;
    
                                                        /**
                                                         * The table associated with the model.
                                                         *
                                                         * @var string
                                                         */
                                                        protected $table = 'roles';
    
                                                        /**
                                                         * The attributes that aren't mass assignable.
                                                         *
                                                         * @var array
                                                         */
                                                        protected $guarded = [];
    
                                                        /**
                                                         * @return 
                                                         *\Illuminate\Database\Eloquent\Relations\BelongsToMany
                                                         */
                                                        public function scopes()
                                                        {
                                                            return $this->belongsToMany(
                                                                Scope::class,
                                                                'role_scopes',
                                                                'role_id', 
                                                                'scope_id'
                                                            );
                                                        }
                                                    }
    
                                            
    
                                        
  2. App\Models\Scope
                                            
                                                <?php
                                                    namespace App\Models;
    
                                                    use Illuminate\Database\Eloquent\Factories\HasFactory;
                                                    use Illuminate\Database\Eloquent\Model;
                                                    use Illuminate\Database\Eloquent\SoftDeletes;
    
                                                    class Scope extends Model
                                                    {
                                                        use SoftDeletes, HasFactory;
    
                                                        /**
                                                        * The table associated with the model.
                                                        *
                                                        * @var string
                                                        */
                                                        protected $table = 'scopes';
    
                                                        /**
                                                        * The attributes that aren't mass assignable.
                                                        *
                                                        * @var array
                                                        */
                                                        protected $guarded = [];
                                                    }
                                            
                                        
  3. Similarly create the App\Models\RoleScope, and App\Models\UserScope models.

Third Step

Fetch the scopes from the database to define them in the Passport::tokensCan() method in App\Providers\AuthServiceProvider class. There are various techniques by which you can achieve this.

  • Creating a helper method in the helper file.
  • Creating a helper method in the helper file.

We opted to create a battle-tested utility library and a dedicated Facade leveraging Laravel's service container capabilities.

  1. Create permission utility class App\Utilities\Permission\Permission.
                                                                                        
                                                <?php
                                                    namespace App\Utilities\Permission;
    
                                                    use App\Models\Role;
                                                    use App\Models\RoleScope;
                                                    use App\Models\Scope;
                                                    use App\Models\User;
                                                    use App\Models\UserScope;
                                                    use Illuminate\Support\Arr;
                                                    use Illuminate\Support\Facades\Schema;
    
                                                    class Permission
                                                    {
                                                        /**
                                                        * Get all scopes from the database
                                                        *
                                                        * @return array
                                                        */
                                                        public function scopes(): array
                                                        {
                                                            if (cache()->has('scopes')) {
                                                                $scopes = cache('scopes');
                                                            } else {
                                                                $scopes = [];
    
                                                                if (Schema::hasTable('scopes')) {
                                                                    $scopes = cache()->rememberForever(
                                                                        'scopes', function () {
                                                                        return Scope::select(
                                                                            'id', 
                                                                            'name', 
                                                                            'description
                                                                            ')
                                                                            ->where('status', 1)
                                                                            ->get()
                                                                            ->mapwithKeys(function ($scope) {
                                                                                return [$scope->id => $scope];
                                                                            })
                                                                            ->toArray();
                                                                    });
                                                                }
                                                            }
    
                                                            return $scopes;
                                                        }
    
                                                        /**
                                                        * Prepare array of scopes for passport
                                                        *
                                                        * @return array
                                                        */
                                                        public function passportScopes(): array
                                                        {
                                                            $scopes = collect($this->scopes());
    
                                                            return $scopes->mapWithKeys(function ($scope) {
                                                                return [
                                                                    $scope['name'] => $scope['description']
                                                                    ];
                                                            })->toArray();
                                                        }
    
                                                        /**
                                                        * Get all roles from the database
                                                        *
                                                        * @return array
                                                        */
                                                        public function roles(): array
                                                        {
                                                            if (cache()->has('roles')) {
                                                                $roles = cache('roles');
                                                            } else {
                                                                $roles = [];
    
                                                                if (Schema::hasTable('roles')) {
                                                                    $roles = cache()->rememberForever(
                                                                        'roles', function () {
                                                                        return Role::select(
                                                                            'id', 
                                                                            'name', 
                                                                            'alias')
                                                                            ->where('status', 1)
                                                                            ->get()
                                                                            ->toArray();
                                                                    });
                                                                }
                                                            }
    
                                                            return $roles;
                                                        }
    
                                                        /**
                                                        * Get all the scopes for a role
                                                        *
                                                        * @param  string|int|null|array  $roleId
                                                        * @param  bool  $ids
                                                        * @return array
                                                        */
                                                        public function roleScopes(
                                                            string|int|array $roleId = null, bool $ids = false
                                                            ): array
                                                        {
                                                            if (cache()->has('role_scopes')) {
                                                                $roleScopes = cache('role_scopes');
                                                            } else {
                                                                $roleScopes = [];
    
                                                                if (Schema::hasTable('role_scopes')) {
                                                                    $roleScopes = cache()->rememberForever(
                                                                        'role_scopes', function () {
                                                                        return RoleScope::select(
                                                                            'role_id', 
                                                                            'scope_id')
                                                                            ->where('status', 1)
                                                                            ->get()
                                                                            ->mapToGroups(function (
                                                                                $scope, 
                                                                                $key
                                                                                ) {
                                                                                return [
                                                                                    $scope->role_id 
                                                                                    => $scope->scope_id
                                                                                    ];
                                                                            })
                                                                            ->toArray();
                                                                    });
                                                                }
                                                            }
    
                                                            if ($roleId && count($roleScopes)) {
                                                                if (is_array($roleId)) {
                                                                    $scopes = [];
    
                                                                    foreach ($roleId as $id) {
                                                                        if (isset($roleScopes[$id])) {
                                                                            $scopes[] = $roleScopes[$id];
                                                                        }
                                                                    }
    
                                                                    $roleScopes = 
                                                                    array_unique(Arr::collapse($scopes));
                                                                } else {
                                                                    $roleScopes = isset($roleScopes[$roleId]) 
                                                                    ? $roleScopes[$roleId] : [];
                                                                }
                                                            }
    
                                                            if (! $ids) {
                                                                $roleScopeNames = [];
                                                                $scopes = $this->scopes();
                                                                $roleScopes = $roleId 
                                                                ? $roleScopes : array_unique(
                                                                    Arr::collapse($roleScopes)
                                                                    );
    
                                                                foreach ($roleScopes as $scopeId) {
                                                                    if (isset($scopes[$scopeId]) && ! in_array(
                                                                        $scopes[$scopeId]['name'], 
                                                                        $roleScopeNames
                                                                        )) {
                                                                        array_push(
                                                                            $roleScopeNames, 
                                                                            $scopes[$scopeId]['name']
                                                                            );
                                                                    }
                                                                }
    
                                                                $roleScopes = $roleScopeNames;
                                                            }
    
                                                            return $roleScopes;
                                                        }
    
                                                        /**
                                                        * Get all the scopes for a user
                                                        *
                                                        * @param  string|int|null  $userId
                                                        * @param  string|int  $roleId
                                                        * @return array
                                                        */
                                                        public function userScopes(
                                                            string|int $userId = null, 
                                                            string|int $roleId
                                                            ): array
                                                        {
                                                            if (cache()->has('user_scopes')) {
                                                                $userScopes = cache('user_scopes');
                                                            } else {
                                                                $userScopes = [];
    
                                                                if (Schema::hasTable('user_scopes')) {
                                                                    $userScopes = cache()
                                                                    ->rememberForever(
                                                                        'user_scopes', function () {
                                                                        return UserScope::select(
                                                                            'user_id', 
                                                                            'scope_id'
                                                                            )
                                                                            ->get()
                                                                            ->mapToGroups(function (
                                                                                $scope, 
                                                                                $key
                                                                                ) {
                                                                                return [
                                                                                    $scope->user_id => [
                                                                                        'scope_id' => $scope
                                                                                        ->scope_id,
                                                                                    ],
                                                                                ];
                                                                            })
                                                                            ->toArray();
                                                                    });
                                                                }
                                                            }
    
                                                            if ($userId) {
                                                                $userScopeIds = $this->roleScopes(
                                                                    $roleId, 
                                                                    true
                                                                    );
    
                                                                if (count($userScopes) 
                                                                && isset($userScopes[$userId])) {
                                                                    foreach ($userScopes[$userId] as $scope) {
                                                                        if (in_array(
                                                                            $scope['scope_id'], 
                                                                            $userScopeIds
                                                                            )) {
                                                                            $userScopeIds = array_diff(
                                                                                $userScopeIds, 
                                                                                [$scope['scope_id']]
                                                                                );
                                                                        }
                                                                    }
                                                                }
    
                                                                $userScopeIds = array_unique($userScopeIds);
                                                                $userScopeNames = [];
                                                                $scopes = $this->scopes();
    
                                                                foreach ($userScopeIds as $scopeId) {
                                                                    if (isset($scopes[$scopeId]) 
                                                                    && ! in_array(
                                                                        $scopes[$scopeId]['name'], 
                                                                        $userScopeNames
                                                                        )) {
                                                                        array_push(
                                                                            $userScopeNames, 
                                                                            $scopes[$scopeId]['name']
                                                                            );
                                                                    }
                                                                }
    
                                                                $userScopes = $userScopeNames;
                                                            }
    
                                                            return $userScopes;
                                                        }
    
                                                        /**
                                                        * Update scopes of all the affected users after a scope
                                                        * is created/updated/deleted.
                                                        *
                                                        * @param  string|int  $id
                                                        * @param  string  $type
                                                        * @return void
                                                        */
                                                        public function scopesChanged(
                                                            string|int $id, 
                                                            string $type = 'role'
                                                            )
                                                        {
                                                            $this->clearScopesCache();
                                                            $users = $this->affectedUsersAfterScopesChanged(
                                                                $id, 
                                                                $type
                                                                );
    
                                                            if ($users && count($users)) {
                                                                $this->refreshUserScopes($users);
                                                            }
    
                                                            $this->createScopesCache();
                                                        }
    
                                                        /**
                                                        * List of all users who are affected from a Scope.
                                                        *
                                                        * @param  string|int  $id
                                                        * @param  string  $type
                                                        * @return array
                                                        */
                                                        public function affectedUsersAfterScopesChanged(
                                                            string|int $id, 
                                                            string $type = 'role'
                                                            ): array
                                                        {
                                                            $users = [];
    
                                                            if ($type === 'role') {
                                                                $users = User::where('role_id', $id)
                                                                ->pluck('id')
                                                                ->toArray();
                                                            } elseif ($type === 'user') {
                                                                $users = [$id];
                                                            }
    
                                                            return $users;
                                                        }
    
                                                        /**
                                                        * Update all user scopes in tokens table.
                                                        *
                                                        * @param  array  $ids
                                                        * @return void
                                                        */
                                                        public function refreshUserScopes(array $ids)
                                                        {
                                                            foreach (User::find($ids) as $user) {
                                                                if ($user->tokens->count()) {
                                                                    $userScopes = $this->userScopes(
                                                                        $user->id, 
                                                                        false, 
                                                                        $user->role_id);
    
                                                                    foreach ($user->tokens as $token) {
                                                                        if (! $token->revoked ||
                                                                         $token->expires_at->gt(now())) {
                                                                            $token->scopes = $userScopes;
                                                                            $token->save();
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                        }
    
                                                        /**
                                                        * Clear scopes from cache storage.
                                                        *
                                                        * @return void
                                                        */
                                                        public function clearScopesCache()
                                                        {
                                                            cache()->forget('scopes');
                                                            cache()->forget('role_scopes');
                                                            cache()->forget('user_scopes');
                                                        }
    
                                                        /**
                                                        * Add scopes data to cache storage.
                                                        *
                                                        * @return void
                                                        */
                                                        public function createScopesCache()
                                                        {
                                                            $this->scopes();
                                                            $this->roleScopes();
                                                            $this->userScopes();
                                                        }
                                                    }
    
                                            
                                        

    Note how we utilized the cache()->rememberForever() method to avoid database queries on every request just to fetch the API scopes.

    Now, there is a crucial method in the App\Utilities\Permission\Permission class that requires explanation.

    • scopesChanged()
      Call this method only when a scope is created, updated, or deleted from our database. This will automatically find all the users who are affected by this scope, also destroying and re-creating the user scopes. We will learn its usage later in this article.

    NOTE: If your application doesn't have role_id column in the users table, please replace the $user->role_id as per your database setup in the affectedUsersAfterScopesChanged() & refreshUserScopes() methods as discussed in the First Step.

  2. Create a facade App\Facades\Utilities\Permission.
                                            
                                                <?php
                                                    namespace App\Facades\Utilities;
    
                                                    use Illuminate\Support\Facades\Facade;
    
                                                    class Permission extends Facade
                                                    {
                                                        /**
                                                        * Get the registered name of the component.
                                                        *
                                                        * @return string
                                                        */
                                                        protected static function getFacadeAccessor()
                                                        {
                                                            return 'permission';
                                                        }
                                                    }
                                            
    
                                        
  3. Create and register the App\Providers\PermissionServiceProvider.

    Execute php artisan make:provider PermissionServiceProvider to create a new service provider.

                                            
                                                <?php
                                                    namespace App\Providers;
    
                                                    use App\Utilities\Permission\Permission;
                                                    use Illuminate\Support\ServiceProvider;
    
                                                    class PermissionServiceProvider extends ServiceProvider
                                                    {
                                                        /**
                                                        * Register services.
                                                        *
                                                        * @return void
                                                        */
                                                        public function register()
                                                        {
                                                            $this->app->bind('permission', function () {
                                                                return new Permission();
                                                            });
                                                        }
    
                                                        ...
                                                    }
                                            
    
                                        

    Register the PermissionServiceProvider in the config/app.php file.

                                            
                                                <?php
                                                    use Illuminate\Support\Facades\Facade;
    
                                                    return [
    
                                                        ...
    
                                                        'providers' => [
    
                                                            ...
    
                                                            /*
                                                            * Application Service Providers...
                                                            */
                                                            App\Providers\AppServiceProvider::class,
                                                            App\Providers\PermissionServiceProvider::class,
                                                        ],
    
                                                        ...
                                                    ];
                                            
    
                                        

Fourth Step

Create RoleScopeObserver and UserScopeObserver classes to track the scope changes in the database and take the necessary steps by executing the following commands:

                                
                                    php artisan make:observer RoleScopeObserver
                                    php artisan make:observer UserScopeObserver                                    
                                
                            

Remember, we declared the scopesChanged() method in the App\Utilities\Permission\Permission class to track the scope changes in the database. Now's the time to call that method from the observers.

  1. App\Observers\RoleScopeObserver
                                            
                                                <?php
                                                    namespace App\Observers;
    
                                                    use App\Facades\Utilities\Permission;
                                                    use App\Models\RoleScope;
    
                                                    class RoleScopeObserver
                                                    {
                                                        /**
                                                        * Handle the RoleScope "created" event.
                                                        *
                                                        * @param  \App\Models\RoleScope  $roleScope
                                                        * @return void
                                                        */
                                                        public function created(RoleScope $roleScope)
                                                        {
                                                            Permission::scopesChanged($roleScope->role_id);
                                                        }
    
                                                        /**
                                                        * Handle the RoleScope "updated" event.
                                                        *
                                                        * @param  \App\Models\RoleScope  $roleScope
                                                        * @return void
                                                        */
                                                        public function updated(RoleScope $roleScope)
                                                        {
                                                            Permission::scopesChanged($roleScope->role_id);
                                                        }
    
                                                        /**
                                                        * Handle the RoleScope "deleted" event.
                                                        *
                                                        * @param  \App\Models\RoleScope  $roleScope
                                                        * @return void
                                                        */
                                                        public function deleted(RoleScope $roleScope)
                                                        {
                                                            Permission::scopesChanged($roleScope->role_id);
                                                        }
    
                                                        /**
                                                        * Handle the RoleScope "restored" event.
                                                        *
                                                        * @param  \App\Models\RoleScope  $roleScope
                                                        * @return void
                                                        */
                                                        public function restored(RoleScope $roleScope)
                                                        {
                                                            Permission::scopesChanged($roleScope->role_id);
                                                        }
    
                                                        /**
                                                        * Handle the RoleScope "force deleted" event.
                                                        *
                                                        * @param  \App\Models\RoleScope  $roleScope
                                                        * @return void
                                                        */
                                                        public function forceDeleted(RoleScope $roleScope)
                                                        {
                                                            Permission::scopesChanged($roleScope->role_id);
                                                        }
                                                    }
                                    
                                            
                                        
  2. App\Observers\UserScopeObserver
                                            
                                                <?php
                                                    namespace App\Observers;
    
                                                    use App\Facades\Utilities\Permission;
                                                    use App\Models\UserScope;
    
                                                    class UserScopeObserver
                                                    {
                                                        /**
                                                        * Handle the UserScope "created" event.
                                                        *
                                                        * @param  \App\Models\UserScope  $userScope
                                                        * @return void
                                                        */
                                                        public function created(UserScope $userScope)
                                                        {
                                                            Permission::scopesChanged(
                                                                $userScope->user_id, 
                                                                'user'
                                                                );
                                                        }
    
                                                        /**
                                                        * Handle the UserScope "updated" event.
                                                        *
                                                        * @param  \App\Models\UserScope  $userScope
                                                        * @return void
                                                        */
                                                        public function updated(UserScope $userScope)
                                                        {
                                                            Permission::scopesChanged(
                                                                $userScope->user_id, 
                                                                'user'
                                                                );
                                                        }
    
                                                        /**
                                                        * Handle the UserScope "deleted" event.
                                                        *
                                                        * @param  \App\Models\UserScope  $userScope
                                                        * @return void
                                                        */
                                                        public function deleted(UserScope $userScope)
                                                        {
                                                            Permission::scopesChanged(
                                                                $userScope->user_id,
                                                                 'user'
                                                                 );
                                                        }
    
                                                        /**
                                                        * Handle the UserScope "restored" event.
                                                        *
                                                        * @param  \App\Models\UserScope  $userScope
                                                        * @return void
                                                        */
                                                        public function restored(UserScope $userScope)
                                                        {
                                                            Permission::scopesChanged(
                                                                $userScope->user_id, 
                                                                'user'
                                                                );
                                                        }
    
                                                        /**
                                                        * Handle the UserScope "force deleted" event.
                                                        *
                                                        * @param  \App\Models\UserScope  $userScope
                                                        * @return void
                                                        */
                                                        public function forceDeleted(UserScope $userScope)
                                                        {
                                                            Permission::scopesChanged(
                                                                $userScope->user_id, 
                                                                'user'
                                                                );
                                                        }
                                                    }
                                    
                                            
                                        
  3. Register the observers in the boot() method of App\Providers\EventServiceProvider class.
                                            
                                                <?php
                                                   namespace App\Providers;
    
                                                    use App\Models\RoleScope;
                                                    use App\Models\UserScope;
                                                    use App\Observers\RoleScopeObserver;
                                                    use App\Observers\UserScopeObserver;
    
                                                    class EventServiceProvider extends ServiceProvider
                                                    {
                                                        ..
    
                                                        /**
                                                        * Register any events for your application.
                                                        *
                                                        * @return void
                                                        */
                                                        public function boot()
                                                        {
                                                            RoleScope::observe(RoleScopeObserver::class);
                                                            UserScope::observe(UserScopeObserver::class);
                                                        }
    
                                                        ...
                                                    }                                
                                            
                                        

    We can now distinguish whether the scope changes were intended for a role or a user by creating separate observers.

Final Step

Call the passportScopes() method from App\Utilities\Permission\Permission library in the AuthServiceProvider class.

                                
                                    <?php
                                        namespace App\Providers;

                                        use App\Facades\Utilities\Permission;
                                        use Illuminate\Foundation\Support\Providers\AuthServiceProvider 
                                        as ServiceProvider;
                                        use Laravel\Passport\Passport;

                                        class AuthServiceProvider extends ServiceProvider
                                        {
                                            ...

                                            /**
                                            * Register any authentication / authorization services.
                                            *
                                            * @return void
                                            */
                                            public function boot()
                                            {
                                                ...

                                                Passport::tokensCan(Permission::passportScopes());

                                                ...
                                            }

                                            ...
                                        }                                    
                                
                            

After setting up your application with all of the above changes, this is how you will add a new token scope in your application:

  1. Create migration to create/update/delete a scope and assign it to a role or a user.
                                            
                                                <?php
                                                    use App\Models\Role;
                                                    use App\Models\RoleScope;
                                                    use App\Models\Scope;
                                                    use Illuminate\Database\Migrations\Migration;
                                                    use Illuminate\Support\Facades\Schema;
    
                                                    return new class extends Migration
                                                    {
                                                        /**
                                                        * Run the migrations.
                                                        *
                                                        * @return void
                                                        */
                                                        public function up()
                                                        {
                                                            $scope = [
                                                                'name' => 'customer-list',
                                                                'description' => 'List of all customers',
                                                            ];
    
                                                            if (Schema::hasTable('scopes')) {
                                                                $scope = Scope::create($scope);
    
                                                                if ($scope) {
                                                                    // Assign it to role
                                                                    RoleScope::create([
                                                                        'role_id' => 1,
                                                                        'scope_id' => $scope->id,
                                                                    ]);
    
                                                                    // Assign it to a user
                                                                    // UserScope::create([
                                                                    //    'role_id' => 1,
                                                                    //    'scope_id' => $scope->id,
                                                                    // ]);
                                                                }
                                                            }
                                                        }
    
                                                        /**
                                                        * Reverse the migrations.
                                                        *
                                                        * @return void
                                                        */
                                                        public function down()
                                                        {
                                                            $scopeName = 'customer-list';
    
                                                            if (Schema::hasTable('scopes')) {
                                                                $scope = Scope::select('id')
                                                                ->where('name', $scopeName)
                                                                ->first();
    
                                                                if ($scope) {
                                                                    RoleScope::where([
                                                                        ['scope_id', '=', $scope->id],
                                                                        ['role_id', '=', 2],
                                                                    ])->delete();
                                                                }
                                                            }
                                                        }
                                                    };
    
                                                                                
                                            
                                        
  2. RoleScopeObserver & UserScopeObserver will activate automatically once the scopes are created/updated/deleted in the database.

    But, we still have problems to solve.

    1. How can we expect the frontend team to pass the scopes without knowing the user role and ID, as the user details are revealed once they are authenticated? Answer: Pass an empty value in the scope parameter while hitting the login API /oauth/token
    2. How will Laravel handle the user authorization as the scope details are not included while creating the Passport access token? Further, we can't even utilize the scope middleware ->middleware('scope:customer-list') in our routes.

      Answer: This is a tricky problem, and the solution is equally interesting. But first, let's understand how Laravel determines the scope of an authenticated user:

      • Passport access token contains the list of all authorized scopes of an authenticated user that can be decoded by pasting the access token into https://jwt.io website.
      • While authorization, Laravel does not match the scope from the route scope middleware with the list of all authorized scopes from the Passport access token of an authenticated user. Instead, it matches the scope with the scopes column from the oauth_access_tokens table.

Deep diving into the Laravel Passport's core files, we discovered an Laravel\Passport\Events\AccessTokenCreated event that is dispatched every time a new access token is created for a user.

Such findings redirected our approach and we figured out that we need to create a listener that listens to the AccessTokenCreated event and update the scopes in the oauth_access_tokens table. So, here comes the actual final step.

Final-Final Step

  1. Create a Listener class App\Listeners\RefreshUserScopes by executing the php artisan make:listener RefreshUserScopes command.
                                            
                                                <?php
                                                    namespace App\Listeners;
    
                                                    use App\Facades\Utilities\Permission;
                                                    use App\Models\OauthAccessToken;
                                                    use Laravel\Passport\Events\AccessTokenCreated;
    
                                                    class RefreshUserScopes
                                                    {
                                                        /**
                                                        * Create the event listener.
                                                        *
                                                        * @return void
                                                        */
                                                        public function __construct()
                                                        {
                                                            //
                                                        }
    
                                                        /**
                                                        * Handle the event.
                                                        *
                                                        * @param  
                                                        * \Laravel\Passport\Events\AccessTokenCreated  $event
                                                        * @return void
                                                        */
                                                        public function handle(AccessTokenCreated $event)
                                                        {
                                                            OauthAccessToken::where('id', $event->tokenId)
                                                                ->update(
                                                                    ['scopes' => Permission::userScopes(
                                                                        $event->userId
                                                                        )
                                                                    ]);
                                                        }
                                                    }
                            
                                            
                                        
  2. Register the App\Listeners\RefreshUserScopes listener in the boot() method of the App\Providers\EventServiceProvider class.
                                            
                                                <?php
                                                    namespace App\Providers;
    
                                                    use App\Listeners\RefreshUserScopes;
                                                    use Laravel\Passport\Events\AccessTokenCreated;
    
                                                    class EventServiceProvider extends ServiceProvider
                                                    {
                                                        /**
                                                        * The event listener mappings for the application.
                                                        *
                                                        * @var array>
                                                        */
                                                        protected $listen = [
                                                            ...
    
                                                            AccessTokenCreated::class => [
                                                                RefreshUserScopes::class,
                                                            ],
    
                                                            ...
                                                        ];
    
                                                        ...
                                                    }
    
                                                    
                                            
                                        

    With this, we solve the 3rd & 4th problems discussed initially of HTTP authorization header max length & slower response as the:

    • Access token length does not increase as the scope increases. Now, the scopes are not a part of the access token.
    • Faster response because of the lightweight access token.

In conclusion

You've now built a comprehensive ACL or a Permission Management System to manage the passport token scopes via database without losing or compromising Laravel's built-in magic. The beauty of this setup is, that if you ever want to ditch the Laravel Passport's authorization and create your own authorization logic, you already have a database-managed setup and zero dependency on Laravel Passport by simply replacing the scope middleware ->middleware('scope:customer-list') from routes with your own middleware.

Why wait? Try this in your existing or new applications and do share your queries, we are more than happy to work on them with you.

Stay in the Know

Get ahead with TechUp Labs' productivity tips & latest tech trend resources