Add oauth support

- Support azure, bitbucket, github, gitlab, google providers
- Add authentication page to settings

Co-authored-by: Suraj Kumar <srjkmr1024@gmail.com>
Co-authored-by: Michael Castanieto <mcastanieto@gmail.com>
Co-authored-by: Mike Kim <m.kim4247@gmail.com>
This commit is contained in:
Pat Rocchio 2024-03-06 11:30:19 -05:00
parent 46ed17c99e
commit 1f37318f79
21 changed files with 563 additions and 4 deletions

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
class OauthController extends Controller {
public function redirect(string $provider)
{
$socialite_provider = get_socialite_provider($provider);
return $socialite_provider->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')]);
}
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Livewire\Settings;
use Livewire\Component;
use App\Models\OauthSetting;
class Auth extends Component {
public $oauth_settings_map;
protected function rules() {
return OauthSetting::all()->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!');
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
class OauthSetting extends Model
{
use HasFactory;
protected function clientSecret(): Attribute
{
return Attribute::make(
get: fn (string | null $value) => empty($value) ? null : Crypt::decryptString($value),
set: fn (string | null $value) => empty($value) ? null : Crypt::encryptString($value),
);
}
}

View File

@ -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
{

View File

@ -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,
]);
});

View File

@ -0,0 +1,26 @@
<?php
use App\Models\OauthSetting;
use Laravel\Socialite\Facades\Socialite;
function get_socialite_provider(string $provider)
{
$oauth_setting = OauthSetting::firstWhere('provider', $provider);
$config = [
'client_id' => $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
);
}

View File

@ -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",

273
composer.lock generated
View File

@ -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",

View File

@ -187,6 +187,7 @@
/*
* Package Service Providers...
*/
\SocialiteProviders\Manager\ServiceProvider::class,
/*
* Application Service Providers...

View File

@ -30,5 +30,4 @@
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
];

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('password')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('password')->nullable(false)->change();
});
}
};

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('oauth_settings', function (Blueprint $table) {
$table->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');
}
};

View File

@ -32,6 +32,7 @@ public function run(): void
StandalonePostgresqlSeeder::class,
ScheduledDatabaseBackupSeeder::class,
ScheduledDatabaseBackupExecutionSeeder::class,
OauthSettingSeeder::class,
]);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace Database\Seeders;
use App\Models\OauthSetting;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class OauthSettingSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
OauthSetting::firstOrCreate([
'id' => 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',
]);
}
}

View File

@ -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();
}
}

View File

@ -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.",

View File

@ -175,3 +175,7 @@ input.input-sm {
option{
@apply text-white;
}
.toast {
z-index: 1;
}

View File

@ -40,6 +40,11 @@ class="text-xs text-center text-white normal-case bg-transparent border-none rou
</a>
@endenv
<x-forms.button type="submit">{{ __('auth.login') }}</x-forms.button>
@foreach ($enabled_oauth_providers as $provider_setting)
<x-forms.button type="button" onclick="document.location.href='/auth/{{$provider_setting->provider}}/redirect'">
{{ __("auth.login.$provider_setting->provider") }}
</x-forms.button>
@endforeach
@if (!$is_registration_enabled)
<div class="text-center ">{{ __('auth.registration_disabled') }}</div>
@endif

View File

@ -0,0 +1,30 @@
<div>
<form wire:submit='submit' class="flex flex-col">
<div class="flex flex-col">
<div class="flex items-center gap-2">
<h2>Authentication</h2>
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</div>
<div class="flex flex-col gap-2 pt-4">
@foreach ($oauth_settings_map as $oauth_setting)
<div class="p-4 border border-coolgray-500">
<h3>{{ucfirst($oauth_setting->provider)}} Oauth</h3>
<div class="w-32">
<x-forms.checkbox instantSave id="oauth_settings_map.{{$oauth_setting->provider}}.enabled" label="Enabled" />
</div>
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input id="oauth_settings_map.{{$oauth_setting->provider}}.client_id" label="Client ID" />
<x-forms.input id="oauth_settings_map.{{$oauth_setting->provider}}.client_secret" type="password" label="Client Secret" />
<x-forms.input id="oauth_settings_map.{{$oauth_setting->provider}}.redirect_uri" label="Redirect URI" />
@if ($oauth_setting->provider == 'azure')
<x-forms.input id="oauth_settings_map.{{$oauth_setting->provider}}.tenant" label="Tenant" />
@endif
</div>
</div>
@endforeach
</div>
</form>
</div>

View File

@ -9,6 +9,8 @@
<a :class="activeTab === 'smtp' && 'text-white'"
@click.prevent="activeTab = 'smtp'; window.location.hash = 'smtp'" href="#">Transactional
Email</a>
<a :class="activeTab === 'auth' && 'text-white'"
@click.prevent="activeTab = 'auth'; window.location.hash = 'auth'" href="#">Authentication</a>
</div>
<div class="w-full pl-8">
<div x-cloak x-show="activeTab === 'general'" class="h-full">
@ -20,6 +22,9 @@
<div x-cloak x-show="activeTab === 'smtp'" class="h-full">
<livewire:settings.email :settings="$settings" />
</div>
<div x-cloak x-show="activeTab === 'auth'" class="h-full">
<livewire:settings.auth />
</div>
</div>
</div>
</div>

View File

@ -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']);