1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace Tests\Models\OAuth;
7
8use App\Models\OAuth\Client;
9use App\Models\OAuth\Token;
10use App\Models\User;
11use Laravel\Passport\AuthCode;
12use Laravel\Passport\RefreshToken;
13use Tests\TestCase;
14
15class ClientTest extends TestCase
16{
17 /** @var Client */
18 protected $client;
19
20 /** @var User */
21 protected $owner;
22
23 public function testScopesFromTokensAreAggregated()
24 {
25 $user = User::factory()->create();
26 $this->client->tokens()->create([
27 'id' => '1',
28 'revoked' => false,
29 'scopes' => ['identify'],
30 'user_id' => $user->getKey(),
31 ]);
32
33 $this->client->tokens()->create([
34 'id' => '2',
35 'revoked' => false,
36 'scopes' => ['friends.read'],
37 'user_id' => $user->getKey(),
38 ]);
39
40 $clients = Client::forUser($user);
41 $this->assertCount(1, $clients);
42 $this->assertSame(['friends.read', 'identify'], $clients[0]->scopes);
43 }
44
45 public function testScopesFromDifferentClientsAreNotAggregated()
46 {
47 $user = User::factory()->create();
48 $this->client->tokens()->create([
49 'id' => '1',
50 'revoked' => false,
51 'scopes' => ['identify'],
52 'user_id' => $user->getKey(),
53 ]);
54
55 $otherClient = Client::factory()->create(['user_id' => $this->owner]);
56 $otherClient->tokens()->create([
57 'id' => '2',
58 'revoked' => false,
59 'scopes' => ['friends.read'],
60 'user_id' => $user->getKey(),
61 ]);
62
63 $clients = Client::forUser($user);
64 $this->assertSame(['identify'], $clients->find($this->client->getKey())->scopes);
65 $this->assertSame(['friends.read'], $clients->find($otherClient->getKey())->scopes);
66 }
67
68 public function testScopesFromRevokedTokensAreNotAggregated()
69 {
70 $user = User::factory()->create();
71 $this->client->tokens()->create([
72 'id' => '1',
73 'revoked' => false,
74 'scopes' => ['identify'],
75 'user_id' => $user->getKey(),
76 ]);
77
78 $this->client->tokens()->create([
79 'id' => '2',
80 'revoked' => true,
81 'scopes' => ['friends.read'],
82 'user_id' => $user->getKey(),
83 ]);
84
85 $clients = Client::forUser($user);
86 $this->assertCount(1, $clients);
87 $this->assertSame(['identify'], $clients[0]->scopes);
88 }
89
90 public function testClientWithOnlyRevokedTokensDoesNotShow()
91 {
92 $user = User::factory()->create();
93 $this->client->tokens()->create([
94 'id' => '1',
95 'revoked' => true,
96 'scopes' => ['identify'],
97 'user_id' => $user->getKey(),
98 ]);
99
100 $clients = Client::forUser($user);
101 $this->assertEmpty($clients);
102 }
103
104 public function testRevokingClientRemovesItFromTheList()
105 {
106 $user = User::factory()->create();
107 $this->client->tokens()->create([
108 'id' => '1',
109 'revoked' => false,
110 'scopes' => ['identify'],
111 'user_id' => $user->getKey(),
112 ]);
113
114 $this->assertCount(1, Client::forUser($user));
115 $this->client->revokeForUser($user);
116 $this->assertCount(0, Client::forUser($user));
117 }
118
119 public function testDoesNotAggregateScopesFromOtherUsers()
120 {
121 $user1 = User::factory()->create();
122 $user2 = User::factory()->create();
123 $this->client->tokens()->create([
124 'id' => '1',
125 'revoked' => false,
126 'scopes' => ['identify'],
127 'user_id' => $user1->getKey(),
128 ]);
129
130 $this->client->tokens()->create([
131 'id' => '2',
132 'revoked' => false,
133 'scopes' => ['friends.read'],
134 'user_id' => $user2->getKey(),
135 ]);
136
137 $clients = Client::forUser($user1);
138 $this->assertCount(1, $clients);
139 $this->assertSame(['identify'], $clients[0]->scopes);
140 }
141
142 public function testDoesNotRevokeOtherUserTokens()
143 {
144 $user1 = User::factory()->create();
145 $user2 = User::factory()->create();
146 $this->client->tokens()->create([
147 'id' => '1',
148 'revoked' => false,
149 'scopes' => ['identify'],
150 'user_id' => $user1->getKey(),
151 ]);
152
153 $this->client->tokens()->create([
154 'id' => '2',
155 'revoked' => false,
156 'scopes' => ['identify'],
157 'user_id' => $user2->getKey(),
158 ]);
159
160 $this->assertCount(1, Client::forUser($user1));
161 $this->assertCount(1, Client::forUser($user2));
162 $this->client->revokeForUser($user1);
163 $this->assertCount(0, Client::forUser($user1));
164 $this->assertCount(1, Client::forUser($user2));
165 }
166
167 public function testNumberOfClientsIsLimited()
168 {
169 config_set('osu.oauth.max_user_clients', 1);
170
171 $client = Client::factory()
172 ->allowUnsaved()
173 ->create(['user_id' => $this->owner]);
174 $this->assertFalse($client->exists);
175 $this->assertArrayHasKey('user.oauthClients.count', $client->validationErrors()->all());
176 }
177
178 public function testNumberOfClientsLimitDoesNotIncludeRevokedClients()
179 {
180 config_set('osu.oauth.max_user_clients', 1);
181 $this->client->update(['revoked' => true]);
182
183 $client = Client::factory()->create(['user_id' => $this->owner]);
184 $this->assertTrue($client->exists);
185 $this->assertEmpty($client->validationErrors()->all());
186 }
187
188 public function testRevokingClientSkipsValidation()
189 {
190 $client = Client::factory()->make(['user_id' => $this->owner]);
191 $client->save(['skipValidations' => true]);
192 $this->assertTrue($client->exists);
193 $client->revoke();
194 $this->assertTrue($client->fresh()->revoked);
195 }
196
197 public function testResetSecretChangesClientSecret()
198 {
199 $oldSecret = $this->client->secret;
200
201 $this->client->resetSecret();
202
203 $this->assertNotSame($oldSecret, $this->client->secret);
204 }
205
206 public function testResetSecretInvalidatesExistingTokens()
207 {
208 $user = User::factory()->create();
209 $token = $this->client->tokens()->create([
210 'id' => '1',
211 'revoked' => false,
212 'scopes' => ['identify'],
213 'user_id' => $user->getKey(),
214 ]);
215
216 $token->refreshToken()->create([
217 'id' => '1',
218 'revoked' => false,
219 ]);
220
221 $this->client->authCodes()->create([
222 'id' => '1',
223 'revoked' => false,
224 'scopes' => json_encode(['identify']),
225 'user_id' => $user->getKey(),
226 ]);
227
228 // assert no revoked tokens;
229 $this->assertSame(1, Token::where('revoked', false)->count());
230 $this->assertSame(1, RefreshToken::where('revoked', false)->count());
231 $this->assertSame(1, AuthCode::where('revoked', false)->count());
232
233 $this->client->resetSecret();
234
235 // assert no unrevoked tokens;
236 $this->assertSame(0, Token::where('revoked', false)->count());
237 $this->assertSame(0, RefreshToken::where('revoked', false)->count());
238 $this->assertSame(0, AuthCode::where('revoked', false)->count());
239 }
240
241 public function testResetSecretPreventsAccessWithExistingToken()
242 {
243 $user = User::factory()->create();
244 $token = $this->client->tokens()->create([
245 'id' => '1',
246 'revoked' => false,
247 'scopes' => ['identify'],
248 'user_id' => $user->getKey(),
249 ]);
250
251 $this->client->resetSecret();
252 $token->refresh();
253 $this->actAsUserWithToken($token);
254
255 $this->get(route('api.me'))->assertUnauthorized();
256 }
257
258 public function testPassportCreateClientCommand()
259 {
260 $countBefore = Client::count();
261
262 $this->artisan('passport:client', ['--password' => true])
263 ->expectsQuestion('What should we name the password grant client?', 'potato')
264 ->expectsQuestion('Which user provider should this client use to retrieve users?', 'user');
265
266 $this->assertSame($countBefore + 1, Client::count(), 'client was not created.');
267 }
268
269 protected function setUp(): void
270 {
271 parent::setUp();
272
273 $this->owner = User::factory()->create();
274 $this->client = Client::factory()->create(['user_id' => $this->owner]);
275 }
276}