From fc32bc6b6c9d260d44ea3e623475c255233fc0f3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:13:39 +0200 Subject: [PATCH] feat(auth): auto-join OAuth users to root team Add an OAuth setting that provisions newly registered users as Root team members without creating a personal team. --- app/Livewire/SettingsOauth.php | 3 ++ app/Models/OauthSetting.php | 3 +- app/Services/Auth/OauthLoginService.php | 30 ++++++++++++++-- ...join_root_team_to_oauth_settings_table.php | 28 +++++++++++++++ .../views/livewire/settings-oauth.blade.php | 8 +++++ tests/Feature/OidcOauthControllerTest.php | 34 +++++++++++++++++++ tests/Feature/SettingsOauthTest.php | 10 ++++-- 7 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 database/migrations/2026_06_23_151229_add_auto_join_root_team_to_oauth_settings_table.php diff --git a/app/Livewire/SettingsOauth.php b/app/Livewire/SettingsOauth.php index d038d9a95..a3ee2c987 100644 --- a/app/Livewire/SettingsOauth.php +++ b/app/Livewire/SettingsOauth.php @@ -42,6 +42,7 @@ private function validationRules(?string $provider = null): array $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'; @@ -148,6 +149,7 @@ private function fillOauthSetting(OauthSetting $oauth, array $data): void '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), @@ -196,6 +198,7 @@ private function oauthSettingToArray(OauthSetting $setting): array '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, diff --git a/app/Models/OauthSetting.php b/app/Models/OauthSetting.php index 25e38b4d1..bc6fc06c1 100644 --- a/app/Models/OauthSetting.php +++ b/app/Models/OauthSetting.php @@ -11,13 +11,14 @@ class OauthSetting extends Model { use HasFactory; - protected $fillable = ['provider', 'client_id', 'client_secret', 'redirect_uri', 'tenant', 'base_url', 'enabled', 'custom_label', 'scopes', 'allow_registration', 'require_email_verified', 'use_pkce', 'clock_skew_seconds']; + protected $fillable = ['provider', 'client_id', 'client_secret', 'redirect_uri', 'tenant', 'base_url', 'enabled', 'custom_label', 'scopes', 'allow_registration', 'auto_join_root_team', 'require_email_verified', 'use_pkce', 'clock_skew_seconds']; protected function casts(): array { return [ 'enabled' => 'boolean', 'allow_registration' => 'boolean', + 'auto_join_root_team' => 'boolean', 'require_email_verified' => 'boolean', 'use_pkce' => 'boolean', 'clock_skew_seconds' => 'integer', diff --git a/app/Services/Auth/OauthLoginService.php b/app/Services/Auth/OauthLoginService.php index f1c3b4d82..f656fad08 100644 --- a/app/Services/Auth/OauthLoginService.php +++ b/app/Services/Auth/OauthLoginService.php @@ -44,7 +44,7 @@ private function resolveOauthUser(object $oauthUser, OauthSetting $oauthSetting, throw new HttpException(403, 'Registration is disabled'); } - return $this->createUser($oauthUser->name ?: $email, $email); + return $this->createUser($oauthUser->name ?: $email, $email, $oauthSetting); } private function resolveOidcUser(object $oauthUser, OauthSetting $oauthSetting, string $email): User @@ -100,7 +100,7 @@ private function resolveOidcUser(object $oauthUser, OauthSetting $oauthSetting, throw new HttpException(403, 'Registration is disabled'); } - $user = $this->createUser($oauthUser->name ?: $email, $email); + $user = $this->createUser($oauthUser->name ?: $email, $email, $oauthSetting); } OauthIdentity::create([ @@ -122,7 +122,7 @@ private function canCreateUser(OauthSetting $oauthSetting): bool return instanceSettings()->is_registration_enabled || $oauthSetting->allow_registration; } - private function createUser(string $name, string $email): User + private function createUser(string $name, string $email, OauthSetting $oauthSetting): User { if (User::count() === 0) { $user = (new User)->forceFill([ @@ -143,10 +143,34 @@ private function createUser(string $name, string $email): User return $user; } + if ($oauthSetting->auto_join_root_team) { + return $this->createRootTeamOnlyUser($name, $email); + } + return User::create([ 'name' => $name, 'email' => $email, 'password' => Hash::make(Str::random(64)), ]); } + + private function createRootTeamOnlyUser(string $name, string $email): User + { + return DB::transaction(function () use ($name, $email) { + $rootTeam = Team::find(0); + if ($rootTeam === null) { + throw new HttpException(403, 'Root team is not available for OAuth user provisioning'); + } + + $user = User::withoutEvents(fn () => User::create([ + 'name' => $name, + 'email' => $email, + 'password' => Hash::make(Str::random(64)), + ])); + + $user->teams()->attach($rootTeam, ['role' => 'member']); + + return $user; + }); + } } diff --git a/database/migrations/2026_06_23_151229_add_auto_join_root_team_to_oauth_settings_table.php b/database/migrations/2026_06_23_151229_add_auto_join_root_team_to_oauth_settings_table.php new file mode 100644 index 000000000..b0f5aad18 --- /dev/null +++ b/database/migrations/2026_06_23_151229_add_auto_join_root_team_to_oauth_settings_table.php @@ -0,0 +1,28 @@ +boolean('auto_join_root_team')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('oauth_settings', function (Blueprint $table) { + $table->dropColumn('auto_join_root_team'); + }); + } +}; diff --git a/resources/views/livewire/settings-oauth.blade.php b/resources/views/livewire/settings-oauth.blade.php index 8308e88d0..1ff75512e 100644 --- a/resources/views/livewire/settings-oauth.blade.php +++ b/resources/views/livewire/settings-oauth.blade.php @@ -136,6 +136,14 @@ class="menu-item-label">{{ $oauth_setting['label'] }} @endif +