From 1f37318f793e1da3bbb35f6bc181c97d6fca9c61 Mon Sep 17 00:00:00 2001 From: Pat Rocchio Date: Wed, 6 Mar 2024 11:30:19 -0500 Subject: [PATCH] Add oauth support - Support azure, bitbucket, github, gitlab, google providers - Add authentication page to settings Co-authored-by: Suraj Kumar Co-authored-by: Michael Castanieto Co-authored-by: Mike Kim --- app/Http/Controllers/OauthController.php | 35 +++ app/Livewire/Settings/Auth.php | 43 +++ app/Models/OauthSetting.php | 21 ++ app/Providers/EventServiceProvider.php | 3 + app/Providers/FortifyServiceProvider.php | 5 +- bootstrap/helpers/socialite.php | 26 ++ composer.json | 2 + composer.lock | 273 +++++++++++++++++- config/app.php | 1 + config/services.php | 1 - .../2024_03_08_180457_nullable_password.php | 28 ++ ...024_03_11_150013_create_oauth_settings.php | 33 +++ database/seeders/DatabaseSeeder.php | 1 + database/seeders/OauthSettingSeeder.php | 37 +++ database/seeders/ProductionSeeder.php | 3 + lang/en.json | 6 + resources/css/app.css | 4 + resources/views/auth/login.blade.php | 5 + .../views/livewire/settings/auth.blade.php | 30 ++ .../views/livewire/settings/index.blade.php | 5 + routes/web.php | 5 +- 21 files changed, 563 insertions(+), 4 deletions(-) create mode 100644 app/Http/Controllers/OauthController.php create mode 100644 app/Livewire/Settings/Auth.php create mode 100644 app/Models/OauthSetting.php create mode 100644 bootstrap/helpers/socialite.php create mode 100644 database/migrations/2024_03_08_180457_nullable_password.php create mode 100644 database/migrations/2024_03_11_150013_create_oauth_settings.php create mode 100644 database/seeders/OauthSettingSeeder.php create mode 100644 resources/views/livewire/settings/auth.blade.php diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php new file mode 100644 index 000000000..7d917e5a6 --- /dev/null +++ b/app/Http/Controllers/OauthController.php @@ -0,0 +1,35 @@ +redirect(); + } + + public function callback(string $provider) + { + try { + $oauthUser = get_socialite_provider($provider)->user(); + $user = User::whereEmail($oauthUser->email)->first(); + if (!$user) { + $user = User::create([ + 'name' => $oauthUser->name, + 'email' => $oauthUser->email, + ]); + } + Auth::login($user); + return redirect('/'); + } catch (\Exception $e) { + ray($e->getMessage()); + return redirect()->route('login')->withErrors([__('auth.failed.callback')]); + } + } +} diff --git a/app/Livewire/Settings/Auth.php b/app/Livewire/Settings/Auth.php new file mode 100644 index 000000000..d8bda0569 --- /dev/null +++ b/app/Livewire/Settings/Auth.php @@ -0,0 +1,43 @@ +reduce(function($carry, $setting) { + $carry["oauth_settings_map.$setting->provider.enabled"] = 'required'; + $carry["oauth_settings_map.$setting->provider.client_id"] = 'nullable'; + $carry["oauth_settings_map.$setting->provider.client_secret"] = 'nullable'; + $carry["oauth_settings_map.$setting->provider.redirect_uri"] = 'nullable'; + $carry["oauth_settings_map.$setting->provider.tenant"] = 'nullable'; + return $carry; + }, []); + } + + public function mount() { + $this->oauth_settings_map = OauthSetting::all()->reduce(function($carry, $setting) { + $carry[$setting->provider] = $setting; + return $carry; + }, []); + } + + private function updateOauthSettings() { + foreach (array_values($this->oauth_settings_map) as &$setting) { + $setting->save(); + } + } + + public function instantSave() { + $this->updateOauthSettings(); + } + + public function submit() { + $this->updateOauthSettings(); + $this->dispatch('success', 'Instance settings updated successfully!'); + } +} diff --git a/app/Models/OauthSetting.php b/app/Models/OauthSetting.php new file mode 100644 index 000000000..4ab21aeec --- /dev/null +++ b/app/Models/OauthSetting.php @@ -0,0 +1,21 @@ + empty($value) ? null : Crypt::decryptString($value), + set: fn (string | null $value) => empty($value) ? null : Crypt::encryptString($value), + ); + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index a9b4496b4..5f95ed37c 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -20,6 +20,9 @@ class EventServiceProvider extends ServiceProvider // Registered::class => [ // SendEmailVerificationNotification::class, // ], + \SocialiteProviders\Manager\SocialiteWasCalled::class => [ + \SocialiteProviders\Azure\AzureExtendSocialite::class.'@handle', + ], ]; public function boot(): void { diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 77de80885..6bb284eef 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -7,6 +7,7 @@ use App\Actions\Fortify\UpdateUserPassword; use App\Actions\Fortify\UpdateUserProfileInformation; use App\Models\InstanceSettings; +use App\Models\OauthSetting; use App\Models\User; use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; @@ -56,13 +57,15 @@ public function boot(): void Fortify::loginView(function () { $settings = InstanceSettings::get(); + $enabled_oauth_providers = OauthSetting::where('enabled', true)->get(); $users = User::count(); if ($users == 0) { // If there are no users, redirect to registration return redirect()->route('register'); } return view('auth.login', [ - 'is_registration_enabled' => $settings->is_registration_enabled + 'is_registration_enabled' => $settings->is_registration_enabled, + 'enabled_oauth_providers' => $enabled_oauth_providers, ]); }); diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php new file mode 100644 index 000000000..0773f6de6 --- /dev/null +++ b/bootstrap/helpers/socialite.php @@ -0,0 +1,26 @@ + $oauth_setting->client_id, + 'client_secret' => $oauth_setting->client_secret, + 'redirect' => $oauth_setting->redirect_uri, + 'tenant' => $oauth_setting->tenant, + ]; + $provider_class_map = [ + 'azure' => \SocialiteProviders\Azure\Provider::class, + 'bitbucket' => \Laravel\Socialite\Two\BitbucketProvider::class, + 'github' => \Laravel\Socialite\Two\GithubProvider::class, + 'gitlab' => \Laravel\Socialite\Two\GitlabProvider::class, + 'google' => \Laravel\Socialite\Two\GoogleProvider::class, + ]; + return Socialite::buildProvider( + $provider_class_map[$provider], + $config + ); +} diff --git a/composer.json b/composer.json index f762ed579..1624e1390 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "laravel/horizon": "^5.15", "laravel/prompts": "^0.1.6", "laravel/sanctum": "^v3.2.1", + "laravel/socialite": "^5.12", "laravel/tinker": "^v2.8.1", "laravel/ui": "^4.2", "lcobucci/jwt": "^5.0.0", @@ -31,6 +32,7 @@ "pusher/pusher-php-server": "^7.2", "resend/resend-laravel": "^0.5.0", "sentry/sentry-laravel": "^3.4", + "socialiteproviders/microsoft-azure": "^5.1", "spatie/laravel-activitylog": "^4.7.3", "spatie/laravel-data": "^3.4.3", "spatie/laravel-ray": "^1.32.4", diff --git a/composer.lock b/composer.lock index 74b27137f..347f9f619 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": "19b19082b605e09867e6ae65fb8135f6", + "content-hash": "9bdaf702cdd870434444f8937a816fdb", "packages": [ { "name": "amphp/amp", @@ -3370,6 +3370,76 @@ }, "time": "2023-11-08T14:08:06+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.12.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "7dae1b072573809f32ab6dcf4aebb57c8b3e8acf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/7dae1b072573809f32ab6dcf4aebb57c8b3e8acf", + "reference": "7dae1b072573809f32ab6dcf4aebb57c8b3e8acf", + "shasum": "" + }, + "require": { + "ext-json": "*", + "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" + }, + "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-02-16T08:58:20+00:00" + }, { "name": "laravel/tinker", "version": "v2.9.0", @@ -4090,6 +4160,82 @@ ], "time": "2024-01-28T23:22:08+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/uri", "version": "7.4.0", @@ -7696,6 +7842,131 @@ ], "time": "2023-10-12T14:38:46+00:00" }, + { + "name": "socialiteproviders/manager", + "version": "v4.5.1", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Manager.git", + "reference": "a67f194f0f4c4c7616c549afc697b78df9658d44" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/a67f194f0f4c4c7616c549afc697b78df9658d44", + "reference": "a67f194f0f4c4c7616c549afc697b78df9658d44", + "shasum": "" + }, + "require": { + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "laravel/socialite": "^5.2", + "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-02-17T08:58:03+00:00" + }, + { + "name": "socialiteproviders/microsoft-azure", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Microsoft-Azure.git", + "reference": "7522b27cd8518706b50e03b40a396fb0a6891feb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/7522b27cd8518706b50e03b40a396fb0a6891feb", + "reference": "7522b27cd8518706b50e03b40a396fb0a6891feb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2 || ^8.0", + "socialiteproviders/manager": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Azure\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Hemmings", + "email": "chris@hemmin.gs" + } + ], + "description": "Microsoft Azure OAuth2 Provider for Laravel Socialite", + "keywords": [ + "azure", + "laravel", + "microsoft", + "oauth", + "provider", + "socialite" + ], + "support": { + "docs": "https://socialiteproviders.com/microsoft-azure", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2022-03-15T21:17:43+00:00" + }, { "name": "spatie/backtrace", "version": "1.5.3", diff --git a/config/app.php b/config/app.php index 5851640be..b95224fdc 100644 --- a/config/app.php +++ b/config/app.php @@ -187,6 +187,7 @@ /* * Package Service Providers... */ + \SocialiteProviders\Manager\ServiceProvider::class, /* * Application Service Providers... diff --git a/config/services.php b/config/services.php index 0ace530e8..509e73756 100644 --- a/config/services.php +++ b/config/services.php @@ -30,5 +30,4 @@ 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], - ]; diff --git a/database/migrations/2024_03_08_180457_nullable_password.php b/database/migrations/2024_03_08_180457_nullable_password.php new file mode 100644 index 000000000..3bd91b0be --- /dev/null +++ b/database/migrations/2024_03_08_180457_nullable_password.php @@ -0,0 +1,28 @@ +string('password')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('password')->nullable(false)->change(); + }); + } +}; diff --git a/database/migrations/2024_03_11_150013_create_oauth_settings.php b/database/migrations/2024_03_11_150013_create_oauth_settings.php new file mode 100644 index 000000000..c2178426d --- /dev/null +++ b/database/migrations/2024_03_11_150013_create_oauth_settings.php @@ -0,0 +1,33 @@ +id(); + $table->string('provider')->unique(); + $table->boolean('enabled')->default(false); + $table->string('client_id')->nullable(); + $table->text('client_secret')->nullable(); + $table->string('redirect_uri')->nullable(); + $table->string('tenant')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('oauth_settings'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 2a2767d78..b3fac350f 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -32,6 +32,7 @@ public function run(): void StandalonePostgresqlSeeder::class, ScheduledDatabaseBackupSeeder::class, ScheduledDatabaseBackupExecutionSeeder::class, + OauthSettingSeeder::class, ]); } } diff --git a/database/seeders/OauthSettingSeeder.php b/database/seeders/OauthSettingSeeder.php new file mode 100644 index 000000000..4d33468c7 --- /dev/null +++ b/database/seeders/OauthSettingSeeder.php @@ -0,0 +1,37 @@ + 0, + 'provider' => 'azure', + ]); + OauthSetting::firstOrCreate([ + 'id' => 1, + 'provider' => 'bitbucket', + ]); + OauthSetting::firstOrCreate([ + 'id' => 2, + 'provider' => 'github', + ]); + OauthSetting::firstOrCreate([ + 'id' => 3, + 'provider' => 'gitlab', + ]); + OauthSetting::firstOrCreate([ + 'id' => 4, + 'provider' => 'google', + ]); + } +} diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index 4c66bdabf..206e92f76 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -198,5 +198,8 @@ public function run(): void } catch (\Throwable $e) { echo "Error: {$e->getMessage()}\n"; } + + $oauth_settings_seeder = new OauthSettingSeeder(); + $oauth_settings_seeder->run(); } } diff --git a/lang/en.json b/lang/en.json index 1f4fbdb74..368a01c0c 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,5 +1,10 @@ { "auth.login": "Login", + "auth.login.azure": "Login with Microsoft", + "auth.login.bitbucket": "Login with Bitbucket", + "auth.login.github": "Login with GitHub", + "auth.login.gitlab": "Login with Gitlab", + "auth.login.google": "Login with Google", "auth.already_registered": "Already registered?", "auth.confirm_password": "Confirm password", "auth.forgot_password": "Forgot password", @@ -10,6 +15,7 @@ "auth.registration_disabled": "Registration is disabled. Please contact the administrator.", "auth.reset_password": "Reset password", "auth.failed": "These credentials do not match our records.", + "auth.failed.callback": "Failed to process callback from login provider.", "auth.failed.password": "The provided password is incorrect.", "auth.failed.email": "We can't find a user with that e-mail address.", "auth.throttle": "Too many login attempts. Please try again in :seconds seconds.", diff --git a/resources/css/app.css b/resources/css/app.css index 1080a84f4..167b7c44a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -175,3 +175,7 @@ input.input-sm { option{ @apply text-white; } + +.toast { + z-index: 1; +} diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index ed288f9ee..6a013b192 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -40,6 +40,11 @@ class="text-xs text-center text-white normal-case bg-transparent border-none rou @endenv {{ __('auth.login') }} + @foreach ($enabled_oauth_providers as $provider_setting) + + {{ __("auth.login.$provider_setting->provider") }} + + @endforeach @if (!$is_registration_enabled)
{{ __('auth.registration_disabled') }}
@endif diff --git a/resources/views/livewire/settings/auth.blade.php b/resources/views/livewire/settings/auth.blade.php new file mode 100644 index 000000000..8c769f233 --- /dev/null +++ b/resources/views/livewire/settings/auth.blade.php @@ -0,0 +1,30 @@ +
+
+
+
+

