mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 02:24:11 -05:00
229 lines
8.4 KiB
PHP
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;
|
|
});
|
|
}
|
|
}
|