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], ]);