From 0b18886719d9d756d96044d079ddc95206ff6156 Mon Sep 17 00:00:00 2001 From: EL OUFIR Hatim Date: Tue, 24 Jan 2023 10:35:05 +0100 Subject: [PATCH] Add OIDC Connect --- .rnd | Bin 0 -> 1024 bytes app/Filament/Pages/ManageGeneralSettings.php | 7 ++ .../Controllers/Auth/OidcAuthController.php | 86 ++++++++++++++++++ app/Models/User.php | 15 ++- app/Settings/GeneralSettings.php | 1 + composer.json | 1 + composer.lock | 72 ++++++++++++++- config/services.php | 12 +++ ...023_01_24_084637_update_users_for_oidc.php | 38 ++++++++ database/seeders/DefaultUserSeeder.php | 4 +- database/seeders/PermissionsSeeder.php | 4 + ...2_add_default_role_to_general_settings.php | 12 +++ lang/fr.json | 7 +- .../vendor/filament-breezy/login.blade.php | 20 ++++ routes/web.php | 8 ++ 15 files changed, 280 insertions(+), 7 deletions(-) create mode 100644 .rnd create mode 100644 app/Http/Controllers/Auth/OidcAuthController.php create mode 100644 database/migrations/2023_01_24_084637_update_users_for_oidc.php create mode 100644 database/settings/2022_11_12_173852_add_default_role_to_general_settings.php diff --git a/.rnd b/.rnd new file mode 100644 index 0000000000000000000000000000000000000000..7a9afee0faf1163280d1cdde263241df74d829b7 GIT binary patch literal 1024 zcmV+b1poWEztHLiToQ%sX@+NcGszx;BTtkWhwVJd`J(7yJd)h2mzspe?FYfXlL~oW zWVUnD$}`%l1_CHz1rjkHi-3D2cZt1hfUY68u>%-~KX#y*pzF5Wiw!i9`KT2ut^p&q zSj0(IDo-n3x4vZgi_qjE zua}3O;;}d&chK^x1ZFH5S_a$GrYg8mC_v>j7_pyvn69yWOtG-FvjLx$MAzm*S{rk$ zqGvj-MiEtmwnGax@uftYQENJI6g2-_PRYH{(JyBfJ&BGIqv(YmSSj9uaz`Bo7802L z;3L$M@LD@3o3*}KBwr}O(Y6A2LV-BmO{z2Wi){WKO>vpR?6jaOl`=1hZ82~^V7ssx z5{;kF2>PTz@G@K9j#i}+mnIi^2`oyd9<3IJnKtQO!&=;4 z)p85G;MFE4QAy+AbL8VCueVO9u4vp!7t}Z~LBttO$Cehps?tR?QDdHisIuZV_=H5j zw0m?n6XW!@QgYgodC0BUp&B1MU2tkBKO$qvMft#jqLAYnj|4~oE`-`Qxm;VY6sm+~ z>5So8?Mw+HTHT;)X0WHLN(UZSCH_w@m#;GErpC50DG=kIBQS4}2$&m( zO51h}X0>BummAG?%+2k-3x9A!%!nFpT_Hzk)`*Z=0|-HorosH^0SN};J9+ehj4W3n zqf94rr@nzKS7ORm=6Wvv%QD75(}wH#wQzkfcaXRuOs$mVkgI{qm25mpWEIZkt7{h^ u7oU^-dM(C>+y-ThelperText(__('The language used by the platform.')) ->searchable() ->options($this->getLanguages()), + + Select::make('default_role') + ->label(__('Default role')) + ->helperText(__('The platform default role (used mainly in OIDC Connect).')) + ->searchable() + ->options(Role::all()->pluck('name', 'id')->toArray()), ]), ]), ]), diff --git a/app/Http/Controllers/Auth/OidcAuthController.php b/app/Http/Controllers/Auth/OidcAuthController.php new file mode 100644 index 00000000..0687ca91 --- /dev/null +++ b/app/Http/Controllers/Auth/OidcAuthController.php @@ -0,0 +1,86 @@ +client = new GenericProvider([ + 'clientId' => config('services.oidc.client_id'), + 'clientSecret' => config('services.oidc.client_secret'), + 'redirectUri' => config('services.oidc.redirect_uri'), + 'urlAuthorize' => config('services.oidc.url_authorize'), + 'urlAccessToken' => config('services.oidc.url_access_token'), + 'urlResourceOwnerDetails' => config('services.oidc.url_resource_owner_details'), + 'scopes' => config('services.oidc.scope') + ]); + } + + public function redirect() + { + $authUrl = $this->client->getAuthorizationUrl(); + return redirect($authUrl); + } + + public function callback(Request $request) + { + try { + $accessToken = $this->client->getAccessToken('authorization_code', [ + 'code' => $request->input('code') + ]); + $user = $this->client->getResourceOwner($accessToken); + + // Perform any additional validation or user creation here + if ($user) { + $data = $user->toArray(); + $user = User::where('email', $data['email'])->first(); + if (!$user) { + $user = User::create([ + 'name' => $data['given_name'] . ' ' . $data['family_name'], + 'email' => $data['email'], + 'oidc_username' => $data['preferred_username'], + 'email_verified_at' => $data['email_verified'] ? now() : null, + 'type' => 'oidc', + 'oidc_sub' => $data['sub'], + 'password' => null + ]); + $defaultRoleSettings = app(GeneralSettings::class)->default_role; + if ($defaultRoleSettings && $defaultRole = Role::where('id', $defaultRoleSettings)->first()) { + $user->syncRoles([$defaultRole]); + } + } else { + $user->update([ + 'name' => $data['given_name'] . ' ' . $data['family_name'], + 'email' => $data['email'], + 'oidc_username' => $data['preferred_username'], + 'type' => 'oidc', + 'oidc_sub' => $data['sub'], + 'password' => null + ]); + $user->refresh(); + } + + // Log the user in + auth()->login($user); + + return redirect()->intended(); + } + session()->flash('oidc_error'); + return redirect()->route('login'); + } catch (IdentityProviderException $e) { + session()->flash('oidc_error'); + return redirect()->route('login'); + } + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 91f53628..33fba62b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -34,7 +34,10 @@ class User extends Authenticatable implements MustVerifyEmail, FilamentUser 'name', 'email', 'password', - 'creation_token' + 'creation_token', + 'type', + 'oidc_username', + 'email_verified_at', ]; /** @@ -61,12 +64,16 @@ public static function boot() parent::boot(); static::creating(function (User $item) { - $item->password = bcrypt(uniqid()); - $item->creation_token = Uuid::uuid4()->toString(); + if ($item->type == 'db') { + $item->password = bcrypt(uniqid()); + $item->creation_token = Uuid::uuid4()->toString(); + } }); static::created(function (User $item) { - $item->notify(new UserCreatedNotification($item)); + if ($item->type == 'db') { + $item->notify(new UserCreatedNotification($item)); + } }); } diff --git a/app/Settings/GeneralSettings.php b/app/Settings/GeneralSettings.php index d0a97feb..b822352a 100644 --- a/app/Settings/GeneralSettings.php +++ b/app/Settings/GeneralSettings.php @@ -12,6 +12,7 @@ class GeneralSettings extends Settings public string|null $site_logo; public string|null $enable_social_login; public string|null $site_language; + public string|null $default_role; public static function group(): string { diff --git a/composer.json b/composer.json index dee6162b..ece2e4df 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "laravel/framework": "^9.19", "laravel/sanctum": "^3.0", "laravel/tinker": "^2.7", + "league/oauth2-client": "^2.6", "maatwebsite/excel": "^3.1", "owenvoke/blade-fontawesome": "^2.1", "protonemedia/laravel-verify-new-email": "^1.6", diff --git a/composer.lock b/composer.lock index 4c170dd9..84b7064f 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": "d3e992c0a1606486ed8c014aa7321666", + "content-hash": "20230161ca53de174c58f91a9fecc906", "packages": [ { "name": "akaunting/laravel-money", @@ -3366,6 +3366,76 @@ }, "time": "2022-04-15T14:02:14+00:00" }, + { + "name": "league/oauth2-client", + "version": "2.6.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "2334c249907190c132364f5dae0287ab8666aa19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/2334c249907190c132364f5dae0287ab8666aa19", + "reference": "2334c249907190c132364f5dae0287ab8666aa19", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "paragonie/random_compat": "^1 || ^2 || ^9.99", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", + "squizlabs/php_codesniffer": "^2.3 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.6.1" + }, + "time": "2021-12-22T16:42:49+00:00" + }, { "name": "league/uri-parser", "version": "1.4.1", diff --git a/config/services.php b/config/services.php index 41b08b92..d5a89ad1 100644 --- a/config/services.php +++ b/config/services.php @@ -55,4 +55,16 @@ 'redirect' => env('TWITTER_CLIENT_CALLBACK') ], + 'oidc' => [ + 'is_enabled' => env('OIDC_IS_ENABLED'), + 'client_id' => env('OIDC_CLIENT_ID'), + 'client_secret' => env('OIDC_CLIENT_SECRET'), + 'discovery_endpoint' => env('OIDC_DISCOVERY_ENDPOINT'), + 'redirect_uri' => env('OIDC_REDIRECT_URI'), + 'url_authorize' => env('OIDC_URL_AUTHORIZE'), + 'url_access_token' => env('OIDC_URL_ACCESS_TOKEN'), + 'url_resource_owner_details' => env('OIDC_URL_RESOURCE_OWNER_DETAILS'), + 'scope' => explode(",", env('OIDC_SCOPE')), + ], + ]; diff --git a/database/migrations/2023_01_24_084637_update_users_for_oidc.php b/database/migrations/2023_01_24_084637_update_users_for_oidc.php new file mode 100644 index 00000000..54a60b52 --- /dev/null +++ b/database/migrations/2023_01_24_084637_update_users_for_oidc.php @@ -0,0 +1,38 @@ +string('password')->nullable(true)->change(); + $table->string('type')->default('db'); + $table->string('oidc_username')->nullable(); + $table->string('oidc_sub')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->string('password')->nullable(false)->change(); + $table->dropColumn('type'); + $table->dropColumn('oidc_username'); + $table->dropColumn('oidc_sub'); + }); + } +}; diff --git a/database/seeders/DefaultUserSeeder.php b/database/seeders/DefaultUserSeeder.php index 1235faf1..f0e52ee6 100644 --- a/database/seeders/DefaultUserSeeder.php +++ b/database/seeders/DefaultUserSeeder.php @@ -16,12 +16,14 @@ class DefaultUserSeeder extends Seeder public function run() { if (User::where('email', 'john.doe@helper.app')->count() == 0) { - User::create([ + $user = User::create([ 'name' => 'John DOE', 'email' => 'john.doe@helper.app', 'password' => bcrypt('Passw@rd'), 'email_verified_at' => now() ]); + $user->creation_token = null; + $user->save(); } } } diff --git a/database/seeders/PermissionsSeeder.php b/database/seeders/PermissionsSeeder.php index 3c0175ec..941b9d4a 100644 --- a/database/seeders/PermissionsSeeder.php +++ b/database/seeders/PermissionsSeeder.php @@ -5,6 +5,7 @@ use App\Models\Permission; use App\Models\Role; use App\Models\User; +use App\Settings\GeneralSettings; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; use Illuminate\Support\Str; @@ -64,6 +65,9 @@ public function run() $role = Role::firstOrCreate([ 'name' => $this->defaultRole ]); + $settings = app(GeneralSettings::class); + $settings->default_role = $role->id; + $settings->save(); // Add all permissions to default role $role->syncPermissions(Permission::all()->pluck('name')->toArray()); diff --git a/database/settings/2022_11_12_173852_add_default_role_to_general_settings.php b/database/settings/2022_11_12_173852_add_default_role_to_general_settings.php new file mode 100644 index 00000000..441745cd --- /dev/null +++ b/database/settings/2022_11_12_173852_add_default_role_to_general_settings.php @@ -0,0 +1,12 @@ +migrator->add('general.default_role'); + } +} diff --git a/lang/fr.json b/lang/fr.json index f80e6d20..0c366166 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -256,5 +256,10 @@ "Jira importation": "Importation jira", "Jira tickets successfully imported": "Tickets jira importés avec succès", "Before you can import jira tickets you need to have all the referentials configured": "Avant de pouvoir importer des tickets jira, vous devez avoir configuré tous les référentiels", - "No content found in jira ticket": "Aucun contenu trouvé sur le ticket jira" + "No content found in jira ticket": "Aucun contenu trouvé sur le ticket jira", + "Default role": "Rôle par défaut", + "The platform default role (used mainly in OIDC Connect).": "Le rôle par défaut de la plateforme (utilisé principal par la connexion OIDC).", + "OIDC Connect error": "Erreur de connection OIDC", + "Invalid account!": "Compte incorrect !", + "OIDC Connect": "Connexion OIDC" } diff --git a/resources/views/vendor/filament-breezy/login.blade.php b/resources/views/vendor/filament-breezy/login.blade.php index 06c68d69..7c272531 100644 --- a/resources/views/vendor/filament-breezy/login.blade.php +++ b/resources/views/vendor/filament-breezy/login.blade.php @@ -4,6 +4,12 @@ + @if(session()->has('oidc_error')) + + @endif +

{{ __('filament::login.heading') }} @@ -31,4 +37,18 @@ @if(config('filament-socialite.enabled')) @endif + + @if(config('services.oidc.is_enabled')) + +
+ + {{ __('OIDC Connect') }} +
+
+ @endif diff --git a/routes/web.php b/routes/web.php index 4b110496..2ff2a050 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Route; use Filament\Http\Middleware\DispatchServingFilamentEvent; use App\Http\Controllers\RoadMap\DataController; +use App\Http\Controllers\Auth\OidcAuthController; // Share ticket Route::get('/tickets/share/{ticket:code}', function (Ticket $ticket) { @@ -28,3 +29,10 @@ Route::get('road-map/data/{project}', [DataController::class, 'data']) ->middleware(['verified', 'auth']) ->name('road-map.data'); + +Route::name('oidc.') + ->prefix('oidc') + ->group(function () { + Route::get('redirect', [OidcAuthController::class, 'redirect'])->name('redirect'); + Route::get('callback', [OidcAuthController::class, 'callback'])->name('callback'); + });