Authentication

+ + Save + +
+
+
+ @foreach ($oauth_settings_map as $oauth_setting) +
+

{{ucfirst($oauth_setting->provider)}} Oauth

+
+ +
+
+ + + + @if ($oauth_setting->provider == 'azure') + + @endif +
+
+ @endforeach +
+
+
\ No newline at end of file diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php index 7491d8d1c..d65cf856e 100644 --- a/resources/views/livewire/settings/index.blade.php +++ b/resources/views/livewire/settings/index.blade.php @@ -9,6 +9,8 @@ Transactional Email + Authentication
@@ -20,6 +22,9 @@
+
+ +
diff --git a/routes/web.php b/routes/web.php index f49d1b278..e5a022406 100644 --- a/routes/web.php +++ b/routes/web.php @@ -11,7 +11,7 @@ use App\Http\Controllers\Controller; use App\Http\Controllers\MagicController; - +use App\Http\Controllers\OauthController; use App\Livewire\Admin\Index as AdminIndex; use App\Livewire\Dev\Compose as Compose; @@ -93,6 +93,9 @@ Route::get('/auth/link', [Controller::class, 'link'])->name('auth.link'); }); +Route::get('/auth/{provider}/redirect', [OauthController::class, 'redirect'])->name('auth.redirect'); +Route::get('/auth/{provider}/callback', [OauthController::class, 'callback'])->name('auth.callback'); + Route::prefix('magic')->middleware(['auth'])->group(function () { Route::get('/servers', [MagicController::class, 'servers']); Route::get('/destinations', [MagicController::class, 'destinations']);