Skip to content

Commit b33bd07

Browse files
committed
avoid matching by emails when provider cannot be trusted to verify emails
1 parent c56c1c3 commit b33bd07

4 files changed

Lines changed: 140 additions & 5 deletions

File tree

src/Http/Controllers/OAuthController.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Statamic\Exceptions\NotFoundHttpException;
1010
use Statamic\Facades\OAuth;
1111
use Statamic\Facades\URL;
12+
use Statamic\Facades\User;
1213
use Statamic\Support\Arr;
1314
use Statamic\Support\Str;
1415

@@ -50,7 +51,7 @@ public function handleProviderCallback(Request $request, string $provider)
5051
if (config('statamic.oauth.merge_user_data', true)) {
5152
$user = $oauth->mergeUser($user, $providerUser);
5253
}
53-
} elseif (config('statamic.oauth.create_user', true)) {
54+
} elseif (config('statamic.oauth.create_user', true) && ! User::findByEmail($providerUser->getEmail())) {
5455
$user = $oauth->createUser($providerUser);
5556
}
5657

src/OAuth/Provider.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,24 @@ public function findOrCreateUser($socialite): StatamicUser
6262
*/
6363
public function findUser($socialite): ?StatamicUser
6464
{
65-
if (
66-
($user = User::findByOAuthId($this, $socialite->getId())) ||
67-
($user = User::findByEmail($socialite->getEmail()))
68-
) {
65+
if ($user = User::findByOAuthId($this, $socialite->getId())) {
66+
return $user;
67+
}
68+
69+
if ($this->trustsEmails() && $user = User::findByEmail($socialite->getEmail())) {
6970
return $user;
7071
}
7172

7273
return null;
7374
}
7475

76+
private function trustsEmails(): bool
77+
{
78+
return in_array($this->name, config('statamic.oauth.trusted_providers', [
79+
'google', 'github', 'apple', 'bitbucket', 'slack', 'slack-openid', 'twitter-oauth-2',
80+
]));
81+
}
82+
7583
/**
7684
* Create a Statamic user from a Socialite user.
7785
*

tests/OAuth/OAuthCallbackTest.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace Tests\OAuth;
4+
5+
use Mockery;
6+
use PHPUnit\Framework\Attributes\Test;
7+
use Statamic\Facades\OAuth;
8+
use Statamic\Facades\User as UserFacade;
9+
use Statamic\OAuth\Provider;
10+
use Tests\PreventSavingStacheItemsToDisk;
11+
use Tests\TestCase;
12+
13+
class OAuthCallbackTest extends TestCase
14+
{
15+
use PreventSavingStacheItemsToDisk;
16+
17+
protected function defineEnvironment($app)
18+
{
19+
$app['config']->set('statamic.oauth.enabled', true);
20+
$app['config']->set('statamic.oauth.providers', ['evil', 'google']);
21+
}
22+
23+
public function tearDown(): void
24+
{
25+
app('files')->deleteDirectory(storage_path('statamic/oauth'));
26+
27+
parent::tearDown();
28+
}
29+
30+
private function fakeProvider(string $name)
31+
{
32+
$socialiteUser = new FakeSocialiteUser();
33+
34+
$provider = Mockery::mock(Provider::class.'[getSocialiteUser]', [$name, []]);
35+
$provider->shouldReceive('getSocialiteUser')->andReturn($socialiteUser);
36+
37+
OAuth::partialMock()->shouldReceive('provider')->with($name)->andReturn($provider);
38+
}
39+
40+
#[Test]
41+
public function an_untrusted_provider_does_not_overwrite_or_log_into_an_existing_account()
42+
{
43+
config(['statamic.oauth.trusted_providers' => ['google']]); // 'evil' is untrusted
44+
config(['statamic.oauth.create_user' => true]);
45+
46+
$admin = UserFacade::make()->email('admin@target.tld')->data(['name' => 'Admin'])->makeSuper()->save();
47+
48+
$this->fakeProvider('evil');
49+
50+
$response = $this->get('/oauth/evil/callback');
51+
52+
// No login happened, and they were sent to the unauthorized redirect.
53+
$this->assertGuest();
54+
$response->assertRedirect();
55+
56+
// The existing super admin is untouched and no duplicate was created.
57+
$this->assertCount(1, UserFacade::all());
58+
$admin = $admin->fresh();
59+
$this->assertTrue($admin->isSuper());
60+
$this->assertEquals('Admin', $admin->name());
61+
}
62+
63+
#[Test]
64+
public function a_trusted_provider_logs_into_the_existing_account()
65+
{
66+
config(['statamic.oauth.trusted_providers' => ['google']]);
67+
68+
$admin = UserFacade::make()->email('admin@target.tld')->data(['name' => 'Admin'])->makeSuper()->save();
69+
70+
$this->fakeProvider('google');
71+
72+
$this->get('/oauth/google/callback');
73+
74+
$this->assertAuthenticatedAs($admin->fresh());
75+
$this->assertCount(1, UserFacade::all());
76+
}
77+
}
78+
79+
class FakeSocialiteUser
80+
{
81+
public function getId()
82+
{
83+
return 'attacker-1';
84+
}
85+
86+
public function getName()
87+
{
88+
return 'Mallory';
89+
}
90+
91+
public function getEmail()
92+
{
93+
return 'admin@target.tld';
94+
}
95+
}

tests/OAuth/ProviderTest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public function setUp(): void
2222
'driver' => 'local',
2323
'root' => $this->tempDir = __DIR__.'/tmp',
2424
]]);
25+
26+
config(['statamic.oauth.trusted_providers' => ['test']]);
2527
}
2628

2729
public function tearDown(): void
@@ -155,6 +157,35 @@ public function it_does_not_find_or_create_a_user_via_find_user_method()
155157
$this->assertNull($user);
156158
}
157159

160+
#[Test]
161+
public function it_does_not_find_an_existing_user_by_email_for_an_untrusted_provider()
162+
{
163+
config(['statamic.oauth.trusted_providers' => ['google']]);
164+
165+
$this->user()->save();
166+
167+
$foundUser = (new Provider('test'))->findUser($this->socialite());
168+
169+
$this->assertNull($foundUser);
170+
}
171+
172+
#[Test]
173+
public function it_still_finds_an_existing_user_by_oauth_id_for_an_untrusted_provider()
174+
{
175+
config(['statamic.oauth.trusted_providers' => ['google']]);
176+
177+
$provider = new Provider('test');
178+
$savedUser = $this->user()->save();
179+
180+
// Link the account to the provider, then ensure a subsequent lookup matches
181+
// on the stored OAuth id even though email matching is not trusted.
182+
$provider->mergeUser($savedUser, $this->socialite());
183+
184+
$foundUser = $provider->findUser($this->socialite());
185+
186+
$this->assertEquals($savedUser, $foundUser);
187+
}
188+
158189
#[Test]
159190
public function it_finds_an_existing_user_via_find_or_create_user_method()
160191
{

0 commit comments

Comments
 (0)