diff --git a/app/Filament/Pages/Auth/Login.php b/app/Filament/Pages/Auth/Login.php index d1bf4b007..774088444 100644 --- a/app/Filament/Pages/Auth/Login.php +++ b/app/Filament/Pages/Auth/Login.php @@ -3,9 +3,12 @@ namespace App\Filament\Pages\Auth; use Coderflex\FilamentTurnstile\Forms\Components\Turnstile; +use Filament\Forms\Components\Actions; +use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Component; use Filament\Forms\Components\TextInput; use Filament\Pages\Auth\Login as BaseLogin; +use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; class Login extends BaseLogin @@ -19,6 +22,7 @@ class Login extends BaseLogin $this->getLoginFormComponent(), $this->getPasswordFormComponent(), $this->getRememberFormComponent(), + $this->getOAuthFormComponent(), Turnstile::make('captcha') ->hidden(!config('turnstile.turnstile_enabled')) ->validationMessages([ @@ -49,6 +53,25 @@ class Login extends BaseLogin ->extraInputAttributes(['tabindex' => 1]); } + protected function getOAuthFormComponent(): Component + { + $actions = []; + + foreach (config('auth.oauth') as $name => $data) { + if (!$data['enabled']) { + continue; + } + + $actions[] = Action::make("oauth_$name") + ->label(Str::title($name)) + ->icon($data['icon']) + ->color($data['color']) + ->url(route('auth.oauth.redirect', ['driver' => $name], false)); + } + + return Actions::make($actions); + } + protected function getCredentialsFromFormData(array $data): array { $loginType = filter_var($data['login'], FILTER_VALIDATE_EMAIL) ? 'email' : 'username'; diff --git a/app/Filament/Resources/UserResource/Pages/EditProfile.php b/app/Filament/Resources/UserResource/Pages/EditProfile.php index 5382ec07c..389542253 100644 --- a/app/Filament/Resources/UserResource/Pages/EditProfile.php +++ b/app/Filament/Resources/UserResource/Pages/EditProfile.php @@ -9,6 +9,7 @@ use App\Models\ApiKey; use App\Models\User; use App\Services\Users\ToggleTwoFactorService; use App\Services\Users\TwoFactorSetupService; +use App\Services\Users\UserUpdateService; use chillerlan\QRCode\Common\EccLevel; use chillerlan\QRCode\Common\Version; use chillerlan\QRCode\QRCode; @@ -16,6 +17,7 @@ use chillerlan\QRCode\QROptions; use Closure; use DateTimeZone; use Exception; +use Filament\Forms\Components\Actions; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Grid; use Filament\Forms\Components\Placeholder; @@ -33,7 +35,9 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Hash; use Illuminate\Support\HtmlString; +use Illuminate\Support\Str; use Illuminate\Validation\Rules\Password; +use Laravel\Socialite\Facades\Socialite; /** * @method User getUser() @@ -113,6 +117,53 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile ->options(fn (User $user) => $user->getAvailableLanguages()), ]), + Tab::make('OAuth') + ->icon('tabler-brand-oauth') + ->visible(function () { + foreach (config('auth.oauth') as $name => $data) { + if ($data['enabled']) { + return true; + } + } + + return false; + }) + ->schema(function () { + $providers = []; + + foreach (config('auth.oauth') as $name => $data) { + if (!$data['enabled']) { + continue; + } + + $unlink = array_key_exists($name, $this->getUser()->oauth); + + $providers[] = Action::make("oauth_$name") + ->label(($unlink ? 'Unlink ' : 'Link ') . Str::title($name)) + ->icon($unlink ? 'tabler-unlink' : 'tabler-link') + ->color($data['color']) + ->action(function (UserUpdateService $updateService) use ($name, $unlink) { + if ($unlink) { + $oauth = auth()->user()->oauth; + unset($oauth[$name]); + + $updateService->handle(auth()->user(), ['oauth' => $oauth]); + + $this->fillForm(); + + Notification::make() + ->title("OAuth provider '$name' unlinked") + ->success() + ->send(); + } elseif (config("auth.oauth.$name.enabled")) { + redirect(Socialite::with($name)->redirect()->getTargetUrl()); + } + }); + } + + return [Actions::make($providers)]; + }), + Tab::make('2FA') ->icon('tabler-shield-lock') ->schema(function (TwoFactorSetupService $setupService) { diff --git a/app/Http/Controllers/Auth/OAuthController.php b/app/Http/Controllers/Auth/OAuthController.php index a0bb554a0..9df0978c1 100644 --- a/app/Http/Controllers/Auth/OAuthController.php +++ b/app/Http/Controllers/Auth/OAuthController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers\Auth; +use App\Filament\Resources\UserResource\Pages\EditProfile; +use Filament\Notifications\Notification; use Illuminate\Auth\AuthManager; use Illuminate\Http\RedirectResponse; use Laravel\Socialite\Facades\Socialite; @@ -26,6 +28,11 @@ class OAuthController extends Controller */ protected function redirect(string $driver): RedirectResponse { + // Driver is disabled - redirect to normal login + if (!config("auth.oauth.$driver.enabled")) { + return redirect()->route('auth.login'); + } + return Socialite::with($driver)->redirect(); } @@ -34,6 +41,11 @@ class OAuthController extends Controller */ protected function callback(Request $request, string $driver): RedirectResponse { + // Driver is disabled - redirect to normal login + if (!config("auth.oauth.$driver.enabled")) { + return redirect()->route('auth.login'); + } + $oauthUser = Socialite::driver($driver)->user(); // User is already logged in and wants to link a new OAuth Provider @@ -43,15 +55,21 @@ class OAuthController extends Controller $this->updateService->handle($request->user(), ['oauth' => $oauth]); - return redirect()->route('account'); + return redirect(EditProfile::getUrl(['tab' => '-oauth-tab'])); } try { $user = User::query()->whereJsonContains('oauth->'. $driver, $oauthUser->getId())->firstOrFail(); $this->auth->guard()->login($user, true); - } catch (Exception $e) { + } catch (Exception) { // No user found - redirect to normal login + Notification::make() + ->title('No linked User found') + ->danger() + ->persistent() + ->send(); + return redirect()->route('auth.login'); } diff --git a/app/Http/Controllers/Base/OAuthController.php b/app/Http/Controllers/Base/OAuthController.php deleted file mode 100644 index 4a8b4f38d..000000000 --- a/app/Http/Controllers/Base/OAuthController.php +++ /dev/null @@ -1,43 +0,0 @@ -get('driver'); - - return Socialite::with($driver)->redirect(); - } - - /** - * Remove a OAuth link - */ - protected function unlink(Request $request): Response - { - $oauth = $request->user()->oauth; - unset($oauth[$request->get('driver')]); - - $this->updateService->handle($request->user(), ['oauth' => $oauth]); - - return new Response('', Response::HTTP_NO_CONTENT); - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 9c719021c..e9dbc067d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -20,7 +20,6 @@ use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; use Laravel\Sanctum\Sanctum; -use SocialiteProviders\Discord\Provider; use SocialiteProviders\Manager\SocialiteWasCalled; class AppServiceProvider extends ServiceProvider @@ -69,8 +68,19 @@ class AppServiceProvider extends ServiceProvider Scramble::registerApi('client', ['api_path' => 'api/client', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens); Scramble::registerApi('remote', ['api_path' => 'api/remote', 'info' => ['version' => '1.0']])->afterOpenApiGenerated($bearerTokens); - Event::listen(function (SocialiteWasCalled $event) { - $event->extendSocialite('discord', Provider::class); + $oauthProviders = []; + foreach (config('auth.oauth') as $name => $data) { + config()->set("services.$name", array_merge($data['service'], ['redirect' => "/auth/oauth/callback/$name"])); + + if (isset($data['provider'])) { + $oauthProviders[$name] = $data['provider']; + } + } + + Event::listen(function (SocialiteWasCalled $event) use ($oauthProviders) { + foreach ($oauthProviders as $name => $provider) { + $event->extendSocialite($name, $provider); + } }); FilamentColor::register([ diff --git a/composer.json b/composer.json index e94f0ca6c..f37c67112 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,9 @@ "predis/predis": "~2.1.1", "ryangjchandler/blade-tabler-icons": "^2.3", "s1lentium/iptools": "~1.2.0", + "socialiteproviders/authentik": "^5.2", "socialiteproviders/discord": "^4.2", + "socialiteproviders/steam": "^4.2", "spatie/laravel-fractal": "^6.2", "spatie/laravel-permission": "^6.9", "spatie/laravel-query-builder": "^5.8.1", diff --git a/composer.lock b/composer.lock index 23ba2ff35..42a9f16cf 100644 --- a/composer.lock +++ b/composer.lock @@ -6825,6 +6825,56 @@ }, "time": "2022-08-17T14:28:59+00:00" }, + { + "name": "socialiteproviders/authentik", + "version": "5.2.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Authentik.git", + "reference": "4cf129cf04728a38e0531c54454464b162f0fa66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Authentik/zipball/4cf129cf04728a38e0531c54454464b162f0fa66", + "reference": "4cf129cf04728a38e0531c54454464b162f0fa66", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^8.0", + "socialiteproviders/manager": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Authentik\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "rf152", + "email": "git@rf152.co.uk" + } + ], + "description": "Authentik OAuth2 Provider for Laravel Socialite", + "keywords": [ + "authentik", + "laravel", + "oauth", + "provider", + "socialite" + ], + "support": { + "docs": "https://socialiteproviders.com/authentik", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2023-11-07T22:21:16+00:00" + }, { "name": "socialiteproviders/discord", "version": "4.2.0", @@ -6949,6 +6999,56 @@ }, "time": "2024-11-10T01:56:18+00:00" }, + { + "name": "socialiteproviders/steam", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SocialiteProviders/Steam.git", + "reference": "922f82a26fb7243d7e7ff2ec8ba7e957e7b9eeb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SocialiteProviders/Steam/zipball/922f82a26fb7243d7e7ff2ec8ba7e957e7b9eeb7", + "reference": "922f82a26fb7243d7e7ff2ec8ba7e957e7b9eeb7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.2 || ^8.0", + "socialiteproviders/manager": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "SocialiteProviders\\Steam\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christopher Eklund", + "email": "eklundchristopher@gmail.com" + } + ], + "description": "Steam OpenID Provider for Laravel Socialite", + "keywords": [ + "OpenId", + "laravel", + "provider", + "socialite", + "steam" + ], + "support": { + "docs": "https://socialiteproviders.com/steam", + "issues": "https://github.com/socialiteproviders/providers/issues", + "source": "https://github.com/socialiteproviders/providers" + }, + "time": "2022-03-28T22:38:40+00:00" + }, { "name": "spatie/color", "version": "1.6.0", diff --git a/config/auth.php b/config/auth.php index eefb4ede4..78ec9b3f1 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,5 +1,7 @@ [ @@ -23,4 +25,115 @@ return [ ], ], + 'oauth' => [ + // Default providers + 'facebook' => [ + 'enabled' => env('OAUTH_FACEBOOK_ENABLED'), + 'icon' => 'tabler-brand-facebook', + 'color' => Color::hex('#1877f2'), + 'service' => [ + 'client_id' => env('OAUTH_FACEBOOK_CLIENT_ID'), + 'client_secret' => env('OAUTH_FACEBOOK_CLIENT_SECRET'), + ], + ], + 'x' => [ + 'enabled' => env('OAUTH_X_ENABLED'), + 'icon' => 'tabler-brand-x', + 'color' => Color::hex('#1da1f2'), + 'service' => [ + 'client_id' => env('OAUTH_X_CLIENT_ID'), + 'client_secret' => env('OAUTH_X_CLIENT_SECRET'), + ], + ], + 'linkedin' => [ + 'enabled' => env('OAUTH_LINKEDIN_ENABLED'), + 'icon' => 'tabler-brand-linkedin', + 'color' => Color::hex('#0a66c2'), + 'service' => [ + 'client_id' => env('OAUTH_LINKEDIN_CLIENT_ID'), + 'client_secret' => env('OAUTH_LINKEDIN_CLIENT_SECRET'), + ], + ], + 'google' => [ + 'enabled' => env('OAUTH_GOOGLE_ENABLED'), + 'icon' => 'tabler-brand-google', + 'color' => Color::hex('#4285f4'), + 'service' => [ + 'client_id' => env('OAUTH_GOOGLE_CLIENT_ID'), + 'client_secret' => env('OAUTH_GOOGLE_CLIENT_SECRET'), + ], + ], + 'github' => [ + 'enabled' => env('OAUTH_GITHUB_ENABLED'), + 'icon' => 'tabler-brand-github', + 'color' => Color::hex('#4078c0'), + 'service' => [ + 'client_id' => env('OAUTH_GITHUB_CLIENT_ID'), + 'client_secret' => env('OAUTH_GITHUB_CLIENT_SECRET'), + ], + ], + 'gitlab' => [ + 'enabled' => env('OAUTH_GITLAB_ENABLED'), + 'icon' => 'tabler-brand-gitlab', + 'color' => Color::hex('#fca326'), + 'service' => [ + 'client_id' => env('OAUTH_GITLAB_CLIENT_ID'), + 'client_secret' => env('OAUTH_GITLAB_CLIENT_SECRET'), + ], + ], + 'bitbucket' => [ + 'enabled' => env('OAUTH_BITBUCKET_ENABLED'), + 'icon' => 'tabler-brand-bitbucket', + 'color' => Color::hex('#205081'), + 'service' => [ + 'client_id' => env('OAUTH_BITBUCKET_CLIENT_ID'), + 'client_secret' => env('OAUTH_BITBUCKET_CLIENT_SECRET'), + ], + ], + 'slack' => [ + 'enabled' => env('OAUTH_SLACK_ENABLED'), + 'icon' => 'tabler-brand-slack', + 'color' => Color::hex('#6ecadc'), + 'service' => [ + 'client_id' => env('OAUTH_SLACK_CLIENT_ID'), + 'client_secret' => env('OAUTH_SLACK_CLIENT_SECRET'), + ], + ], + + // Additional providers from socialiteproviders.com + 'authentik' => [ + 'enabled' => env('OAUTH_AUTHENTIK_ENABLED'), + 'icon' => null, + 'color' => Color::hex('#fd4b2d'), + 'service' => [ + 'base_url' => env('OAUTH_AUTHENTIK_BASE_URL'), + 'client_id' => env('OAUTH_AUTHENTIK_CLIENT_ID'), + 'client_secret' => env('OAUTH_AUTHENTIK_CLIENT_SECRET'), + ], + 'provider' => \SocialiteProviders\Authentik\Provider::class, + ], + 'discord' => [ + 'enabled' => env('OAUTH_DISCORD_ENABLED'), + 'icon' => 'tabler-brand-discord', + 'color' => Color::hex('#5865F2'), + 'service' => [ + 'client_id' => env('OAUTH_DISCORD_CLIENT_ID'), + 'client_secret' => env('OAUTH_DISCORD_CLIENT_SECRET'), + ], + 'provider' => \SocialiteProviders\Discord\Provider::class, + ], + 'steam' => [ + 'enabled' => env('OAUTH_STEAM_ENABLED'), + 'icon' => 'tabler-brand-steam', + 'color' => Color::hex('#00adee'), + 'service' => [ + 'client_secret' => env('OAUTH_STEAM_CLIENT_SECRET'), + 'allowed_hosts' => [ + env('APP_URL'), + ], + ], + 'provider' => \SocialiteProviders\Steam\Provider::class, + ], + ], + ]; diff --git a/config/services.php b/config/services.php index 7311fdc16..62e0a08a8 100644 --- a/config/services.php +++ b/config/services.php @@ -9,16 +9,4 @@ return [ 'scheme' => 'https', ], - 'github' => [ - 'client_id' => env('OAUTH_GITHUB_CLIENT_ID'), - 'client_secret' => env('OAUTH_GITHUB_CLIENT_SECRET'), - 'redirect' => '/auth/oauth/callback/github', - ], - - 'discord' => [ - 'client_id' => env('OAUTH_DISCORD_CLIENT_ID'), - 'client_secret' => env('OAUTH_DISCORD_CLIENT_SECRET'), - 'redirect' => '/auth/oauth/callback/discord', - ], - ]; diff --git a/routes/base.php b/routes/base.php index b07e1e531..d10490b2e 100644 --- a/routes/base.php +++ b/routes/base.php @@ -10,9 +10,6 @@ Route::get('/account', [Base\IndexController::class, 'index']) ->withoutMiddleware(RequireTwoFactorAuthentication::class) ->name('account'); -Route::get('/account/oauth/link', [Base\OAuthController::class, 'link'])->name('account.oauth.link'); -Route::get('/account/oauth/unlink', [Base\OAuthController::class, 'unlink'])->name('account.oauth.unlink'); - Route::get('/locales/locale.json', Base\LocaleController::class) ->withoutMiddleware(['auth', RequireTwoFactorAuthentication::class]) ->where('namespace', '.*');