Skip to content

Commit

Permalink
feat: Keycloak role permission system
Browse files Browse the repository at this point in the history
  • Loading branch information
dogukanoksuz committed Sep 10, 2024
1 parent 4f7f853 commit 51d1d01
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 40 deletions.
97 changes: 58 additions & 39 deletions app/Classes/Authentication/KeycloakAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,76 +4,95 @@

use App\Models\Oauth2Token;
use App\Models\User;
use GuzzleHttp\Client;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
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
{
public function authenticate($credentials, $request): JsonResponse
private $kcClient;

private $oauthProvider;

private $kcUserApi;

public function __construct()
{
$client = new Client([
'verify' => false,
$this->kcClient = new KeycloakClient(
env('KEYCLOAK_CLIENT_ID'),
env('KEYCLOAK_CLIENT_SECRET'),
env('KEYCLOAK_REALM'),
env('KEYCLOAK_BASE_URL'),
null,
''
);

$this->kcUserApi = new UserApi($this->kcClient);

$this->oauthProvider = new KeycloakProvider([
'authServerUrl' => env('KEYCLOAK_BASE_URL'),
'realm' => env('KEYCLOAK_REALM'),
'clientId' => env('KEYCLOAK_CLIENT_ID'),
'clientSecret' => env('KEYCLOAK_CLIENT_SECRET'),
'redirectUri' => env('KEYCLOAK_REDIRECT_URI'),
'version' => '24.0.0',
]);
}

public function authenticate($credentials, $request): JsonResponse
{
try {
$r = $client->post(
env('KEYCLOAK_BASE_URL').'/realms/'.env('KEYCLOAK_REALM').'/protocol/openid-connect/token',
[
'form_params' => [
'client_id' => env('KEYCLOAK_CLIENT_ID'),
'client_secret' => env('KEYCLOAK_CLIENT_SECRET'),
'username' => $request->email,
'password' => $request->password,
'grant_type' => 'password',
'scope' => 'openid',
],
]
);
} catch (\Exception $e) {
Log::error('Keycloak authentication failed. '.$e->getMessage());
$accessTokenObject = $this->oauthProvider->getAccessToken('password', [
'username' => $request->email,
'password' => $request->password,
'scope' => 'openid',
]);

return Authenticator::returnLoginError($request->email);
}
$resourceOwner = $this->oauthProvider->getResourceOwner($accessTokenObject);

$response = json_decode($r->getBody()->getContents(), true);
if (! isset($response['access_token'])) {
Log::error('Keycloak authentication failed. Access token is missing.');
$roles = collect($this->kcUserApi->getRoles($resourceOwner->getId()))
->map(function ($role) {
return $role->name;
})->toArray();
} catch (\Exception $e) {
Log::error('Keycloak authentication failed. '.$e->getMessage());

return Authenticator::returnLoginError($request->email);
}
$details = json_decode(base64_decode(str_replace('_', '/', str_replace('-', '+', explode('.', $response['access_token'])[1]))));

$create = User::where('email', strtolower($request->email))
->orWhere('username', strtolower($request->email))
->first();

if (! $create) {
$user = User::create([
'id' => $details->sub,
'name' => $details->name,
'email' => $details->email,
'username' => $details->preferred_username,
'id' => $resourceOwner->getId(),
'name' => $resourceOwner->getName(),
'email' => $resourceOwner->getEmail(),
'username' => $resourceOwner->getUsername(),
'auth_type' => 'keycloak',
'password' => Hash::make(Str::random(16)),
'password' => Hash::make(Str::uuid()),
'forceChange' => false,
]);
} else {
$user = User::where('id', $details->sub)->first();
$user = User::where('id', $resourceOwner->getId())->first();
}

Oauth2Token::updateOrCreate([
'user_id' => $details->sub,
'token_type' => $response['token_type'],
'user_id' => $resourceOwner->getId(),
'token_type' => $accessTokenObject->getValues()['token_type'],
], [
'user_id' => $details->sub,
'token_type' => $response['token_type'],
'access_token' => $response['access_token'],
'refresh_token' => $response['refresh_token'],
'expires_in' => (int) $response['expires_in'],
'refresh_expires_in' => (int) $response['refresh_expires_in'],
'user_id' => $resourceOwner->getId(),
'token_type' => $accessTokenObject->getValues()['token_type'],
'access_token' => $accessTokenObject->getToken(),
'refresh_token' => $accessTokenObject->getRefreshToken(),
'expires_in' => $accessTokenObject->getExpires(),
'refresh_expires_in' => $accessTokenObject->getValues()['refresh_expires_in'],
'permissions' => $roles,
]);

return Authenticator::createNewToken(
Expand Down
5 changes: 5 additions & 0 deletions app/Models/Oauth2Token.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ class Oauth2Token extends Model
'token_type',
'expires_in',
'refresh_expires_in',
'permissions',
];

protected $casts = [
'permissions' => 'array',
];

/**
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"ext-snmp": "*",
"ext-xml": "*",
"ext-zip": "*",
"acsystems/keycloak-php-sdk": "^4.4",
"ankitpokhrel/tus-php": "^2.3",
"bacon/bacon-qr-code": "^2.0",
"beebmx/blade": "^1.5",
Expand All @@ -31,6 +32,7 @@
"phpseclib/phpseclib": "~3.0",
"pragmarx/google2fa-laravel": "^2.0",
"pusher/pusher-php-server": "^7.0",
"stevenmaguire/oauth2-keycloak": "^5.1",
"tymon/jwt-auth": "^2.0"
},
"require-dev": {
Expand Down
Loading

0 comments on commit 51d1d01

Please sign in to comment.