mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 10:05:47 -05:00
feat(auth): consolidate OAuth settings into standard authentication page
Display all OAuth providers on one settings page, prioritize OpenID Connect, and add its provider icon.
This commit is contained in:
@@ -66,11 +66,13 @@ class SettingsOauth extends Component
|
||||
$this->settings = instanceSettings();
|
||||
$this->selectedProvider = $provider;
|
||||
$this->disable_registration_when_oauth_enabled = (bool) $this->settings->disable_registration_when_oauth_enabled;
|
||||
$this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) {
|
||||
$carry[$setting->provider] = $this->oauthSettingToArray($setting);
|
||||
$this->oauth_settings_map = OauthSetting::all()
|
||||
->sortBy(fn (OauthSetting $setting): string => $setting->isOidc() ? '' : $setting->provider)
|
||||
->reduce(function ($carry, $setting) {
|
||||
$carry[$setting->provider] = $this->oauthSettingToArray($setting);
|
||||
|
||||
return $carry;
|
||||
}, []);
|
||||
return $carry;
|
||||
}, []);
|
||||
|
||||
if ($this->selectedProvider !== null && ! array_key_exists($this->selectedProvider, $this->oauth_settings_map)) {
|
||||
abort(404);
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>OpenID Connect</title>
|
||||
<path fill-rule="evenodd" d="M10 5.5a7.5 7.5 0 1 0 7.5 7.5c0-.9-.16-1.77-.45-2.57l-2.78 1.04c.15.48.23.99.23 1.53a4.5 4.5 0 1 1-4.5-4.5c.54 0 1.05.09 1.53.26l1.05-2.8A7.48 7.48 0 0 0 10 5.5Z" clip-rule="evenodd"/>
|
||||
<path d="M16.5 1 12 5.5h3V10h3V5.5h3L16.5 1Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 383 B |
@@ -1,153 +1,122 @@
|
||||
<div>
|
||||
<x-slot:title>
|
||||
Settings | Coolify
|
||||
Authentication | Coolify
|
||||
</x-slot>
|
||||
<x-settings.navbar />
|
||||
|
||||
<div class="flex flex-col h-full gap-8 sm:flex-row">
|
||||
<div class="sub-menu-wrapper">
|
||||
<a class="sub-menu-item {{ $selectedProvider === null ? 'menu-item-active' : '' }}" {{ wireNavigate() }}
|
||||
href="{{ route('settings.oauth') }}"><span class="menu-item-label">General</span></a>
|
||||
<x-settings.layout>
|
||||
<x-slot:submenu>
|
||||
<div
|
||||
x-data="{ activeProvider: location.hash.slice(1).replace('-oauth-section', '') || @js($selectedProvider ?? array_key_first($oauth_settings_map)) }"
|
||||
@hashchange.window="activeProvider = location.hash.slice(1).replace('-oauth-section', '')">
|
||||
<nav aria-label="OAuth providers" class="grid gap-0.5 py-1">
|
||||
@foreach ($oauth_settings_map as $provider => $oauth_setting)
|
||||
<a href="#{{ $provider }}-oauth-section" class="menu-item min-h-8! py-1! text-[12px]!"
|
||||
:class="{ 'menu-item-active': activeProvider === '{{ $provider }}' }"
|
||||
@click.prevent="activeProvider = '{{ $provider }}'; history.replaceState(null, '', '#{{ $provider }}-oauth-section'); window.scrollToSettingsSection?.('{{ $provider }}-oauth-section')">
|
||||
<span class="menu-item-icon bg-current"
|
||||
style="mask: url('{{ asset('svgs/' . $provider . '.svg') }}') center / contain no-repeat; -webkit-mask: url('{{ asset('svgs/' . $provider . '.svg') }}') center / contain no-repeat;"></span>
|
||||
<span class="menu-item-label">{{ $oauth_setting['label'] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
</x-slot:submenu>
|
||||
|
||||
<form wire:submit="submit" class="application-settings-form flex w-full min-w-0 flex-col gap-6">
|
||||
<x-unsaved-bar action="submit" />
|
||||
|
||||
<x-application.settings-section title="Registration"
|
||||
description="Control password registration when an OAuth provider is available.">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="disable_registration_when_oauth_enabled"
|
||||
label="Disable password registration when OAuth is enabled"
|
||||
helper="OAuth providers can still create users when registration is enabled for that provider."
|
||||
instantSave="saveRegistrationPolicy" />
|
||||
</x-application.settings-section>
|
||||
|
||||
@foreach ($oauth_settings_map as $provider => $oauth_setting)
|
||||
<a class="sub-menu-item {{ $selectedProvider === $provider ? 'menu-item-active' : '' }}"
|
||||
{{ wireNavigate() }} href="{{ route('settings.oauth.provider', $provider) }}"><span
|
||||
class="menu-item-label">{{ $oauth_setting['label'] }}</span></a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<form wire:submit='submit' class="flex flex-col w-full">
|
||||
@if ($selectedProvider === null)
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<h2>Authentication</h2>
|
||||
</div>
|
||||
<div class="pb-4">General authentication settings for your Coolify instance.</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<h3>Registration</h3>
|
||||
<x-helper
|
||||
helper="When enabled, the normal registration page is hidden if at least one OAuth provider is enabled. OAuth providers can still create users if their provider-specific registration option allows it." />
|
||||
<x-application.settings-section id="{{ $provider }}-oauth-section" class="scroll-mt-28"
|
||||
title="{{ $oauth_setting['label'] }}">
|
||||
<x-slot:actions>
|
||||
<div x-data="{ enabled: @js((bool) $oauth_setting['enabled']), provider: @js($provider) }">
|
||||
<x-forms.button canGate="update" :canResource="$settings" type="button"
|
||||
:isHighlighted="!$oauth_setting['enabled']"
|
||||
x-on:click="
|
||||
if (!enabled) {
|
||||
const invalidField = [...$el.closest('section').querySelectorAll('[required]')]
|
||||
.find(field => !field.checkValidity());
|
||||
if (invalidField) { invalidField.reportValidity(); return; }
|
||||
}
|
||||
$wire.toggleProvider(provider);
|
||||
">
|
||||
{{ $oauth_setting['enabled'] ? 'Disable' : 'Enable' }}
|
||||
</x-forms.button>
|
||||
</div>
|
||||
<div class="w-full max-w-2xl">
|
||||
</x-slot:actions>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.redirect_uri"
|
||||
placeholder="{{ route('auth.callback', $provider) }}" label="Redirect URI" />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.client_id" label="Client ID" required />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.client_secret" type="password"
|
||||
label="Client secret" autocomplete="new-password" required />
|
||||
|
||||
@if ($provider === 'azure')
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.tenant" label="Tenant" required />
|
||||
@endif
|
||||
|
||||
@if ($provider === 'google')
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.tenant"
|
||||
helper="Optional hosted domain supplied to Google as a login hint."
|
||||
label="Hosted domain" />
|
||||
@endif
|
||||
|
||||
@if (in_array($provider, ['authentik', 'clerk', 'zitadel', 'gitlab'], true))
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.base_url" label="Base URL"
|
||||
:required="in_array($provider, ['authentik', 'clerk'], true)" />
|
||||
@endif
|
||||
|
||||
@if ($provider === 'oidc')
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.base_url" label="Issuer URL" required
|
||||
helper="OpenID Provider issuer URL, for example https://example.okta.com. Coolify uses it to discover the authorization, token, userinfo, and JWKS endpoints." />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.custom_label" label="Login button label"
|
||||
placeholder="Login with SSO" />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.scopes" label="Scopes"
|
||||
helper="Must include openid. Common scopes are openid email profile groups." />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.clock_skew_seconds" type="number"
|
||||
label="Clock skew (seconds)" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 lg:grid-cols-2">
|
||||
@if ($provider === 'oidc')
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="disable_registration_when_oauth_enabled"
|
||||
label="Disable password registration when OAuth is enabled"
|
||||
instantSave="saveRegistrationPolicy" fullWidth />
|
||||
</div>
|
||||
id="oauth_settings_map.{{ $provider }}.allow_registration"
|
||||
label="Allow OIDC user creation"
|
||||
helper="Allow a successful OIDC login to create a user when password registration is disabled." />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.require_email_verified"
|
||||
label="Require verified email" />
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.use_pkce" label="Use PKCE" />
|
||||
@endif
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $provider }}.auto_join_root_team"
|
||||
label="Auto-join new users to Root team"
|
||||
helper="Add newly-created OAuth users to the Root team as members without creating a personal team." />
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@php
|
||||
$oauth_setting = $oauth_settings_map[$selectedProvider] ?? null;
|
||||
@endphp
|
||||
|
||||
@if ($oauth_setting)
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center gap-2 pb-2">
|
||||
<h2>{{ $oauth_setting['label'] }}</h2>
|
||||
@if ($oauth_setting['enabled'])
|
||||
<x-forms.button canGate="update" :canResource="$settings" type="submit">
|
||||
Save
|
||||
</x-forms.button>
|
||||
<x-forms.button canGate="update" :canResource="$settings"
|
||||
wire:click="toggleProvider('{{ $oauth_setting['provider'] }}')">
|
||||
Disable {{ $oauth_setting['label'] }}
|
||||
</x-forms.button>
|
||||
@else
|
||||
<x-forms.button canGate="update" :canResource="$settings" isHighlighted
|
||||
wire:click="toggleProvider('{{ $oauth_setting['provider'] }}')">
|
||||
Enable {{ $oauth_setting['label'] }}
|
||||
</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="pb-4">OAuth configuration for {{ $oauth_setting['label'] }}.</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 pt-4">
|
||||
<div>
|
||||
<div class="flex flex-col w-full gap-2 xl:flex-row">
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.client_id"
|
||||
label="Client ID" />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.client_secret"
|
||||
type="password" label="Client Secret" autocomplete="new-password" />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.redirect_uri"
|
||||
placeholder="{{ route('auth.callback', $oauth_setting['provider']) }}"
|
||||
label="Redirect URI" />
|
||||
@if ($oauth_setting['provider'] == 'azure')
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.tenant"
|
||||
label="Tenant" />
|
||||
@endif
|
||||
@if ($oauth_setting['provider'] == 'google')
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.tenant"
|
||||
helper="Optional parameter that supplies a hosted domain (HD) to Google, which<br>triggers a login hint to be displayed on the OAuth screen with this domain.<br><br><a class='underline dark:text-warning text-coollabs' href='https://developers.google.com/identity/openid-connect/openid-connect#hd-param' target='_blank'>Google Documentation</a>"
|
||||
label="Tenant" />
|
||||
@endif
|
||||
@if (
|
||||
$oauth_setting['provider'] == 'authentik' ||
|
||||
$oauth_setting['provider'] == 'clerk' ||
|
||||
$oauth_setting['provider'] == 'zitadel' ||
|
||||
$oauth_setting['provider'] == 'gitlab')
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.base_url"
|
||||
label="Base URL" />
|
||||
@endif
|
||||
@if ($oauth_setting['provider'] == 'oidc')
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.base_url"
|
||||
label="Issuer URL"
|
||||
helper="OpenID Provider issuer URL, for example https://example.okta.com. Coolify uses this URL to discover authorization, token, userinfo, and JWKS endpoints." />
|
||||
@endif
|
||||
</div>
|
||||
@if ($oauth_setting['provider'] == 'oidc')
|
||||
<div class="flex flex-col w-full gap-2 pt-2 xl:flex-row">
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.custom_label"
|
||||
label="Login Button Label" placeholder="Login with SSO" />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.scopes"
|
||||
label="Scopes"
|
||||
helper="Must include openid. Common Okta scopes: openid email profile groups." />
|
||||
<x-forms.input canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.clock_skew_seconds"
|
||||
type="number" label="Clock Skew (seconds)" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 pt-2">
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.allow_registration"
|
||||
label="Allow OIDC user creation"
|
||||
helper="When enabled, a successful OIDC login can create a Coolify user even if normal password registration is disabled." />
|
||||
</div>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.require_email_verified"
|
||||
label="Require verified email" />
|
||||
</div>
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.use_pkce"
|
||||
label="Use PKCE" />
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-col gap-2 pt-2">
|
||||
<div class="md:w-96">
|
||||
<x-forms.checkbox canGate="update" :canResource="$settings"
|
||||
id="oauth_settings_map.{{ $oauth_setting['provider'] }}.auto_join_root_team"
|
||||
label="Auto-join new users to Root team"
|
||||
helper="When enabled, newly-created OAuth users are added to the Root team as members and no personal team is created for them." />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</x-application.settings-section>
|
||||
@endforeach
|
||||
</form>
|
||||
</div>
|
||||
</x-settings.layout>
|
||||
</div>
|
||||
|
||||
@@ -36,31 +36,33 @@ beforeEach(function () {
|
||||
OauthSetting::create(['provider' => 'bitbucket']);
|
||||
});
|
||||
|
||||
it('shows oauth general settings with provider subnavigation', function () {
|
||||
it('uses the standard settings design and keeps every oauth provider on one page', function () {
|
||||
actingAsInstanceAdmin();
|
||||
|
||||
$this->withoutMiddleware(DecideWhatToDoWithUser::class)
|
||||
->get(route('settings.oauth'))
|
||||
->assertSuccessful()
|
||||
->assertSee('General')
|
||||
->assertSee('Authentication')
|
||||
->assertSee('Registration')
|
||||
->assertSee('Authentik')
|
||||
->assertSee('Bitbucket')
|
||||
->assertSee(route('settings.oauth.provider', 'authentik'), false)
|
||||
->assertSee(route('settings.oauth.provider', 'bitbucket'), false)
|
||||
->assertSee('OpenID Connect')
|
||||
->assertSee('Disable password registration when OAuth is enabled')
|
||||
->assertDontSee('Client Secret');
|
||||
->assertSee('Client secret')
|
||||
->assertSee('application-settings-form', false)
|
||||
->assertDontSee(route('settings.oauth.provider', 'authentik'), false);
|
||||
});
|
||||
|
||||
it('shows the registration helper next to the section title in a wider row', function () {
|
||||
it('lists openid connect before the other oauth providers', function () {
|
||||
actingAsInstanceAdmin();
|
||||
|
||||
$this->withoutMiddleware(DecideWhatToDoWithUser::class)
|
||||
->get(route('settings.oauth'))
|
||||
->assertSuccessful()
|
||||
->assertSee('flex items-center gap-2', false)
|
||||
->assertSee('max-w-2xl', false)
|
||||
->assertSee('Disable password registration when OAuth is enabled')
|
||||
->assertDontSee('md:w-96', false);
|
||||
$providers = array_keys(Livewire::test(SettingsOauth::class)->get('oauth_settings_map'));
|
||||
|
||||
expect($providers[0])->toBe('oidc');
|
||||
});
|
||||
|
||||
it('has an icon for openid connect', function () {
|
||||
expect(public_path('svgs/oidc.svg'))->toBeFile();
|
||||
});
|
||||
|
||||
it('auto saves registration policy without a general save button', function () {
|
||||
@@ -81,24 +83,24 @@ it('auto saves registration policy without a general save button', function () {
|
||||
expect(instanceSettings()->fresh()->disable_registration_when_oauth_enabled)->toBeTrue();
|
||||
});
|
||||
|
||||
it('shows a provider settings page with a naked okta issuer url example', function () {
|
||||
it('shows oidc fields with a naked okta issuer url example', function () {
|
||||
actingAsInstanceAdmin();
|
||||
|
||||
$this->withoutMiddleware(DecideWhatToDoWithUser::class)
|
||||
->get(route('settings.oauth.provider', 'oidc'))
|
||||
->get(route('settings.oauth'))
|
||||
->assertSuccessful()
|
||||
->assertSee('OpenID Connect')
|
||||
->assertSee('https://example.okta.com', false)
|
||||
->assertDontSee('/oauth2/default', false);
|
||||
});
|
||||
|
||||
it('shows provider enable controls as actions without boxed sections', function () {
|
||||
it('shows provider enable controls as settings section actions', function () {
|
||||
actingAsInstanceAdmin();
|
||||
|
||||
$this->withoutMiddleware(DecideWhatToDoWithUser::class)
|
||||
->get(route('settings.oauth.provider', 'authentik'))
|
||||
->get(route('settings.oauth'))
|
||||
->assertSuccessful()
|
||||
->assertSee('Enable Authentik')
|
||||
->assertSee('Enable')
|
||||
->assertDontSee('label="Enabled"', false)
|
||||
->assertDontSee('p-4 border dark:border-coolgray-300 border-neutral-200', false);
|
||||
});
|
||||
@@ -107,7 +109,7 @@ it('stacks oidc option checkboxes vertically', function () {
|
||||
actingAsInstanceAdmin();
|
||||
|
||||
$this->withoutMiddleware(DecideWhatToDoWithUser::class)
|
||||
->get(route('settings.oauth.provider', 'oidc'))
|
||||
->get(route('settings.oauth'))
|
||||
->assertSuccessful()
|
||||
->assertSee('Allow OIDC user creation')
|
||||
->assertSee('Require verified email')
|
||||
|
||||
Reference in New Issue
Block a user