Authorization
Authorization involves defining roles and permissions to enhance security and control access to specific features.
INFO
Make sure to add /api prefix in the API endpoint.
Preview of Authorization Implementation
In this case, I'll take an example from a user list page that can only be accessed by certain roles. Let's consider two roles: Admin
and Operator
.
- Operator, with the permission, can access the component.
Operator view
- Admin, without the permission, cannot access the component that are only available to the Operator.
Admin view
Get Abilities Endpoint
GET /abilities
Headers
- Content-Type: application/json
- Authorization: Bearer {token}
Responses
200
OK
json
[
"dashboard",
"users.index",
"users.show",
"users.create",
"users.update",
"users.delete",
"ref_employee_types.index",
"ref_year_types.index",
"ref_loan_types.index",
"mst_configs.index",
"mst_configs.show",
"mst_configs.create",
// and so on...
]
[
"dashboard",
"users.index",
"users.show",
"users.create",
"users.update",
"users.delete",
"ref_employee_types.index",
"ref_year_types.index",
"ref_loan_types.index",
"mst_configs.index",
"mst_configs.show",
"mst_configs.create",
// and so on...
]
401
Unauthorized
Possibilities:
- User is not active
json
{
"message": "Unauthenticated."
}
{
"message": "Unauthenticated."
}
Authorization BE Implementation
- Create 4 tables:
roles
andpermissions
with a name column, as well aspermission_role
androle_user
pivot tables. - Ensure
app.php
file contains theproviders
key, which references the AuthServiceProvider class.app.php
php'providers' => [ // Other Service Providers... App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, ],
'providers' => [ // Other Service Providers... App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, ],
- Define gates in the AuthServiceProvider class and create a registerUserAccessToGates function. This function starts by retrieving available permissions and then checks whether the user has those permissions or not.
AuthServiceProvider.php
phpclass AuthServiceProvider extends ServiceProvider { public function boot() { $this->registerPolicies(); $this->registerUserAccessToGates(); } protected function registerUserAccessToGates() { try { foreach (Permission::pluck('name') as $permission) { Gate::define($permission, function ($user) use ($permission) { return $user->roles()->whereHas('permissions', function ($q) use ($permission) { $q->where('name', $permission); })->count() > 0; }); } } catch (\Exception $e) { info('registerUserAccessToGates: Database not found or not yet migrated. Ignoring user permissions while booting app.'); } } }
class AuthServiceProvider extends ServiceProvider { public function boot() { $this->registerPolicies(); $this->registerUserAccessToGates(); } protected function registerUserAccessToGates() { try { foreach (Permission::pluck('name') as $permission) { Gate::define($permission, function ($user) use ($permission) { return $user->roles()->whereHas('permissions', function ($q) use ($permission) { $q->where('name', $permission); })->count() > 0; }); } } catch (\Exception $e) { info('registerUserAccessToGates: Database not found or not yet migrated. Ignoring user permissions while booting app.'); } } }
- i.e add the
authorize
function to thestore
function in the MstConfigController class. This function will check whether the user has the permission to create a new master config or not.MstConfigController.php
phpclass MstConfigController extends Controller { public function store(MstConfigStoreRequest $mstConfigStoreRequest, MstConfigStoreAction $mstConfigStoreAction) { $this->authorize(MstConfig::TABLE_NAME . '.create'); try { DB::beginTransaction(); $mstConfig = $mstConfigStoreAction->execute($mstConfigStoreRequest); DB::commit(); return response(['message' => 'Data master config berhasil dibuat', 'data' => new MstConfigStoreResource($mstConfig)], 201); } catch (\Exception $exception) { DB::rollBack(); Log::error($exception->getMessage()); return response(['message' => $exception->getMessage()], 422); } } }
class MstConfigController extends Controller { public function store(MstConfigStoreRequest $mstConfigStoreRequest, MstConfigStoreAction $mstConfigStoreAction) { $this->authorize(MstConfig::TABLE_NAME . '.create'); try { DB::beginTransaction(); $mstConfig = $mstConfigStoreAction->execute($mstConfigStoreRequest); DB::commit(); return response(['message' => 'Data master config berhasil dibuat', 'data' => new MstConfigStoreResource($mstConfig)], 201); } catch (\Exception $exception) { DB::rollBack(); Log::error($exception->getMessage()); return response(['message' => $exception->getMessage()], 422); } } }
Authorization FE Implementation
- @casl/vue and @casl/ability packages must be installed
- Get abilities endpoint must be implemented
- Utilize the
can
function andrules
from AbilityBuilder withAbility
as a parameter. Insert permissions into thecan()
function and update the ability.useAuth.js
jsimport { AbilityBuilder, Ability } from '@casl/ability'; import { ABILITY_TOKEN } from '@casl/vue'; export default function useAuth() { const config = useConfig() const ability = inject(ABILITY_TOKEN) const permissions = usePermissions() // from states.js const bearerToken = useToken() // from states.js const getAbilities = async () => { assignToken() await $fetch(`${config.BASE_URL}/api/abilities`, { method: 'GET', credentials: 'omit', headers: { Authorization: `Bearer ${bearerToken.value}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }) .then(response => { permissions.value = response const { can, rules } = new AbilityBuilder(Ability) can(permissions.value) ability.update([ { subject: 'all', action: permissions.value } ]) }) .catch(error => { navigateTo('/login') }) } }
import { AbilityBuilder, Ability } from '@casl/ability'; import { ABILITY_TOKEN } from '@casl/vue'; export default function useAuth() { const config = useConfig() const ability = inject(ABILITY_TOKEN) const permissions = usePermissions() // from states.js const bearerToken = useToken() // from states.js const getAbilities = async () => { assignToken() await $fetch(`${config.BASE_URL}/api/abilities`, { method: 'GET', credentials: 'omit', headers: { Authorization: `Bearer ${bearerToken.value}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }) .then(response => { permissions.value = response const { can, rules } = new AbilityBuilder(Ability) can(permissions.value) ability.update([ { subject: 'all', action: permissions.value } ]) }) .catch(error => { navigateTo('/login') }) } }
- Call the
getAbilities
function within the authentication or user login process.useAuth.js
jsconst loginUser = async (response) => { userUuid = response.data.uuid userUuid.replace(/['"]+/g, '') userProfile.value.name = response.data.name userProfile.value.uuid = response.data.uuid userProfile.value.email_verified_at = response.data.email_verified_at userProfile.value.groups = response.data.groups userProfile.value.token = response.token userProfile.value.group_id = response.data.group_mode_id if (userProfile.value.name === '') { localStorage.setItem('loggedIn', JSON.stringify(false)) } else { if (process.client) { localStorage.setItem('loggedIn', JSON.stringify(true)) localStorage.setItem('token', JSON.stringify(userProfile.value.token)) localStorage.setItem('userUuid', JSON.stringify(userUuid)) } await getAbilities() // <--- call getAbilities() here if (userProfile.value.groups.length === 1) { userProfile.value.group_id = userProfile.value.groups[0]['id'] userProfile.value.group = userProfile.value.groups[0]['name'] } if ( userProfile.value.group_id === groupMember || !userProfile.value.group_id ) { await navigateTo(`/master/users/${userUuid}`) } else if (userProfile.value.group_id && userProfile.value.group_id !== groupMember) { await navigateTo('/') } } } const getUser = async () => { assignToken() await $fetch(`${config.BASE_URL}/api/user`, { method: 'GET', credentials: 'omit', headers: { Authorization: `Bearer ${bearerToken.value}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }) .then(response => { if (userProfile.value.group_id === groupMember) { navigateTo(`/master/users/${userProfile.value.uuid}`) } let shortUsername = response.name.substring(0, 16) userProfile.value.name = `${shortUsername}` userProfile.value.groups = response.groups userProfile.value.uuid = response.uuid userProfile.value.email_verified_at = response.email_verified_at userProfile.value.group_id = response.group_mode_id if (userProfile.value.groups.length === 1) { userProfile.value.group_id = userProfile.value.groups[0]['id'] userProfile.value.group = userProfile.value.groups[0]['name'] } getAbilities() // <--- call getAbilities() here isLoadingUser.value = false; }) .catch(error => { localStorage.setItem('token', '') navigateTo('/login', { replace: true }) }) }
const loginUser = async (response) => { userUuid = response.data.uuid userUuid.replace(/['"]+/g, '') userProfile.value.name = response.data.name userProfile.value.uuid = response.data.uuid userProfile.value.email_verified_at = response.data.email_verified_at userProfile.value.groups = response.data.groups userProfile.value.token = response.token userProfile.value.group_id = response.data.group_mode_id if (userProfile.value.name === '') { localStorage.setItem('loggedIn', JSON.stringify(false)) } else { if (process.client) { localStorage.setItem('loggedIn', JSON.stringify(true)) localStorage.setItem('token', JSON.stringify(userProfile.value.token)) localStorage.setItem('userUuid', JSON.stringify(userUuid)) } await getAbilities() // <--- call getAbilities() here if (userProfile.value.groups.length === 1) { userProfile.value.group_id = userProfile.value.groups[0]['id'] userProfile.value.group = userProfile.value.groups[0]['name'] } if ( userProfile.value.group_id === groupMember || !userProfile.value.group_id ) { await navigateTo(`/master/users/${userUuid}`) } else if (userProfile.value.group_id && userProfile.value.group_id !== groupMember) { await navigateTo('/') } } } const getUser = async () => { assignToken() await $fetch(`${config.BASE_URL}/api/user`, { method: 'GET', credentials: 'omit', headers: { Authorization: `Bearer ${bearerToken.value}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }) .then(response => { if (userProfile.value.group_id === groupMember) { navigateTo(`/master/users/${userProfile.value.uuid}`) } let shortUsername = response.name.substring(0, 16) userProfile.value.name = `${shortUsername}` userProfile.value.groups = response.groups userProfile.value.uuid = response.uuid userProfile.value.email_verified_at = response.email_verified_at userProfile.value.group_id = response.group_mode_id if (userProfile.value.groups.length === 1) { userProfile.value.group_id = userProfile.value.groups[0]['id'] userProfile.value.group = userProfile.value.groups[0]['name'] } getAbilities() // <--- call getAbilities() here isLoadingUser.value = false; }) .catch(error => { localStorage.setItem('token', '') navigateTo('/login', { replace: true }) }) }
- Add condition to the component that will be displayed based on the user's permission.
master/configs/index.vue
vue<template> <LazyButtonModalCreateConfigComponent v-if="can('mst_configs.create')" /> </template> <script setup> import { useAbility } from "@casl/vue"; const { can } = useAbility(); </script>
<template> <LazyButtonModalCreateConfigComponent v-if="can('mst_configs.create')" /> </template> <script setup> import { useAbility } from "@casl/vue"; const { can } = useAbility(); </script>
- This can be applied to other components or pages that require authorization.