mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
Validate that the OAuth redirect URI path matches the auth.callback route for the provider. Any other path falls through to the catch-all route and silently sends the user back to the login page, so reject it with a clear validation error instead.
357 lines
13 KiB
PHP
357 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire;
|
|
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\OauthSetting;
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Livewire\Component;
|
|
use Livewire\Features\SupportRedirects\Redirector;
|
|
|
|
class SettingsOauth extends Component
|
|
{
|
|
use AuthorizesRequests;
|
|
|
|
public InstanceSettings $settings;
|
|
|
|
public $oauth_settings_map;
|
|
|
|
public ?string $selectedProvider = null;
|
|
|
|
public bool $disable_registration_when_oauth_enabled = false;
|
|
|
|
protected function rules(): array
|
|
{
|
|
return $this->validationRules();
|
|
}
|
|
|
|
private function validationRules(?string $provider = null): array
|
|
{
|
|
$rules = OauthSetting::all()->reduce(function ($carry, $setting) use ($provider) {
|
|
if ($provider !== null && $setting->provider !== $provider) {
|
|
return $carry;
|
|
}
|
|
|
|
$carry["oauth_settings_map.$setting->provider.enabled"] = 'required|boolean';
|
|
$carry["oauth_settings_map.$setting->provider.client_id"] = 'nullable|string';
|
|
$carry["oauth_settings_map.$setting->provider.client_secret"] = 'nullable|string';
|
|
$carry["oauth_settings_map.$setting->provider.redirect_uri"] = ['nullable', 'string', 'max:2048', 'url:http,https', $this->callbackPathRule($setting->provider)];
|
|
$carry["oauth_settings_map.$setting->provider.tenant"] = 'nullable|string';
|
|
$carry["oauth_settings_map.$setting->provider.base_url"] = 'nullable|string|max:2048|url:http,https';
|
|
$carry["oauth_settings_map.$setting->provider.custom_label"] = 'nullable|string|max:255';
|
|
$carry["oauth_settings_map.$setting->provider.scopes"] = 'nullable|string|max:1000';
|
|
$carry["oauth_settings_map.$setting->provider.allow_registration"] = 'boolean';
|
|
$carry["oauth_settings_map.$setting->provider.auto_join_root_team"] = 'boolean';
|
|
$carry["oauth_settings_map.$setting->provider.require_email_verified"] = 'boolean';
|
|
$carry["oauth_settings_map.$setting->provider.use_pkce"] = 'boolean';
|
|
$carry["oauth_settings_map.$setting->provider.clock_skew_seconds"] = 'nullable|integer|min:0|max:600';
|
|
|
|
return $carry;
|
|
}, []);
|
|
|
|
if ($provider === null) {
|
|
$rules['disable_registration_when_oauth_enabled'] = 'boolean';
|
|
}
|
|
|
|
return $rules;
|
|
}
|
|
|
|
/**
|
|
* The provider sends the browser back to the redirect URI. Only the callback route can
|
|
* finish the login; any other path falls through to the catch-all route, which sends
|
|
* the user to the login page without an error.
|
|
*/
|
|
private function callbackPathRule(string $provider): \Closure
|
|
{
|
|
return function (string $attribute, mixed $value, \Closure $fail) use ($provider): void {
|
|
$expectedPath = route('auth.callback', $provider, false);
|
|
$path = rtrim((string) parse_url((string) $value, PHP_URL_PATH), '/');
|
|
|
|
if ($path !== $expectedPath) {
|
|
$fail("The redirect URI must end with {$expectedPath}.");
|
|
}
|
|
};
|
|
}
|
|
|
|
public function mount(?string $provider = null): ?Redirector
|
|
{
|
|
if (! isInstanceAdmin()) {
|
|
return redirect()->route('dashboard');
|
|
}
|
|
|
|
$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(fn (OauthSetting $setting): string => $setting->isOidc() ? '' : $setting->provider)
|
|
->reduce(function ($carry, $setting) {
|
|
$carry[$setting->provider] = $this->oauthSettingToArray($setting);
|
|
|
|
return $carry;
|
|
}, []);
|
|
|
|
if ($this->selectedProvider !== null && ! array_key_exists($this->selectedProvider, $this->oauth_settings_map)) {
|
|
abort(404);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function updateOauthSettings(?string $provider = null): void
|
|
{
|
|
$this->validate($this->validationRules($provider));
|
|
|
|
if ($provider) {
|
|
$oauthData = $this->oauth_settings_map[$provider];
|
|
$oauth = OauthSetting::find($oauthData['id']);
|
|
|
|
if (! $oauth) {
|
|
throw new \Exception('OAuth setting for '.$provider.' not found. It may have been deleted.');
|
|
}
|
|
|
|
$this->fillOauthSetting($oauth, $oauthData);
|
|
$this->ensureProviderCanBeEnabled($oauth);
|
|
$oauth->save();
|
|
|
|
$this->auditOauthSettings($oauth, 'updated');
|
|
|
|
$this->oauth_settings_map[$provider] = $this->oauthSettingToArray($oauth);
|
|
|
|
$this->dispatch('success', 'OAuth settings for '.$oauth->provider.' updated successfully!');
|
|
|
|
return;
|
|
}
|
|
|
|
$errors = [];
|
|
foreach (array_values($this->oauth_settings_map) as $settingData) {
|
|
$oauth = OauthSetting::find($settingData['id']);
|
|
|
|
if (! $oauth) {
|
|
$errors[] = "OAuth setting for provider '{$settingData['provider']}' not found. It may have been deleted.";
|
|
|
|
continue;
|
|
}
|
|
|
|
$this->fillOauthSetting($oauth, $settingData);
|
|
|
|
if ($oauth->enabled && ! $oauth->couldBeEnabled()) {
|
|
$oauth->enabled = false;
|
|
$errors[] = "OAuth settings are incomplete for '{$oauth->provider}'. Required fields are missing. The provider has been disabled.";
|
|
}
|
|
|
|
if ($oauth->enabled && $oauth->isOidc() && ! in_array('openid', $oauth->scopeList(), true)) {
|
|
$oauth->enabled = false;
|
|
$errors[] = "OIDC scopes must include 'openid'. The provider has been disabled.";
|
|
}
|
|
|
|
$oauth->save();
|
|
$this->auditOauthSettings($oauth, 'updated');
|
|
$this->oauth_settings_map[$oauth->provider] = $this->oauthSettingToArray($oauth);
|
|
}
|
|
|
|
instanceSettings()->update([
|
|
'disable_registration_when_oauth_enabled' => $this->disable_registration_when_oauth_enabled,
|
|
]);
|
|
auditLog('ui.instance.authentication.updated', [
|
|
'team_id' => null,
|
|
'resource' => 'instance',
|
|
'changed_fields' => ['disable_registration_when_oauth_enabled'],
|
|
]);
|
|
|
|
if (! empty($errors)) {
|
|
$this->dispatch('error', implode('<br/>', $errors));
|
|
}
|
|
}
|
|
|
|
private function fillOauthSetting(OauthSetting $oauth, array $data): void
|
|
{
|
|
$oauth->fill([
|
|
'enabled' => (bool) ($data['enabled'] ?? false),
|
|
'client_id' => $data['client_id'] ?? null,
|
|
'client_secret' => $data['client_secret'] ?? null,
|
|
'redirect_uri' => $this->nullableString($data['redirect_uri'] ?? null),
|
|
'tenant' => $data['tenant'] ?? null,
|
|
'base_url' => $this->nullableString($data['base_url'] ?? null),
|
|
'custom_label' => $data['custom_label'] ?? null,
|
|
'scopes' => $data['scopes'] ?? null,
|
|
'allow_registration' => (bool) ($data['allow_registration'] ?? false),
|
|
'auto_join_root_team' => (bool) ($data['auto_join_root_team'] ?? false),
|
|
'require_email_verified' => (bool) ($data['require_email_verified'] ?? true),
|
|
'use_pkce' => (bool) ($data['use_pkce'] ?? true),
|
|
'clock_skew_seconds' => (int) ($data['clock_skew_seconds'] ?? 60),
|
|
]);
|
|
}
|
|
|
|
private function nullableString(mixed $value): ?string
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
$value = trim((string) $value);
|
|
|
|
return $value === '' ? null : $value;
|
|
}
|
|
|
|
private function ensureProviderCanBeEnabled(OauthSetting $oauth): void
|
|
{
|
|
if (! $oauth->enabled) {
|
|
return;
|
|
}
|
|
|
|
if (! $oauth->couldBeEnabled()) {
|
|
$oauth->update(['enabled' => false]);
|
|
throw new \Exception('OAuth settings are not complete for '.$oauth->provider.'.<br/>Please fill in all required fields.');
|
|
}
|
|
|
|
if ($oauth->isOidc() && ! in_array('openid', $oauth->scopeList(), true)) {
|
|
$oauth->update(['enabled' => false]);
|
|
throw new \Exception("OIDC scopes must include 'openid'.");
|
|
}
|
|
}
|
|
|
|
private function oauthSettingToArray(OauthSetting $setting): array
|
|
{
|
|
return [
|
|
'id' => $setting->id,
|
|
'provider' => $setting->provider,
|
|
'enabled' => $setting->enabled,
|
|
'client_id' => $setting->client_id,
|
|
'client_secret' => $setting->client_secret,
|
|
'redirect_uri' => $setting->redirect_uri,
|
|
'tenant' => $setting->tenant,
|
|
'base_url' => $setting->base_url,
|
|
'custom_label' => $setting->custom_label,
|
|
'scopes' => $setting->scopes ?: 'openid email profile',
|
|
'allow_registration' => $setting->allow_registration,
|
|
'auto_join_root_team' => $setting->auto_join_root_team,
|
|
'require_email_verified' => $setting->require_email_verified ?? true,
|
|
'use_pkce' => $setting->use_pkce ?? true,
|
|
'clock_skew_seconds' => $setting->clock_skew_seconds ?? 60,
|
|
'label' => $this->providerLabel($setting->provider),
|
|
];
|
|
}
|
|
|
|
public function providerLabel(string $provider): string
|
|
{
|
|
return match ($provider) {
|
|
'oidc' => 'OpenID Connect',
|
|
'gitlab' => 'GitLab',
|
|
default => str($provider)->headline()->toString(),
|
|
};
|
|
}
|
|
|
|
public function instantSave(string $provider)
|
|
{
|
|
try {
|
|
$this->authorize('update', instanceSettings());
|
|
$this->updateOauthSettings($provider);
|
|
} catch (\Exception $e) {
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
public function toggleProvider(string $provider)
|
|
{
|
|
try {
|
|
$this->authorize('update', instanceSettings());
|
|
|
|
if (! array_key_exists($provider, $this->oauth_settings_map)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! (bool) $this->oauth_settings_map[$provider]['enabled']) {
|
|
$this->validateProviderCanBeEnabled($provider);
|
|
}
|
|
|
|
$this->oauth_settings_map[$provider]['enabled'] = ! (bool) $this->oauth_settings_map[$provider]['enabled'];
|
|
$this->updateOauthSettings($provider);
|
|
} catch (\Exception $e) {
|
|
$oauth = OauthSetting::where('provider', $provider)->first();
|
|
if ($oauth) {
|
|
$this->oauth_settings_map[$provider] = $this->oauthSettingToArray($oauth);
|
|
}
|
|
|
|
return handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
private function validateProviderCanBeEnabled(string $provider): void
|
|
{
|
|
$this->validate($this->validationRules($provider));
|
|
|
|
$oauth = OauthSetting::find($this->oauth_settings_map[$provider]['id']);
|
|
if (! $oauth) {
|
|
throw new \Exception('OAuth setting for '.$provider.' not found. It may have been deleted.');
|
|
}
|
|
|
|
$this->fillOauthSetting($oauth, [
|
|
...$this->oauth_settings_map[$provider],
|
|
'enabled' => true,
|
|
]);
|
|
|
|
if (! $oauth->couldBeEnabled()) {
|
|
throw new \Exception('OAuth settings are not complete for '.$oauth->provider.'.<br/>Please fill in all required fields.');
|
|
}
|
|
|
|
if ($oauth->isOidc() && ! in_array('openid', $oauth->scopeList(), true)) {
|
|
throw new \Exception("OIDC scopes must include 'openid'.");
|
|
}
|
|
}
|
|
|
|
public function saveRegistrationPolicy(): void
|
|
{
|
|
$this->authorize('update', instanceSettings());
|
|
$this->validate([
|
|
'disable_registration_when_oauth_enabled' => 'boolean',
|
|
]);
|
|
|
|
instanceSettings()->update([
|
|
'disable_registration_when_oauth_enabled' => $this->disable_registration_when_oauth_enabled,
|
|
]);
|
|
auditLog('ui.instance.authentication.updated', [
|
|
'team_id' => null,
|
|
'resource' => 'instance',
|
|
'changed_fields' => ['disable_registration_when_oauth_enabled'],
|
|
]);
|
|
|
|
$this->dispatch('success', 'Authentication settings updated successfully!');
|
|
}
|
|
|
|
public function submit(): void
|
|
{
|
|
try {
|
|
$this->authorize('update', instanceSettings());
|
|
$this->updateOauthSettings($this->selectedProvider);
|
|
|
|
if ($this->selectedProvider === null) {
|
|
$this->dispatch('success', 'Instance settings updated successfully!');
|
|
}
|
|
} catch (ValidationException $e) {
|
|
throw $e;
|
|
} catch (\Exception $e) {
|
|
if ($this->selectedProvider !== null) {
|
|
$oauth = OauthSetting::where('provider', $this->selectedProvider)->first();
|
|
if ($oauth) {
|
|
$this->oauth_settings_map[$this->selectedProvider] = $this->oauthSettingToArray($oauth);
|
|
}
|
|
}
|
|
|
|
handleError($e, $this);
|
|
}
|
|
}
|
|
|
|
private function auditOauthSettings(OauthSetting $oauth, string $action): void
|
|
{
|
|
auditLog("ui.oauth_setting.{$action}", [
|
|
'team_id' => null,
|
|
'resource' => 'oauth_setting',
|
|
'oauth_setting_name' => $oauth->provider,
|
|
'provider' => $oauth->provider,
|
|
'enabled' => $oauth->enabled,
|
|
'changed_fields' => array_values(array_diff(array_keys($oauth->getChanges()), ['client_secret', 'updated_at'])),
|
|
]);
|
|
}
|
|
}
|