Files
coolify/app/Services/Auth/OauthLoginService.php
T
2026-08-21 12:15:46 +02:00

229 lines
8.4 KiB
PHP

<?php
namespace App\Services\Auth;
use App\Auth\Oidc\OidcUser;
use App\Models\OauthIdentity;
use App\Models\OauthSetting;
use App\Models\Team;
use App\Models\User;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\HttpException;
class OauthLoginService
{
public function login(string $provider, object $oauthUser, OauthSetting $oauthSetting): User
{
$email = strtolower(trim((string) $oauthUser->email));
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new HttpException(403, 'OAuth provider did not return a valid email address');
}
$user = $provider === 'oidc'
? $this->resolveOidcUser($oauthUser, $oauthSetting, $email)
: $this->resolveOauthUser($oauthUser, $oauthSetting, $email);
Auth::login($user);
$team = $user->currentTeam() ?? $user->teams()->first() ?? $user->recreate_personal_team();
session(['currentTeam' => $user->currentTeam = $team]);
return $user;
}
private function resolveOauthUser(object $oauthUser, OauthSetting $oauthSetting, string $email): User
{
$provider = $oauthSetting->provider;
$providerUserId = $oauthUser->id ?? null;
if (
(! is_string($providerUserId) && ! is_int($providerUserId))
|| (is_string($providerUserId) && trim($providerUserId) === '')
) {
throw new HttpException(403, 'OAuth provider did not return a valid user ID');
}
$providerUserId = (string) $providerUserId;
$rawClaims = is_array($oauthUser->user ?? null) ? $oauthUser->user : [];
$identityKey = [
'provider' => $provider,
'issuer' => $provider,
'provider_user_id' => $providerUserId,
];
try {
return DB::transaction(function () use ($oauthUser, $oauthSetting, $email, $provider, $providerUserId, $rawClaims, $identityKey): User {
$identity = OauthIdentity::where($identityKey)->first();
if ($identity) {
$identity->update([
'email' => $email,
'raw_claims' => $rawClaims,
'last_login_at' => now(),
]);
return $identity->user;
}
$user = User::whereEmail($email)->first();
if (! $user) {
if (! $this->canCreateUser($oauthSetting)) {
throw new HttpException(403, 'Registration is disabled');
}
$user = $this->createUser($oauthUser->name ?: $email, $email, $oauthSetting);
}
OauthIdentity::create([
'user_id' => $user->id,
'provider' => $provider,
'issuer' => $provider,
'provider_user_id' => $providerUserId,
'email' => $email,
'raw_claims' => $rawClaims,
'last_login_at' => now(),
]);
return $user;
});
} catch (UniqueConstraintViolationException $exception) {
return OauthIdentity::where($identityKey)->first()?->user ?? throw $exception;
}
}
private function resolveOidcUser(object $oauthUser, OauthSetting $oauthSetting, string $email): User
{
$issuer = $oauthUser instanceof OidcUser && filled($oauthUser->issuer)
? $oauthUser->issuer
: data_get($oauthUser->user, 'iss');
$subject = $oauthUser instanceof OidcUser && filled($oauthUser->subject)
? $oauthUser->subject
: data_get($oauthUser->user, 'sub', $oauthUser->id);
$emailVerified = ($oauthUser instanceof OidcUser && $oauthUser->emailVerified)
|| data_get($oauthUser->user, 'email_verified') === true;
if (! is_string($issuer) || $issuer === '' || ! is_string($subject) || $subject === '') {
throw new HttpException(403, 'OIDC provider did not return issuer and subject claims');
}
if ($oauthSetting->require_email_verified && ! $emailVerified) {
throw new HttpException(403, 'OIDC provider did not verify the email address');
}
$rawClaims = is_array($oauthUser->user ?? null) ? $oauthUser->user : [];
$identityKey = [
'provider' => 'oidc',
'issuer' => $issuer,
'provider_user_id' => $subject,
];
try {
return DB::transaction(function () use ($oauthUser, $oauthSetting, $email, $issuer, $subject, $emailVerified, $rawClaims, $identityKey): User {
$identity = OauthIdentity::where($identityKey)->first();
if ($identity) {
$identity->update([
'email' => $email,
'raw_claims' => $rawClaims,
'last_login_at' => now(),
]);
return $identity->user;
}
$user = User::whereEmail($email)->first();
// Linking a new OIDC identity to an existing local account by email
// is account takeover unless the provider attests the email. This
// guard is independent of the require_email_verified toggle, which
// only governs the broader login flow.
if ($user && ! $emailVerified) {
throw new HttpException(403, 'OIDC provider must verify the email address before linking to an existing account');
}
if (! $user) {
if (! $this->canCreateUser($oauthSetting)) {
throw new HttpException(403, 'Registration is disabled');
}
$user = $this->createUser($oauthUser->name ?: $email, $email, $oauthSetting);
}
OauthIdentity::create([
'user_id' => $user->id,
'provider' => 'oidc',
'issuer' => $issuer,
'provider_user_id' => $subject,
'email' => $email,
'raw_claims' => $rawClaims,
'last_login_at' => now(),
]);
return $user;
});
} catch (UniqueConstraintViolationException $exception) {
return OauthIdentity::where($identityKey)->first()?->user ?? throw $exception;
}
}
private function canCreateUser(OauthSetting $oauthSetting): bool
{
return instanceSettings()->is_registration_enabled || $oauthSetting->allow_registration;
}
private function createUser(string $name, string $email, OauthSetting $oauthSetting): User
{
if (User::count() === 0) {
$user = (new User)->forceFill([
'id' => 0,
'name' => $name,
'email' => $email,
'password' => Hash::make(Str::random(64)),
]);
$user->save();
$team = $user->teams()->first() ?? Team::find(0);
if ($team !== null && ! $user->teams()->where('team_id', $team->id)->exists()) {
$user->teams()->attach($team, ['role' => 'owner']);
}
instanceSettings()->update(['is_registration_enabled' => false]);
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;
});
}
}