From 9c6a758723d561eeed4a930f22113d80ecbc6cdf Mon Sep 17 00:00:00 2001 From: c-jar Date: Sat, 2 Nov 2024 15:19:37 +0100 Subject: [PATCH 1/6] Add socialite et keycloak provider in requierement --- composer.json | 4 +- composer.lock | 280 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 279 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index e28c9ad..86d11d7 100644 --- a/composer.json +++ b/composer.json @@ -10,11 +10,13 @@ "laravel/framework": "^11.9", "laravel/passport": "^12.3", "laravel/sanctum": "^4.0", + "laravel/socialite": "^5.16", "laravel/tinker": "^2.9", "laravel/ui": "^4.5", "maatwebsite/excel": "^3.1", + "phpmailer/phpmailer": "^6.9.2", "phpoffice/phpword": "^1.3", - "phpmailer/phpmailer": "^6.9.2" + "socialiteproviders/keycloak": "^5.3" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 7371d09..d39badf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "24e1dc9b787f5455925005c70616e2bc", + "content-hash": "24e0bcdfbf7496d757375087555668a7", "packages": [ { "name": "brick/math", @@ -1845,6 +1845,78 @@ }, "time": "2024-09-23T13:33:08+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.16.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "league/oauth1-client": "^1.10.1", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.0|^9.3|^10.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ], + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + } + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2024-09-03T09:46:57+00:00" + }, { "name": "laravel/tinker", "version": "v2.10.0", @@ -2541,6 +2613,82 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "league/oauth1-client", + "version": "v1.10.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1" + }, + "time": "2022-04-15T14:02:14+00:00" + }, { "name": "league/oauth2-server", "version": "8.5.4", @@ -5011,6 +5159,130 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "socialiteproviders/keycloak", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Keycloak.git", + "reference": "87d13f8a411a6f8f5010ecbaff9aedd4494863e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Keycloak/zipball/87d13f8a411a6f8f5010ecbaff9aedd4494863e4", + "reference": "87d13f8a411a6f8f5010ecbaff9aedd4494863e4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4 || ^8.0", + "socialiteproviders/manager": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Keycloak\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oleg Kuchumov", + "email": "voenniy@gmail.com" + } + ], + "description": "Keycloak OAuth2 Provider for Laravel Socialite", + "keywords": [ + "keycloak", + "laravel", + "oauth", + "provider", + "socialite" + ], + "support": { + "docs": "https://socialiteproviders.com/keycloak", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2023-04-10T05:50:49+00:00" + }, + { + "name": "socialiteproviders/manager", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Manager.git", + "reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/dea5190981c31b89e52259da9ab1ca4e2b258b21", + "reference": "dea5190981c31b89e52259da9ab1ca4e2b258b21", + "shasum": "" + }, + "require": { + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "laravel/socialite": "^5.5", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "SocialiteProviders\\Manager\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "SocialiteProviders\\Manager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andy Wendt", + "email": "andy@awendt.com" + }, + { + "name": "Anton Komarev", + "email": "a.komarev@cybercog.su" + }, + { + "name": "Miguel Piedrafita", + "email": "soy@miguelpiedrafita.com" + }, + { + "name": "atymic", + "email": "atymicq@gmail.com", + "homepage": "https://atymic.dev" + } + ], + "description": "Easily add new or override built-in providers in Laravel Socialite.", + "homepage": "https://socialiteproviders.com", + "keywords": [ + "laravel", + "manager", + "oauth", + "providers", + "socialite" + ], + "support": { + "issues": "https://github.com/socialiteproviders/manager/issues", + "source": "https://github.com/socialiteproviders/manager" + }, + "time": "2024-05-04T07:57:39+00:00" + }, { "name": "symfony/clock", "version": "v7.1.6", @@ -9733,12 +10005,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": [], - "plugin-api-version": "2.3.0" + "platform-dev": {}, + "plugin-api-version": "2.6.0" } From 4f3f7cb0ca0a73e4a8da782d157f5967046f1924 Mon Sep 17 00:00:00 2001 From: c-jar Date: Sat, 2 Nov 2024 15:29:31 +0100 Subject: [PATCH 2/6] Add Socialite controller and routes --- app/Http/Controllers/SocialiteController.php | 251 +++++++++++++++++++ config/services.php | 6 + routes/web.php | 4 + 3 files changed, 261 insertions(+) create mode 100644 app/Http/Controllers/SocialiteController.php diff --git a/app/Http/Controllers/SocialiteController.php b/app/Http/Controllers/SocialiteController.php new file mode 100644 index 0000000..2efa5a9 --- /dev/null +++ b/app/Http/Controllers/SocialiteController.php @@ -0,0 +1,251 @@ + '1', + 'user' => '2', + 'auditee' => '5', + 'auditor' => '3', + //'api' => '4', + ]; + + public const LOCALES = ['en', 'fr']; + + public function __construct() + { + $this->middleware('auth:api', ['except' => ['redirect', 'callback']]); + } + + /** + * Redirect action use to redirect user to OIDC provider. + */ + public function redirect(string $provider) + { + $providers = config('services.socialite_controller.providers', []); + + if (in_array($provider, $providers)) { + Log::debug("Redirect with '$provider' provider"); + $config_name = 'services.socialite_controller.'.$provider; + $additional_scopes = config($config_name.'.additional_scopes'); + return Socialite::with($provider)->scopes($additional_scopes)->redirect(); + } + + Log::warning("Redirect: Provider '$provider' not found."); + abort(404); + } + + /** + * Callback action use when OIDC provider redirect user to app. + */ + public function callback(Request $request, string $provider) + { + $providers = config('services.socialite_controller.providers', []); + + if (! in_array($provider, $providers)) { + Log::warning("Callback: Provider '$provider' not found."); + abort(404); + } + + Log::debug("Callback provider : '$provider'"); + + // Get additionnal config for current provider + $config_name = 'services.socialite_controller.'.$provider; + $allow_create_user = false; + $allow_update_user = false; + if(config($config_name)){ + $allow_create_user = config($config_name.'.allow_create_user', $allow_create_user); + $allow_update_user = config($config_name.'.allow_update_user', $allow_update_user); + } + Log::debug('CONFIG: allow_create_user='.($allow_create_user ? 'true' : 'false')); + Log::debug('CONFIG: allow_update_user='.($allow_update_user ? 'true' : 'false')); + if($allow_create_user || $allow_update_user){ + $role_claim = config($config_name.'.role_claim', ''); + Log::debug('CONFIG: role_claim='.$role_claim); + $default_role = config($config_name.'.default_role', ''); + Log::debug('CONFIG: default_role='.$default_role); + } + + try { + $socialite_user = Socialite::with($provider)->user(); + $user = null; + + // Search user by email + if($socialite_user->email){ + $user = User::query()->whereEmail($socialite_user->email)->first(); + } else { + Log::warning("User has no attribute email"); + } + + // If not exist and allow to create user then create it + if (!$user && $allow_create_user) { + $user = $this->create_user($socialite_user, $provider, $role_claim, $default_role); + } + + // If no user redirect to login with error message + if (!$user) { + Log::warning("User [$socialite_user->id, $socialite_user->email] not found in deming database"); + return redirect('login')->withErrors(['socialite' => trans('cruds.login.error.user_not_exist') ]); + } + + if($allow_update_user){ + $this->update_user($user, $socialite_user, $provider, $role_claim, $default_role); + } + + Log::info("User '$user->login' login with $provider provider"); + + Auth::guard('web')->login($user); + + return redirect(route('home')); + } catch (Exception $exception) { + return redirect('login'); + } + } + + /** + * Create user with claims provided. + */ + protected function create_user(SocialiteUser $socialite_user, string $provider, string $role_claim, string $default_role) + { + $user = new User(); + + $user->login = $this->get_user_login($socialite_user); + $user->name = $socialite_user->name; + $user->email = $socialite_user->email; + $user->title = "User provide by $provider"; + $user->role = $this->get_user_role($socialite_user, $role_claim, $default_role); + $user->language = $this->get_user_langage($socialite_user); + + // TODO allow null password + $user->password = bin2hex(random_bytes(32)); + + Log::info("Create new user '$user->login' with role '$user->role' from $provider provider"); + try { + $user->save(); + } catch(QueryException $exception){ + Log::debug($exception->getMessage()); + Log::error("Unable to create user"); + return null; + } + + return $user; + } + + /** + * Update user with claims providid. + */ + protected function update_user(User $user, SocialiteUser $socialite_user, string $provider, string $role_claim, string $default_role) + { + $updated = false; + + $login = $this->get_user_login($socialite_user); + if ($login !== $user->login) { + Log::debug("Login changed $user->login => $login"); + $user->login = $login; + $updated = true; + } + + if ($socialite_user->name !== $user->name) { + Log::debug("Name changed $user->name => $socialite_user->name"); + $user->name = $socialite_user->name; + $updated = true; + } + + $role = $this->get_user_role($socialite_user, $role_claim, $default_role); + if($role != $user->role){ + Log::debug("Role changed $user->role => $role"); + $user->role = $role; + $updated = true; + } + + $language = $this->get_user_langage($socialite_user); + if ($language !== $user->language) { + Log::debug("Lauguage change $user->language => $language"); + $user->language = $language; + $updated = true; + } + + if ($updated) { + Log::info("Update user '$user->login' with role '$user->role' from $provider provider"); + $user->save(); + } + return $user; + } + + /** + * Return user's login. + */ + private function get_user_login(SocialiteUser $socialite_user) + { + // set login with preferred_username, otherwise use id + if($socialite_user->offsetExists('preferred_username')){ + return $socialite_user->offsetGet("preferred_username"); + } + return $socialite_user->id; + } + + /** + * Return user's role. + * If no role provided, use $default_role value. + * If $default_role is null and no role provided, null return. + */ + private function get_user_role(SocialiteUser $socialite_user, string $role_claim, string $default_role) + { + $role_name = ""; + if(!empty($role_claim)){ + $role_name = $this->get_claim_value($socialite_user, $role_claim); + Log::debug("Provided claim '$role_claim'='$role_name'"); + } + if(!array_key_exists($role_name, self::ROLES_MAP)){ + if(!empty($default_role)){ + $role_name = $default_role; + } else { + Log::error("No default role set! A valid role must be provided. role='$role_name'"); + return null; + } + } + return self::ROLES_MAP[$role_name]; + } + + /** + * Return user's language. + * Use locale claim to dertermine user's language. + */ + private function get_user_langage(SocialiteUser $socialite_user) + { + if ($socialite_user->offsetExists('locale')){ + $locale = explode('-', $socialite_user->offsetGet('locale'))[0]; + if (in_array($locale, self::LOCALES)) return $locale; + } + return self::LOCALES[0]; + } + + private function get_claim_value(SocialiteUser $user, string $claim){ + $value = null; + foreach(explode('.', $claim) as $offset) { + if(! $value){ + if (! $user->offsetExists($offset)) return null; + $value = $user->offsetGet($offset); + continue; + } + if (! array_key_exists($offset, $value)) return null; + $value = $value[$offset]; + } + return $value; + } +} \ No newline at end of file diff --git a/config/services.php b/config/services.php index 2a1d616..af83b6f 100644 --- a/config/services.php +++ b/config/services.php @@ -30,4 +30,10 @@ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], + 'socialite_controller' => [ + 'providers' => ! empty(env('SOCIALITE_PROVIDERS', "")) + ? explode(' ', env('SOCIALITE_PROVIDERS', "")) + : [], + ] + ]; diff --git a/routes/web.php b/routes/web.php index 5f43699..84b82a5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,6 +19,10 @@ Route::get('/home', 'HomeController@index'); Route::get('/index', 'HomeController@index'); + /* Socialite (auth) */ + Route::get('auth/redirect/{driver}', 'SocialiteController@redirect')->name("socialite.redirect"); + Route::get('auth/callback/{driver}', 'SocialiteController@callback')->name("socialite.callback"); + /* Testing */ Route::get('/test', 'HomeController@test'); From 94143a49a39871880c91324f102c593f56cce5bc Mon Sep 17 00:00:00 2001 From: c-jar Date: Sat, 2 Nov 2024 15:45:49 +0100 Subject: [PATCH 3/6] Integrate Keyloak provider --- app/Http/Controllers/SocialiteController.php | 2 +- app/Providers/AppServiceProvider.php | 7 +++++++ config/services.php | 11 +++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/Http/Controllers/SocialiteController.php b/app/Http/Controllers/SocialiteController.php index 2efa5a9..e9fad28 100644 --- a/app/Http/Controllers/SocialiteController.php +++ b/app/Http/Controllers/SocialiteController.php @@ -111,7 +111,7 @@ public function callback(Request $request, string $provider) Auth::guard('web')->login($user); - return redirect(route('home')); + return redirect('/'); } catch (Exception $exception) { return redirect('login'); } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0c84d57..f51c606 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ use DB; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; @@ -40,5 +41,11 @@ public function boot() ); }); } + + if (in_array('keycloak', Config::get('services.socialite_controller.providers'))){ + Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) { + $event->extendSocialite('keycloak', \SocialiteProviders\Keycloak\Provider::class); + }); + } } } diff --git a/config/services.php b/config/services.php index af83b6f..4fdd1d6 100644 --- a/config/services.php +++ b/config/services.php @@ -34,6 +34,13 @@ 'providers' => ! empty(env('SOCIALITE_PROVIDERS', "")) ? explode(' ', env('SOCIALITE_PROVIDERS', "")) : [], - ] - + ], + + 'keycloak' => [ + 'client_id' => env('KEYCLOAK_CLIENT_ID'), + 'client_secret' => env('KEYCLOAK_CLIENT_SECRET'), + 'redirect' => env('KEYCLOAK_REDIRECT_URI'), + 'base_url' => env('KEYCLOAK_BASE_URL'), // Specify your keycloak server URL here + 'realms' => env('KEYCLOAK_REALM'), // Specify your keycloak realm + ], ]; From 133f15375cdfdcdc302267b9a6e7880a7d9ce4d4 Mon Sep 17 00:00:00 2001 From: c-jar Date: Sat, 2 Nov 2024 15:54:21 +0100 Subject: [PATCH 4/6] login.blade.php: add conection with btn --- config/services.php | 3 + resources/lang/en/cruds.php | 4 ++ resources/lang/fr/cruds.php | 4 ++ resources/views/auth/login.blade.php | 89 +++++++++++++++++----------- 4 files changed, 65 insertions(+), 35 deletions(-) diff --git a/config/services.php b/config/services.php index 4fdd1d6..564aa6f 100644 --- a/config/services.php +++ b/config/services.php @@ -34,6 +34,9 @@ 'providers' => ! empty(env('SOCIALITE_PROVIDERS', "")) ? explode(' ', env('SOCIALITE_PROVIDERS', "")) : [], + 'keycloak' => [ + 'display_name' => env('KEYCLAOK_DISPLAY_NAME', 'Keycloak'), + ], ], 'keycloak' => [ diff --git a/resources/lang/en/cruds.php b/resources/lang/en/cruds.php index b8be02d..6ac9ee4 100644 --- a/resources/lang/en/cruds.php +++ b/resources/lang/en/cruds.php @@ -169,6 +169,10 @@ 'title' => 'Enter a password', 'identification' => 'Login', 'connection' => 'Connection', + 'connection_with' => 'Connection with', + 'error' => [ + 'user_not_exist' => 'User not exist', + ], ], 'report' => [ 'action_plan' => [ diff --git a/resources/lang/fr/cruds.php b/resources/lang/fr/cruds.php index 3135cb3..90f8242 100644 --- a/resources/lang/fr/cruds.php +++ b/resources/lang/fr/cruds.php @@ -168,6 +168,10 @@ 'title' => 'Entrez un mot de passe', 'identification' => 'Identification', 'connection' => 'Connexion', + 'connection_with' => 'Connexion avec', + 'error' => [ + 'user_not_exist' => 'L\'utilisateur n\'existe pas', + ], ], 'report' => [ 'action_plan' => [ diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index a705189..0ca9ff0 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -39,43 +39,62 @@ -