Skip to content

Commit 089e2c3

Browse files
[13.x] Determine if the client handles the specified grant (#1762)
* check client handles grant * override Bridge\Client::hasGrantType * simplify validate client * formatting * add tests * formatting * formatting * add more tests * formatting
1 parent b907586 commit 089e2c3

9 files changed

+380
-187
lines changed

src/Bridge/Client.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,21 @@ public function __construct(
2121
string $name,
2222
array $redirectUri,
2323
bool $isConfidential = false,
24-
public ?string $provider = null
24+
public ?string $provider = null,
25+
public array $grantTypes = [],
2526
) {
2627
$this->setIdentifier($identifier);
2728

2829
$this->name = $name;
2930
$this->isConfidential = $isConfidential;
3031
$this->redirectUri = $redirectUri;
3132
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function supportsGrantType(string $grantType): bool
38+
{
39+
return in_array($grantType, $this->grantTypes);
40+
}
3241
}

src/Bridge/ClientRepository.php

+3-25
Original file line numberDiff line numberDiff line change
@@ -34,32 +34,9 @@ public function getClientEntity(string $clientIdentifier): ?ClientEntityInterfac
3434
*/
3535
public function validateClient(string $clientIdentifier, ?string $clientSecret, ?string $grantType): bool
3636
{
37-
// First, we will verify that the client exists and is authorized to create personal
38-
// access tokens. Generally personal access tokens are only generated by the user
39-
// from the main interface. We'll only let certain clients generate the tokens.
4037
$record = $this->clients->findActive($clientIdentifier);
4138

42-
if (! $record || ! $this->handlesGrant($record, $grantType)) {
43-
return false;
44-
}
45-
46-
return ! $record->confidential() || $this->verifySecret($clientSecret, $record->secret);
47-
}
48-
49-
/**
50-
* Determine if the given client can handle the given grant type.
51-
*/
52-
protected function handlesGrant(ClientModel $record, string $grantType): bool
53-
{
54-
return $record->hasGrantType($grantType);
55-
}
56-
57-
/**
58-
* Verify the client secret is valid.
59-
*/
60-
protected function verifySecret(string $clientSecret, string $storedHash): bool
61-
{
62-
return $this->hasher->check($clientSecret, $storedHash);
39+
return $record && ! empty($clientSecret) && $this->hasher->check($clientSecret, $record->secret);
6340
}
6441

6542
/**
@@ -82,7 +59,8 @@ protected function fromClientModel(ClientModel $model): ClientEntityInterface
8259
$model->name,
8360
$model->redirect_uris,
8461
$model->confidential(),
85-
$model->provider
62+
$model->provider,
63+
$model->grant_types
8664
);
8765
}
8866
}

src/Client.php

+19-11
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,24 @@ protected function redirectUris(): Attribute
147147
);
148148
}
149149

150+
/**
151+
* Interact with the client's grant types.
152+
*/
153+
protected function grantTypes(): Attribute
154+
{
155+
return Attribute::make(
156+
get: fn (?string $value): array => isset($value) ? $this->fromJson($value) : array_keys(array_filter([
157+
'authorization_code' => ! empty($this->redirect_uris),
158+
'client_credentials' => $this->confidential() && $this->firstParty(),
159+
'implicit' => ! empty($this->redirect_uris),
160+
'password' => $this->password_client,
161+
'personal_access' => $this->personal_access_client && $this->confidential(),
162+
'refresh_token' => true,
163+
'urn:ietf:params:oauth:grant-type:device_code' => true,
164+
])),
165+
);
166+
}
167+
150168
/**
151169
* Determine if the client is a "first party" client.
152170
*/
@@ -170,17 +188,7 @@ public function skipsAuthorization(Authenticatable $user, array $scopes): bool
170188
*/
171189
public function hasGrantType(string $grantType): bool
172190
{
173-
if (isset($this->attributes['grant_types']) && is_array($this->grant_types)) {
174-
return in_array($grantType, $this->grant_types);
175-
}
176-
177-
return match ($grantType) {
178-
'authorization_code' => ! $this->personal_access_client && ! $this->password_client,
179-
'personal_access' => $this->personal_access_client && $this->confidential(),
180-
'password' => $this->password_client,
181-
'client_credentials' => $this->confidential(),
182-
default => true,
183-
};
191+
return in_array($grantType, $this->grant_types);
184192
}
185193

186194
/**

tests/Feature/AuthorizationCodeGrantTest.php

+63
Original file line numberDiff line numberDiff line change
@@ -338,4 +338,67 @@ public function testPromptLogin()
338338
$response->assertSessionHas('promptedForLogin', true);
339339
$response->assertRedirectToRoute('login');
340340
}
341+
342+
public function testUnauthorizedClient()
343+
{
344+
$client = ClientFactory::new()->create([
345+
'grant_types' => [],
346+
]);
347+
348+
$query = http_build_query([
349+
'client_id' => $client->getKey(),
350+
'redirect_uri' => $client->redirect_uris[0],
351+
'response_type' => 'code',
352+
]);
353+
354+
$user = UserFactory::new()->create();
355+
$this->actingAs($user, 'web');
356+
357+
$json = $this->get('/oauth/authorize?'.$query)
358+
->assertBadRequest()
359+
->assertSessionMissing(['authRequest', 'authToken'])
360+
->json();
361+
362+
$this->assertSame('unauthorized_client', $json['error']);
363+
$this->assertSame(
364+
'The authenticated client is not authorized to use this authorization grant type.',
365+
$json['error_description']
366+
);
367+
}
368+
369+
public function testIssueAccessTokenWithoutRefreshToken()
370+
{
371+
$client = ClientFactory::new()->create([
372+
'grant_types' => ['authorization_code'],
373+
]);
374+
375+
$query = http_build_query([
376+
'client_id' => $client->getKey(),
377+
'redirect_uri' => $redirect = $client->redirect_uris[0],
378+
'response_type' => 'code',
379+
]);
380+
381+
$user = UserFactory::new()->create();
382+
$this->actingAs($user, 'web');
383+
384+
$authToken = $this->get('/oauth/authorize?'.$query)
385+
->assertOk()
386+
->json('authToken');
387+
388+
$response = $this->post('/oauth/authorize', ['auth_token' => $authToken])->assertRedirect();
389+
parse_str(parse_url($response->headers->get('Location'), PHP_URL_QUERY), $params);
390+
391+
$json = $this->post('/oauth/token', [
392+
'grant_type' => 'authorization_code',
393+
'client_id' => $client->getKey(),
394+
'client_secret' => $client->plainSecret,
395+
'redirect_uri' => $redirect,
396+
'code' => $params['code'],
397+
])->assertOk()->json();
398+
399+
$this->assertArrayHasKey('access_token', $json);
400+
$this->assertArrayNotHasKey('refresh_token', $json);
401+
$this->assertSame('Bearer', $json['token_type']);
402+
$this->assertArrayHasKey('expires_in', $json);
403+
}
341404
}

tests/Feature/ClientCredentialsGrantTest.php

+31
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,35 @@ public function testIssueAccessToken()
5454
$response = $this->withToken($json['access_token'], $json['token_type'])->get('/bar');
5555
$response->assertForbidden();
5656
}
57+
58+
public function testPublicClient()
59+
{
60+
$client = ClientFactory::new()->asClientCredentials()->asPublic()->create();
61+
62+
$json = $this->post('/oauth/token', [
63+
'grant_type' => 'client_credentials',
64+
'client_id' => $client->getKey(),
65+
'client_secret' => $client->plainSecret,
66+
])->assertUnauthorized()->json();
67+
68+
$this->assertSame('invalid_client', $json['error']);
69+
$this->assertSame('Client authentication failed', $json['error_description']);
70+
}
71+
72+
public function testUnauthorizedClient()
73+
{
74+
$client = ClientFactory::new()->create();
75+
76+
$json = $this->post('/oauth/token', [
77+
'grant_type' => 'client_credentials',
78+
'client_id' => $client->getKey(),
79+
'client_secret' => $client->plainSecret,
80+
])->assertBadRequest()->json();
81+
82+
$this->assertSame('unauthorized_client', $json['error']);
83+
$this->assertSame(
84+
'The authenticated client is not authorized to use this authorization grant type.',
85+
$json['error_description']
86+
);
87+
}
5788
}

tests/Feature/ClientTest.php

+18-4
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,19 @@ public function testGrantTypesWhenColumnDoesNotExist(): void
7575
$client = new Client();
7676
$client->exists = true;
7777

78-
$this->assertTrue($client->hasGrantType('foo'));
79-
8078
$client->personal_access_client = false;
8179
$client->password_client = false;
8280

81+
$this->assertFalse($client->hasGrantType('foo'));
82+
$this->assertFalse($client->hasGrantType('authorization_code'));
83+
$this->assertFalse($client->hasGrantType('password'));
84+
$this->assertFalse($client->hasGrantType('personal_access'));
85+
$this->assertFalse($client->hasGrantType('client_credentials'));
86+
87+
$client->redirect = 'http://localhost';
8388
$this->assertTrue($client->hasGrantType('authorization_code'));
89+
$this->assertTrue($client->hasGrantType('implicit'));
90+
unset($client->redirect);
8491

8592
$client->personal_access_client = false;
8693
$client->password_client = true;
@@ -100,11 +107,18 @@ public function testGrantTypesWhenColumnIsNull(): void
100107
$client = new Client(['grant_types' => null]);
101108
$client->exists = true;
102109

103-
$this->assertTrue($client->hasGrantType('foo'));
104-
105110
$client->personal_access_client = false;
106111
$client->password_client = false;
112+
$this->assertFalse($client->hasGrantType('foo'));
113+
$this->assertFalse($client->hasGrantType('authorization_code'));
114+
$this->assertFalse($client->hasGrantType('password'));
115+
$this->assertFalse($client->hasGrantType('personal_access'));
116+
$this->assertFalse($client->hasGrantType('client_credentials'));
117+
118+
$client->redirect = 'http://localhost';
107119
$this->assertTrue($client->hasGrantType('authorization_code'));
120+
$this->assertTrue($client->hasGrantType('implicit'));
121+
unset($client->redirect);
108122

109123
$client->personal_access_client = false;
110124
$client->password_client = true;

tests/Feature/ImplicitGrantTest.php

+25
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,29 @@ public function testPromptLogin()
319319
$response->assertSessionHas('promptedForLogin', true);
320320
$response->assertRedirectToRoute('login');
321321
}
322+
323+
public function testUnauthorizedClient()
324+
{
325+
$client = ClientFactory::new()->create();
326+
327+
$query = http_build_query([
328+
'client_id' => $client->getKey(),
329+
'redirect_uri' => $client->redirect_uris[0],
330+
'response_type' => 'token',
331+
]);
332+
333+
$user = UserFactory::new()->create();
334+
$this->actingAs($user, 'web');
335+
336+
$json = $this->get('/oauth/authorize?'.$query)
337+
->assertBadRequest()
338+
->assertSessionMissing(['authRequest', 'authToken'])
339+
->json();
340+
341+
$this->assertSame('unauthorized_client', $json['error']);
342+
$this->assertSame(
343+
'The authenticated client is not authorized to use this authorization grant type.',
344+
$json['error_description']
345+
);
346+
}
322347
}

0 commit comments

Comments
 (0)