diff --git a/.env.example b/.env.example index f35c31b0d..eaf788da8 100644 --- a/.env.example +++ b/.env.example @@ -97,13 +97,52 @@ AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" +GOOGLE_ENABLED=true GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_REDIRECT_URI= +GITHUB_ENABLED=true GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= +# Keycloak SSO (https://www.keycloak.org) +KEYCLOAK_ENABLED=false +KEYCLOAK_BASE_URL= +KEYCLOAK_REALM=master +KEYCLOAK_CLIENT_ID= +KEYCLOAK_CLIENT_SECRET= +KEYCLOAK_REDIRECT_URI=/auth/callback/keycloak +KEYCLOAK_DISPLAY_NAME=SSO + +# Okta SSO (https://www.okta.com) +OKTA_ENABLED=false +OKTA_BASE_URL= +OKTA_CLIENT_ID= +OKTA_CLIENT_SECRET= +OKTA_REDIRECT_URI=/auth/callback/okta + +# Azure AD SSO (https://azure.microsoft.com) +AZURE_ENABLED=false +AZURE_TENANT=common +AZURE_CLIENT_ID= +AZURE_CLIENT_SECRET= +AZURE_REDIRECT_URI=/auth/callback/azure + +# Authentik SSO (https://goauthentik.io) +AUTHENTIK_ENABLED=false +AUTHENTIK_BASE_URL= +AUTHENTIK_CLIENT_ID= +AUTHENTIK_CLIENT_SECRET= +AUTHENTIK_REDIRECT_URI=/auth/callback/authentik + +# Auth0 SSO (https://auth0.com) +AUTH0_ENABLED=false +AUTH0_BASE_URL= +AUTH0_CLIENT_ID= +AUTH0_CLIENT_SECRET= +AUTH0_REDIRECT_URI=/auth/callback/auth0 + SENTRY_LARAVEL_DSN= SENTRY_TRACES_SAMPLE_RATE=1.0 diff --git a/app/Enums/SocialiteProvider.php b/app/Enums/SocialiteProvider.php index 627d605ef..c51dc5652 100644 --- a/app/Enums/SocialiteProvider.php +++ b/app/Enums/SocialiteProvider.php @@ -6,6 +6,61 @@ enum SocialiteProvider: string { - case GOOGLE = 'google'; - case GITHUB = 'github'; + case Google = 'google'; + case GitHub = 'github'; + case Keycloak = 'keycloak'; + case Okta = 'okta'; + case Azure = 'azure'; + case Authentik = 'authentik'; + case Auth0 = 'auth0'; + + private const array SSO_PROVIDERS = [ + self::Keycloak->value, + self::Okta->value, + self::Azure->value, + self::Authentik->value, + self::Auth0->value, + ]; + + public function enabled(): bool + { + $key = $this->value; + $defaultEnabled = ! in_array($key, self::SSO_PROVIDERS, true); + + if (! (bool) config("services.{$key}.enabled", $defaultEnabled)) { + return false; + } + + $required = match ($this) { + self::Google, self::GitHub, self::Azure => ['client_id', 'client_secret'], + default => ['client_id', 'client_secret', 'base_url'], + }; + + foreach ($required as $field) { + if (! filled(config("services.{$key}.{$field}"))) { + return false; + } + } + + return true; + } + + /** @return array */ + public static function enabledProviders(): array + { + return array_values(array_filter(self::cases(), fn (self $provider): bool => $provider->enabled())); + } + + public function label(): string + { + return match ($this) { + self::Google => 'Google', + self::GitHub => 'GitHub', + self::Keycloak => (string) config('services.keycloak.display_name', 'Keycloak'), + self::Okta => 'Okta', + self::Azure => 'Microsoft', + self::Authentik => 'Authentik', + self::Auth0 => 'Auth0', + }; + } } diff --git a/app/Http/Controllers/Auth/CallbackController.php b/app/Http/Controllers/Auth/CallbackController.php index 7a2fa67a8..099960399 100644 --- a/app/Http/Controllers/Auth/CallbackController.php +++ b/app/Http/Controllers/Auth/CallbackController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth; use App\Contracts\User\CreatesNewSocialUsers; +use App\Enums\SocialiteProvider; use App\Models\User; use App\Models\UserSocialAccount; use Filament\Notifications\Notification; @@ -24,6 +25,10 @@ public function __invoke( string $provider, CreatesNewSocialUsers $creator ): RedirectResponse { + $providerEnum = SocialiteProvider::tryFrom($provider); + + abort_if(! $providerEnum || ! $providerEnum->enabled(), 404); + if (! $request->has('code')) { return $this->handleError('Authorization was cancelled or failed. Please try again.'); } diff --git a/app/Http/Controllers/Auth/RedirectController.php b/app/Http/Controllers/Auth/RedirectController.php index 1337d4146..918fa0150 100644 --- a/app/Http/Controllers/Auth/RedirectController.php +++ b/app/Http/Controllers/Auth/RedirectController.php @@ -13,6 +13,8 @@ { public function __invoke(SocialiteProvider $provider): RedirectResponse { + abort_if(! $provider->enabled(), 404); + /** @var AbstractProvider $driver */ $driver = Socialite::driver($provider->value); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fb1e23e52..a4d89bed8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -35,6 +35,12 @@ use Laravel\Sanctum\Sanctum; use Relaticle\CustomFields\CustomFields; use Relaticle\SystemAdmin\Models\SystemAdministrator; +use SocialiteProviders\Auth0\Provider as Auth0Provider; +use SocialiteProviders\Authentik\Provider as AuthentikProvider; +use SocialiteProviders\Azure\Provider as AzureProvider; +use SocialiteProviders\Keycloak\Provider as KeycloakProvider; +use SocialiteProviders\Manager\SocialiteWasCalled; +use SocialiteProviders\Okta\Provider as OktaProvider; final class AppServiceProvider extends ServiceProvider { @@ -59,6 +65,7 @@ public function boot(): void $this->configureFilament(); $this->configureGitHubStars(); $this->configureLivewire(); + $this->configureSocialiteProviders(); $this->configureRateLimiting(); $this->configureScribe(); } @@ -226,6 +233,17 @@ private function configureFilament(): void }); } + private function configureSocialiteProviders(): void + { + Facades\Event::listen(function (SocialiteWasCalled $event): void { + $event->extendSocialite('keycloak', KeycloakProvider::class); + $event->extendSocialite('okta', OktaProvider::class); + $event->extendSocialite('azure', AzureProvider::class); + $event->extendSocialite('authentik', AuthentikProvider::class); + $event->extendSocialite('auth0', Auth0Provider::class); + }); + } + /** * Configure GitHub stars count. */ diff --git a/app/Providers/Filament/AppPanelProvider.php b/app/Providers/Filament/AppPanelProvider.php index 1995c0b0d..c6ef4aa27 100644 --- a/app/Providers/Filament/AppPanelProvider.php +++ b/app/Providers/Filament/AppPanelProvider.php @@ -4,6 +4,7 @@ namespace App\Providers\Filament; +use App\Enums\SocialiteProvider; use App\Features\SocialAuth; use App\Filament\Pages\AccessTokens; use App\Filament\Pages\Auth\Login; @@ -183,11 +184,15 @@ public function panel(Panel $panel): Panel $panel ->renderHook( PanelsRenderHook::AUTH_LOGIN_FORM_BEFORE, - fn (): View|Factory => view('filament.auth.social_login_buttons') + fn (): View|Factory => view('filament.auth.social_login_buttons', [ + 'enabledProviders' => SocialiteProvider::enabledProviders(), + ]) ) ->renderHook( PanelsRenderHook::AUTH_REGISTER_FORM_BEFORE, - fn (): View|Factory => view('filament.auth.social_login_buttons') + fn (): View|Factory => view('filament.auth.social_login_buttons', [ + 'enabledProviders' => SocialiteProvider::enabledProviders(), + ]) ); } diff --git a/composer.json b/composer.json index 69525b846..7b02d22d6 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,11 @@ "ryangjchandler/laravel-cloudflare-turnstile": "^3.0", "scalar/laravel": "^0.2.0", "sentry/sentry-laravel": "^4.13", + "socialiteproviders/auth0": "^4.2", + "socialiteproviders/authentik": "^5.3", + "socialiteproviders/keycloak": "^5.3", + "socialiteproviders/microsoft-azure": "^5.2", + "socialiteproviders/okta": "^4.5", "spatie/cpu-load-health-check": "^1.0", "spatie/eloquent-sortable": "^4.4", "spatie/laravel-data": "^4.15", diff --git a/composer.lock b/composer.lock index 81e9326d2..20856d149 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": "1c86c5e64cb30e2d65114a04f883cac7", + "content-hash": "4ae9d799505666954d6402f64efec32d", "packages": [ { "name": "andreiio/blade-remix-icon", @@ -2062,12 +2062,12 @@ "version": "v7.0.3", "source": { "type": "git", - "url": "https://github.com/firebase/php-jwt.git", + "url": "https://github.com/googleapis/php-jwt.git", "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", "reference": "28aa0694bcfdfa5e2959c394d5a1ee7a5083629e", "shasum": "" }, diff --git a/config/services.php b/config/services.php index be3c4bab3..58ee62e3d 100644 --- a/config/services.php +++ b/config/services.php @@ -38,17 +38,61 @@ ], 'google' => [ + 'enabled' => env('GOOGLE_ENABLED', true), 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => env('GOOGLE_REDIRECT_URI'), ], 'github' => [ + 'enabled' => env('GITHUB_ENABLED', true), 'client_id' => env('GITHUB_CLIENT_ID'), 'client_secret' => env('GITHUB_CLIENT_SECRET'), 'redirect' => '/auth/callback/github', ], + 'keycloak' => [ + 'enabled' => env('KEYCLOAK_ENABLED', false), + 'display_name' => env('KEYCLOAK_DISPLAY_NAME', 'Keycloak'), + 'client_id' => env('KEYCLOAK_CLIENT_ID'), + 'client_secret' => env('KEYCLOAK_CLIENT_SECRET'), + 'base_url' => env('KEYCLOAK_BASE_URL'), + 'realms' => env('KEYCLOAK_REALM', 'master'), + 'redirect' => env('KEYCLOAK_REDIRECT_URI', '/auth/callback/keycloak'), + ], + + 'okta' => [ + 'enabled' => env('OKTA_ENABLED', false), + 'client_id' => env('OKTA_CLIENT_ID'), + 'client_secret' => env('OKTA_CLIENT_SECRET'), + 'base_url' => env('OKTA_BASE_URL'), + 'redirect' => env('OKTA_REDIRECT_URI', '/auth/callback/okta'), + ], + + 'azure' => [ + 'enabled' => env('AZURE_ENABLED', false), + 'client_id' => env('AZURE_CLIENT_ID'), + 'client_secret' => env('AZURE_CLIENT_SECRET'), + 'tenant' => env('AZURE_TENANT', 'common'), + 'redirect' => env('AZURE_REDIRECT_URI', '/auth/callback/azure'), + ], + + 'authentik' => [ + 'enabled' => env('AUTHENTIK_ENABLED', false), + 'client_id' => env('AUTHENTIK_CLIENT_ID'), + 'client_secret' => env('AUTHENTIK_CLIENT_SECRET'), + 'base_url' => env('AUTHENTIK_BASE_URL'), + 'redirect' => env('AUTHENTIK_REDIRECT_URI', '/auth/callback/authentik'), + ], + + 'auth0' => [ + 'enabled' => env('AUTH0_ENABLED', false), + 'client_id' => env('AUTH0_CLIENT_ID'), + 'client_secret' => env('AUTH0_CLIENT_SECRET'), + 'base_url' => env('AUTH0_BASE_URL'), + 'redirect' => env('AUTH0_REDIRECT_URI', '/auth/callback/auth0'), + ], + 'fathom' => [ 'site_id' => env('FATHOM_ANALYTICS_SITE_ID'), ], diff --git a/resources/views/components/icons/auth0.blade.php b/resources/views/components/icons/auth0.blade.php new file mode 100644 index 000000000..9752ea6fb --- /dev/null +++ b/resources/views/components/icons/auth0.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/resources/views/components/icons/authentik.blade.php b/resources/views/components/icons/authentik.blade.php new file mode 100644 index 000000000..609929468 --- /dev/null +++ b/resources/views/components/icons/authentik.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/resources/views/components/icons/azure.blade.php b/resources/views/components/icons/azure.blade.php new file mode 100644 index 000000000..f142a0c85 --- /dev/null +++ b/resources/views/components/icons/azure.blade.php @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/views/components/icons/github.blade.php b/resources/views/components/icons/github.blade.php new file mode 100644 index 000000000..fce480f27 --- /dev/null +++ b/resources/views/components/icons/github.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/resources/views/components/icons/google.blade.php b/resources/views/components/icons/google.blade.php new file mode 100644 index 000000000..6a208e3d9 --- /dev/null +++ b/resources/views/components/icons/google.blade.php @@ -0,0 +1,10 @@ + + + + + + diff --git a/resources/views/components/icons/keycloak.blade.php b/resources/views/components/icons/keycloak.blade.php new file mode 100644 index 000000000..e490b2ce8 --- /dev/null +++ b/resources/views/components/icons/keycloak.blade.php @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/resources/views/components/icons/okta.blade.php b/resources/views/components/icons/okta.blade.php new file mode 100644 index 000000000..f72c97f51 --- /dev/null +++ b/resources/views/components/icons/okta.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/resources/views/filament/auth/social_login_buttons.blade.php b/resources/views/filament/auth/social_login_buttons.blade.php index 6c0736eb5..d6d000265 100644 --- a/resources/views/filament/auth/social_login_buttons.blade.php +++ b/resources/views/filament/auth/social_login_buttons.blade.php @@ -1,48 +1,29 @@ @feature(App\Features\SocialAuth::class)
- - - - - - - - - Continue with Google - - - - - - - - - Continue with GitHub - - - -
-
- or -
-
+ @foreach ($enabledProviders as $provider) + + + + Continue with {{ $provider->label() }} + + + @endforeach + @if (count($enabledProviders) > 0) +
+
+ or +
+
+ @endif
@endfeature diff --git a/tests/Feature/Auth/SocialiteLoginTest.php b/tests/Feature/Auth/SocialiteLoginTest.php index 053a70ceb..8e6d5fea7 100644 --- a/tests/Feature/Auth/SocialiteLoginTest.php +++ b/tests/Feature/Auth/SocialiteLoginTest.php @@ -23,20 +23,26 @@ function makeSocialiteUser(string $id, string $name, string $email): SocialiteUs } test('redirect to socialite provider', function () { - Socialite::fake(SocialiteProvider::GOOGLE->value); + config()->set('services.google.client_id', 'test-id'); + config()->set('services.google.client_secret', 'test-secret'); - $response = $this->get(route('auth.socialite.redirect', ['provider' => SocialiteProvider::GOOGLE->value])); + Socialite::fake(SocialiteProvider::Google->value); + + $response = $this->get(route('auth.socialite.redirect', ['provider' => SocialiteProvider::Google->value])); $response->assertRedirect(); }); test('callback from socialite provider creates new user when user does not exist', function () { + config()->set('services.google.client_id', 'test-id'); + config()->set('services.google.client_secret', 'test-secret'); + Socialite::fake( - SocialiteProvider::GOOGLE->value, + SocialiteProvider::Google->value, makeSocialiteUser('123456789', 'Test User', 'test@example.com'), ); - $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::GOOGLE->value, 'code' => 'test-code'])); + $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::Google->value, 'code' => 'test-code'])); $this->assertDatabaseHas('users', [ 'email' => 'test@example.com', @@ -44,7 +50,7 @@ function makeSocialiteUser(string $id, string $name, string $email): SocialiteUs ]); $this->assertDatabaseHas('user_social_accounts', [ - 'provider_name' => SocialiteProvider::GOOGLE->value, + 'provider_name' => SocialiteProvider::Google->value, 'provider_id' => '123456789', ]); @@ -54,6 +60,9 @@ function makeSocialiteUser(string $id, string $name, string $email): SocialiteUs }); test('callback from socialite provider logs in existing user when social account exists', function () { + config()->set('services.google.client_id', 'test-id'); + config()->set('services.google.client_secret', 'test-secret'); + $user = User::factory()->withTeam()->create([ 'email' => 'existing@example.com', 'name' => 'Existing User', @@ -61,16 +70,16 @@ function makeSocialiteUser(string $id, string $name, string $email): SocialiteUs UserSocialAccount::factory()->create([ 'user_id' => $user->id, - 'provider_name' => SocialiteProvider::GOOGLE->value, + 'provider_name' => SocialiteProvider::Google->value, 'provider_id' => '123456789', ]); Socialite::fake( - SocialiteProvider::GOOGLE->value, + SocialiteProvider::Google->value, makeSocialiteUser('123456789', 'Existing User', 'existing@example.com'), ); - $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::GOOGLE->value, 'code' => 'test-code'])); + $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::Google->value, 'code' => 'test-code'])); $this->assertAuthenticated(); $this->assertAuthenticatedAs($user); @@ -79,17 +88,20 @@ function makeSocialiteUser(string $id, string $name, string $email): SocialiteUs }); test('callback from socialite provider links social account to existing user when email matches', function () { + config()->set('services.google.client_id', 'test-id'); + config()->set('services.google.client_secret', 'test-secret'); + $user = User::factory()->withTeam()->create([ 'email' => 'existing@example.com', 'name' => 'Existing User', ]); Socialite::fake( - SocialiteProvider::GOOGLE->value, + SocialiteProvider::Google->value, makeSocialiteUser('123456789', 'Existing User', 'existing@example.com'), ); - $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::GOOGLE->value, 'code' => 'test-code'])); + $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::Google->value, 'code' => 'test-code'])); $response->assertRedirect(); @@ -98,19 +110,25 @@ function makeSocialiteUser(string $id, string $name, string $email): SocialiteUs }); test('callback from socialite provider handles error gracefully', function () { + config()->set('services.google.client_id', 'test-id'); + config()->set('services.google.client_secret', 'test-secret'); + Socialite::fake( - SocialiteProvider::GOOGLE->value, + SocialiteProvider::Google->value, fn () => throw new Exception('Socialite error'), ); - $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::GOOGLE->value, 'code' => 'test-code'])); + $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::Google->value, 'code' => 'test-code'])); $response->assertRedirect(route('login')); $response->assertSessionHasErrors(['login']); }); test('callback from socialite provider handles missing code parameter', function () { - $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::GOOGLE->value])); + config()->set('services.google.client_id', 'test-id'); + config()->set('services.google.client_secret', 'test-secret'); + + $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::Google->value])); $response->assertRedirect(route('login')); $response->assertSessionHasErrors(['login']); @@ -119,3 +137,120 @@ function makeSocialiteUser(string $id, string $name, string $email): SocialiteUs $errors = session('errors')->getBag('default'); expect($errors->first('login'))->toBe('Authorization was cancelled or failed. Please try again.'); }); + +test('provider enabled() returns false when credentials are missing', function () { + config()->set('services.keycloak.enabled', true); + config()->set('services.keycloak.client_id', null); + config()->set('services.keycloak.client_secret', 'secret'); + config()->set('services.keycloak.base_url', 'https://keycloak.example.com'); + + expect(SocialiteProvider::Keycloak->enabled())->toBeFalse(); +}); + +test('provider enabled() returns false when explicitly disabled', function () { + config()->set('services.google.enabled', false); + config()->set('services.google.client_id', 'id'); + config()->set('services.google.client_secret', 'secret'); + + expect(SocialiteProvider::Google->enabled())->toBeFalse(); +}); + +test('provider enabled() returns true when configured and enabled', function () { + config()->set('services.keycloak.enabled', true); + config()->set('services.keycloak.client_id', 'id'); + config()->set('services.keycloak.client_secret', 'secret'); + config()->set('services.keycloak.base_url', 'https://keycloak.example.com'); + + expect(SocialiteProvider::Keycloak->enabled())->toBeTrue(); +}); + +test('provider enabled() defaults to true for Google and GitHub', function () { + config()->set('services.google.client_id', 'id'); + config()->set('services.google.client_secret', 'secret'); + + expect(SocialiteProvider::Google->enabled())->toBeTrue(); +}); + +test('provider enabled() defaults to false for SSO providers', function () { + config()->set('services.keycloak.client_id', 'id'); + config()->set('services.keycloak.client_secret', 'secret'); + config()->set('services.keycloak.base_url', 'https://keycloak.example.com'); + + expect(SocialiteProvider::Keycloak->enabled())->toBeFalse(); +}); + +test('enabledProviders() returns only enabled providers', function () { + config()->set('services.google.enabled', true); + config()->set('services.google.client_id', 'id'); + config()->set('services.google.client_secret', 'secret'); + + config()->set('services.github.enabled', false); + + config()->set('services.keycloak.enabled', true); + config()->set('services.keycloak.client_id', 'id'); + config()->set('services.keycloak.client_secret', 'secret'); + config()->set('services.keycloak.base_url', 'https://keycloak.example.com'); + + $providers = SocialiteProvider::enabledProviders(); + + expect($providers)->toContain(SocialiteProvider::Google) + ->toContain(SocialiteProvider::Keycloak) + ->not->toContain(SocialiteProvider::GitHub); +}); + +test('redirect returns 404 for disabled provider', function () { + config()->set('services.google.enabled', false); + + $response = $this->get(route('auth.socialite.redirect', ['provider' => SocialiteProvider::Google->value])); + + $response->assertNotFound(); +}); + +test('callback returns 404 for disabled provider', function () { + config()->set('services.google.enabled', false); + + $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::Google->value, 'code' => 'test-code'])); + + $response->assertNotFound(); +}); + +test('redirect to keycloak provider when enabled', function () { + config()->set('services.keycloak.enabled', true); + config()->set('services.keycloak.client_id', 'test-id'); + config()->set('services.keycloak.client_secret', 'test-secret'); + config()->set('services.keycloak.base_url', 'https://keycloak.example.com'); + config()->set('services.keycloak.realms', 'master'); + config()->set('services.keycloak.redirect', '/auth/callback/keycloak'); + + $response = $this->get(route('auth.socialite.redirect', ['provider' => SocialiteProvider::Keycloak->value])); + + $response->assertRedirect(); + expect($response->headers->get('Location'))->toContain('keycloak.example.com'); +}); + +test('callback from keycloak provider creates new user', function () { + config()->set('services.keycloak.enabled', true); + config()->set('services.keycloak.client_id', 'test-id'); + config()->set('services.keycloak.client_secret', 'test-secret'); + config()->set('services.keycloak.base_url', 'https://keycloak.example.com'); + + Socialite::fake( + SocialiteProvider::Keycloak->value, + makeSocialiteUser('kc-user-123', 'Keycloak User', 'keycloak@example.com'), + ); + + $response = $this->get(route('auth.socialite.callback', ['provider' => SocialiteProvider::Keycloak->value, 'code' => 'test-code'])); + + $this->assertDatabaseHas('users', [ + 'email' => 'keycloak@example.com', + 'name' => 'Keycloak User', + ]); + + $this->assertDatabaseHas('user_social_accounts', [ + 'provider_name' => SocialiteProvider::Keycloak->value, + 'provider_id' => 'kc-user-123', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(url()->getAppUrl()); +}); diff --git a/tests/Feature/Public/PublicPagesTest.php b/tests/Feature/Public/PublicPagesTest.php index 9b4fffd88..891787aa2 100644 --- a/tests/Feature/Public/PublicPagesTest.php +++ b/tests/Feature/Public/PublicPagesTest.php @@ -175,12 +175,18 @@ }); it('accepts github as a provider for redirect', function () { + config()->set('services.github.client_id', 'test-id'); + config()->set('services.github.client_secret', 'test-secret'); + $response = $this->get('/auth/redirect/github'); $response->assertStatus(302); // Redirect to GitHub }); it('accepts google as a provider for redirect', function () { + config()->set('services.google.client_id', 'test-id'); + config()->set('services.google.client_secret', 'test-secret'); + $response = $this->get('/auth/redirect/google'); $response->assertStatus(302); // Redirect to Google