mirror of
https://github.com/pelican-dev/panel.git
synced 2025-05-19 19:54:45 +02:00
Add Oauth frontend and backend improvements (#718)
* better oauth provider loading * add auth frontend * add configs for all default providers * add more default providers * add env variables to enable oauth providers * small refactor to link/ unlink routes * add oauth tab to (admin) profile * use redirects instead of exceptions * add notification if no oauth user is found * use import in config * remove whmcs provider * replace hardcoded links with `route` * redirect to account page on unlink * remove unnecessary controller and handle linking/ unlinking in action * only show oauth tab if at least one oauth provider is enabled
This commit is contained in:
parent
951fc73363
commit
b208835ed4
@ -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';
|
||||
|
@ -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) {
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Base;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Users\UserUpdateService;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class OAuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* OAuthController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private UserUpdateService $updateService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Link a new OAuth
|
||||
*/
|
||||
protected function link(Request $request): RedirectResponse
|
||||
{
|
||||
$driver = $request->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);
|
||||
}
|
||||
}
|
@ -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([
|
||||
|
@ -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",
|
||||
|
100
composer.lock
generated
100
composer.lock
generated
@ -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",
|
||||
|
113
config/auth.php
113
config/auth.php
@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Filament\Support\Colors\Color;
|
||||
|
||||
return [
|
||||
|
||||
'lockout' => [
|
||||
@ -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,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -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',
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -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', '.*');
|
||||
|
Loading…
x
Reference in New Issue
Block a user