mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-24 02:24:11 -05:00
Recover existing OAuth and OIDC identities after unique constraint races and reject boolean or floating-point provider IDs.
192 lines
6.4 KiB
PHP
192 lines
6.4 KiB
PHP
<?php
|
|
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\OauthIdentity;
|
|
use App\Models\OauthSetting;
|
|
use App\Models\User;
|
|
use App\Services\Auth\OauthLoginService;
|
|
use Illuminate\Database\UniqueConstraintViolationException;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Event;
|
|
use Illuminate\Support\Once;
|
|
use Laravel\Socialite\Facades\Socialite;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
InstanceSettings::forceCreate([
|
|
'id' => 0,
|
|
'is_registration_enabled' => false,
|
|
]);
|
|
|
|
Once::flush();
|
|
|
|
OauthSetting::create([
|
|
'provider' => 'google',
|
|
'client_id' => 'client-id',
|
|
'client_secret' => 'client-secret',
|
|
'redirect_uri' => 'https://coolify.example.com/auth/google/callback',
|
|
'tenant' => 'example.com',
|
|
'enabled' => true,
|
|
]);
|
|
});
|
|
|
|
it('logs in an existing user when the oauth provider returns a mixed-case email', function () {
|
|
config()->set('app.maintenance.driver', 'file');
|
|
|
|
$user = User::factory()->create([
|
|
'email' => 'username@example.edu',
|
|
]);
|
|
|
|
$provider = Mockery::mock();
|
|
$provider->shouldReceive('setConfig')->once()->andReturnSelf();
|
|
$provider->shouldReceive('with')->once()->with(['hd' => 'example.com'])->andReturnSelf();
|
|
$provider->shouldReceive('user')->once()->andReturn((object) [
|
|
'email' => 'UserName@example.edu',
|
|
'name' => 'Example User',
|
|
'id' => 'google-user-id',
|
|
]);
|
|
|
|
Socialite::shouldReceive('driver')->once()->with('google')->andReturn($provider);
|
|
|
|
$response = $this->get(route('auth.callback', 'google'));
|
|
|
|
$response->assertRedirect('/');
|
|
$this->assertAuthenticatedAs($user);
|
|
expect(User::count())->toBe(1);
|
|
expect(OauthIdentity::where([
|
|
'user_id' => $user->id,
|
|
'provider' => 'google',
|
|
'provider_user_id' => 'google-user-id',
|
|
])->exists())->toBeTrue();
|
|
});
|
|
|
|
it('never moves an existing oauth identity when the provider email changes', function () {
|
|
config()->set('app.maintenance.driver', 'file');
|
|
|
|
$identityOwner = User::factory()->create(['email' => 'old@example.com']);
|
|
$otherUser = User::factory()->create(['email' => 'new@example.com']);
|
|
$identity = OauthIdentity::create([
|
|
'user_id' => $identityOwner->id,
|
|
'provider' => 'google',
|
|
'issuer' => 'google',
|
|
'provider_user_id' => 'google-user-id',
|
|
'email' => 'old@example.com',
|
|
]);
|
|
|
|
$provider = Mockery::mock();
|
|
$provider->shouldReceive('setConfig')->once()->andReturnSelf();
|
|
$provider->shouldReceive('with')->once()->with(['hd' => 'example.com'])->andReturnSelf();
|
|
$provider->shouldReceive('user')->once()->andReturn((object) [
|
|
'email' => 'new@example.com',
|
|
'name' => 'Example User',
|
|
'id' => 'google-user-id',
|
|
]);
|
|
|
|
Socialite::shouldReceive('driver')->once()->with('google')->andReturn($provider);
|
|
|
|
$this->get(route('auth.callback', 'google'))->assertRedirect('/');
|
|
|
|
$this->assertAuthenticatedAs($identityOwner);
|
|
expect($identity->refresh()->user_id)->toBe($identityOwner->id)
|
|
->and($identity->email)->toBe('new@example.com')
|
|
->and($identity->user_id)->not->toBe($otherUser->id);
|
|
});
|
|
|
|
it('continues oauth login when another request creates the identity first', function () {
|
|
$user = User::factory()->create(['email' => 'race@example.com']);
|
|
$eventName = 'eloquent.creating: '.OauthIdentity::class;
|
|
|
|
Event::listen($eventName, function (OauthIdentity $identity): void {
|
|
$attributes = $identity->getAttributes();
|
|
|
|
DB::afterRollBack(fn () => DB::table('oauth_identities')->insert($attributes));
|
|
|
|
throw new UniqueConstraintViolationException(
|
|
DB::getDefaultConnection(),
|
|
'insert into oauth_identities',
|
|
[],
|
|
new PDOException('duplicate identity'),
|
|
);
|
|
});
|
|
|
|
try {
|
|
$resolvedUser = app(OauthLoginService::class)->login('google', (object) [
|
|
'email' => 'race@example.com',
|
|
'name' => 'Race User',
|
|
'id' => 'google-race-id',
|
|
], OauthSetting::where('provider', 'google')->firstOrFail());
|
|
} finally {
|
|
Event::forget($eventName);
|
|
}
|
|
|
|
expect($resolvedUser->is($user))->toBeTrue()
|
|
->and(OauthIdentity::where('provider_user_id', 'google-race-id')->count())->toBe(1);
|
|
$this->assertAuthenticatedAs($user);
|
|
});
|
|
|
|
it('rejects oauth logins when the provider does not return an email address', function (?string $providerEmail) {
|
|
config()->set('app.maintenance.driver', 'file');
|
|
InstanceSettings::firstOrCreate([
|
|
'id' => 0,
|
|
], [
|
|
'is_registration_enabled' => false,
|
|
])->update([
|
|
'is_registration_enabled' => true,
|
|
]);
|
|
|
|
$provider = Mockery::mock();
|
|
$provider->shouldReceive('setConfig')->once()->andReturnSelf();
|
|
$provider->shouldReceive('with')->once()->with(['hd' => 'example.com'])->andReturnSelf();
|
|
$provider->shouldReceive('user')->once()->andReturn((object) [
|
|
'email' => $providerEmail,
|
|
'name' => 'Example User',
|
|
'id' => 'google-user-id',
|
|
]);
|
|
|
|
Socialite::shouldReceive('driver')->once()->with('google')->andReturn($provider);
|
|
|
|
$response = $this->from('/login')->get(route('auth.callback', 'google'));
|
|
|
|
$response->assertRedirect('/login');
|
|
expect(User::count())->toBe(0);
|
|
})->with([
|
|
'null email' => [null],
|
|
'blank email' => [' '],
|
|
'malformed email' => ['not-an-email'],
|
|
'missing domain' => ['user@'],
|
|
]);
|
|
|
|
it('rejects oauth logins when the provider does not return a valid user id', function (mixed $invalidId) {
|
|
$oauthUser = (object) [
|
|
'email' => 'user@example.edu',
|
|
'name' => 'Example User',
|
|
];
|
|
|
|
if ($invalidId !== 'missing') {
|
|
$oauthUser->id = $invalidId;
|
|
}
|
|
|
|
try {
|
|
app(OauthLoginService::class)->login('google', $oauthUser, OauthSetting::where('provider', 'google')->firstOrFail());
|
|
} catch (HttpException $exception) {
|
|
expect($exception->getStatusCode())->toBe(403)
|
|
->and(OauthIdentity::count())->toBe(0)
|
|
->and(User::count())->toBe(0);
|
|
|
|
return;
|
|
}
|
|
|
|
$this->fail('Expected an invalid OAuth provider user ID to be rejected.');
|
|
})->with([
|
|
'null id' => [null],
|
|
'missing id' => ['missing'],
|
|
'blank id' => [' '],
|
|
'non-scalar id' => [[]],
|
|
'true id' => [true],
|
|
'false id' => [false],
|
|
'float id' => [1.0],
|
|
]);
|