From 7a6f775f9ebc68315c7a9571ba5a813cae69ed93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fukan=20=C3=96ks=C3=BCz?= Date: Thu, 16 Jan 2025 15:08:38 +0300 Subject: [PATCH] feat: Keycloak authentication role mapping and add composite role support --- .../Authentication/KeycloakAuthenticator.php | 39 ++++++++++++++----- app/Models/User.php | 22 +++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/app/Classes/Authentication/KeycloakAuthenticator.php b/app/Classes/Authentication/KeycloakAuthenticator.php index 02fdb135..581407da 100644 --- a/app/Classes/Authentication/KeycloakAuthenticator.php +++ b/app/Classes/Authentication/KeycloakAuthenticator.php @@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Keycloak\KeycloakClient; -use Keycloak\User\UserApi; use Stevenmaguire\OAuth2\Client\Provider\Keycloak as KeycloakProvider; class KeycloakAuthenticator implements AuthenticatorInterface @@ -18,8 +17,6 @@ class KeycloakAuthenticator implements AuthenticatorInterface private $oauthProvider; - private $kcUserApi; - public function __construct() { $this->kcClient = new KeycloakClient( @@ -31,8 +28,6 @@ public function __construct() '' ); - $this->kcUserApi = new UserApi($this->kcClient); - $this->oauthProvider = new KeycloakProvider([ 'authServerUrl' => env('KEYCLOAK_BASE_URL'), 'realm' => env('KEYCLOAK_REALM'), @@ -54,10 +49,7 @@ public function authenticate($credentials, $request): JsonResponse $resourceOwner = $this->oauthProvider->getResourceOwner($accessTokenObject); - $roles = collect($this->kcUserApi->getRoles($resourceOwner->getId())) - ->map(function ($role) { - return $role->name; - })->toArray(); + $roles = $resourceOwner->toArray()['realm_access']['roles']; } catch (\Exception $e) { Log::error('Keycloak authentication failed. '.$e->getMessage()); @@ -95,9 +87,38 @@ public function authenticate($credentials, $request): JsonResponse 'permissions' => $roles, ]); + try { + $allRealmRoles = $this->getAllRealmRoles(); + + foreach ($allRealmRoles as $role) { + if (in_array($role->name, $roles)) { + // If user role is matched with realm role, check attributes and assign user to liman role + if (isset($role->attributes->liman_role) && count($role->attributes->liman_role) > 0) { + // Assign all items in liman_role attribute to user + foreach ($role->attributes->liman_role as $limanRole) { + $user->assignRole($limanRole); + } + } + } + } + } catch (\Throwable $e) { + Log::warning('Failed to fetch realm roles from Keycloak. '.$e->getMessage()); + } + return Authenticator::createNewToken( auth('api')->login($user), $request ); } + + private function getAllRealmRoles() + { + $response = $this->kcClient->sendRequest('GET', "roles?briefRepresentation=false"); + + if ($response->getStatusCode() !== 200) { + throw new \Exception('Failed to fetch composite roles'); + } + + return json_decode($response->getBody()->getContents()); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 45cfbf82..d45dc902 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -16,6 +16,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Tymon\JWTAuth\Contracts\JWTSubject as JWTSubject; +use Illuminate\Support\Str; /** * App\Models\User @@ -190,6 +191,27 @@ public function authLogs() return $this->hasMany('\App\Models\AuthLog'); } + /** + * Assign role to user + * + * @param string $id + * @return void + */ + public function assignRole($id) + { + // Check if id is valid for a role + if (Role::find($id) == null) { + return; + } + + // Check if role is already assigned + if ($this->roles()->where('role_id', $id)->exists()) { + return; + } + + $this->roles()->attach($id, ['id' => Str::uuid()]); + } + /** * Interact with the user's OTP secret. *