Skip to content

Commit f654cb3

Browse files
authored
Merge pull request #1420 from hafezdivandari/always-validate-client
Validate confidential clients and determine if the client handles the grant type
2 parents d4ef047 + db44e64 commit f654cb3

11 files changed

+189
-125
lines changed

src/Entities/ClientEntityInterface.php

+7
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,11 @@ public function getRedirectUri(): string|array;
3838
* Returns true if the client is confidential.
3939
*/
4040
public function isConfidential(): bool;
41+
42+
/*
43+
* Returns true if the client supports the given grant type.
44+
*
45+
* To be added in a future major release.
46+
*/
47+
// public function supportsGrantType(string $grantType): bool;
4148
}

src/Entities/Traits/ClientTrait.php

+8
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,12 @@ public function isConfidential(): bool
5252
{
5353
return $this->isConfidential;
5454
}
55+
56+
/**
57+
* Returns true if the client supports the given grant type.
58+
*/
59+
public function supportsGrantType(string $grantType): bool
60+
{
61+
return true;
62+
}
5563
}

src/Exception/OAuthServerException.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,17 @@ public static function slowDown(string $hint = '', ?Throwable $previous = null):
252252
}
253253

254254
/**
255+
* Unauthorized client error.
256+
*/
257+
public static function unauthorizedClient(?string $hint = null): static
255258
{
256-
return $this->errorType;
259+
return new static(
260+
'The authenticated client is not authorized to use this authorization grant type.',
261+
14,
262+
'unauthorized_client',
263+
400,
264+
$hint
265+
);
257266
}
258267

259268
/**

src/Grant/AbstractGrant.php

+26-9
Original file line numberDiff line numberDiff line change
@@ -151,18 +151,18 @@ protected function validateClient(ServerRequestInterface $request): ClientEntity
151151
{
152152
[$clientId, $clientSecret] = $this->getClientCredentials($request);
153153

154-
if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) {
155-
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
156-
157-
throw OAuthServerException::invalidClient($request);
158-
}
159154
$client = $this->getClientEntityOrFail($clientId, $request);
160155

161-
// If a redirect URI is provided ensure it matches what is pre-registered
162-
$redirectUri = $this->getRequestParameter('redirect_uri', $request);
156+
if ($client->isConfidential()) {
157+
if ($clientSecret === '') {
158+
throw OAuthServerException::invalidRequest('client_secret');
159+
}
163160

164-
if ($redirectUri !== null) {
165-
$this->validateRedirectUri($redirectUri, $client, $request);
161+
if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) {
162+
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
163+
164+
throw OAuthServerException::invalidClient($request);
165+
}
166166
}
167167

168168
return $client;
@@ -189,9 +189,22 @@ protected function getClientEntityOrFail(string $clientId, ServerRequestInterfac
189189
throw OAuthServerException::invalidClient($request);
190190
}
191191

192+
if ($this->supportsGrantType($client, $this->getIdentifier()) === false) {
193+
throw OAuthServerException::unauthorizedClient();
194+
}
195+
192196
return $client;
193197
}
194198

199+
/**
200+
* Returns true if the given client is authorized to use the given grant type.
201+
*/
202+
protected function supportsGrantType(ClientEntityInterface $client, string $grantType): bool
203+
{
204+
return method_exists($client, 'supportsGrantType') === false
205+
|| $client->supportsGrantType($grantType) === true;
206+
}
207+
195208
/**
196209
* Gets the client credentials from the request from the request body or
197210
* the Http Basic Authorization header
@@ -484,6 +497,10 @@ protected function issueAuthCode(
484497
*/
485498
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface
486499
{
500+
if ($this->supportsGrantType($accessToken->getClient(), 'refresh_token') === false) {
501+
return null;
502+
}
503+
487504
$refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
488505

489506
if ($refreshToken === null) {

src/Grant/AuthCodeGrant.php

+1-8
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,7 @@ public function respondToAccessTokenRequest(
9797
ResponseTypeInterface $responseType,
9898
DateInterval $accessTokenTTL
9999
): ResponseTypeInterface {
100-
list($clientId) = $this->getClientCredentials($request);
101-
102-
$client = $this->getClientEntityOrFail($clientId, $request);
103-
104-
// Only validate the client if it is confidential
105-
if ($client->isConfidential()) {
106-
$this->validateClient($request);
107-
}
100+
$client = $this->validateClient($request);
108101

109102
$encryptedAuthCode = $this->getRequestParameter('code', $request);
110103

src/Grant/ClientCredentialsGrant.php

+1-6
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,14 @@ public function respondToAccessTokenRequest(
3434
ResponseTypeInterface $responseType,
3535
DateInterval $accessTokenTTL
3636
): ResponseTypeInterface {
37-
list($clientId) = $this->getClientCredentials($request);
38-
39-
$client = $this->getClientEntityOrFail($clientId, $request);
37+
$client = $this->validateClient($request);
4038

4139
if (!$client->isConfidential()) {
4240
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));
4341

4442
throw OAuthServerException::invalidClient($request);
4543
}
4644

47-
// Validate request
48-
$this->validateClient($request);
49-
5045
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
5146

5247
// Finalize the requested scopes

tests/Grant/AbstractGrantTest.php

+55-78
Original file line numberDiff line numberDiff line change
@@ -265,84 +265,6 @@ public function testValidateClientInvalidClientSecret(): void
265265
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
266266
}
267267

268-
public function testValidateClientInvalidRedirectUri(): void
269-
{
270-
$client = new ClientEntity();
271-
$client->setRedirectUri('http://foo/bar');
272-
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
273-
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
274-
275-
/** @var AbstractGrant $grantMock */
276-
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
277-
$grantMock->setClientRepository($clientRepositoryMock);
278-
279-
$abstractGrantReflection = new ReflectionClass($grantMock);
280-
281-
$serverRequest = (new ServerRequest())->withParsedBody([
282-
'client_id' => 'foo',
283-
'redirect_uri' => 'http://bar/foo',
284-
]);
285-
286-
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
287-
$validateClientMethod->setAccessible(true);
288-
289-
$this->expectException(OAuthServerException::class);
290-
291-
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
292-
}
293-
294-
public function testValidateClientInvalidRedirectUriArray(): void
295-
{
296-
$client = new ClientEntity();
297-
$client->setRedirectUri(['http://foo/bar']);
298-
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
299-
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
300-
301-
/** @var AbstractGrant $grantMock */
302-
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
303-
$grantMock->setClientRepository($clientRepositoryMock);
304-
305-
$abstractGrantReflection = new ReflectionClass($grantMock);
306-
307-
$serverRequest = (new ServerRequest())->withParsedBody([
308-
'client_id' => 'foo',
309-
'redirect_uri' => 'http://bar/foo',
310-
]);
311-
312-
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
313-
$validateClientMethod->setAccessible(true);
314-
315-
$this->expectException(OAuthServerException::class);
316-
317-
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
318-
}
319-
320-
public function testValidateClientMalformedRedirectUri(): void
321-
{
322-
$client = new ClientEntity();
323-
$client->setRedirectUri('http://foo/bar');
324-
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
325-
$clientRepositoryMock->method('getClientEntity')->willReturn($client);
326-
327-
/** @var AbstractGrant $grantMock */
328-
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
329-
$grantMock->setClientRepository($clientRepositoryMock);
330-
331-
$abstractGrantReflection = new ReflectionClass($grantMock);
332-
333-
$serverRequest = (new ServerRequest())->withParsedBody([
334-
'client_id' => 'foo',
335-
'redirect_uri' => ['not', 'a', 'string'],
336-
]);
337-
338-
$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
339-
$validateClientMethod->setAccessible(true);
340-
341-
$this->expectException(OAuthServerException::class);
342-
343-
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
344-
}
345-
346268
public function testValidateClientBadClient(): void
347269
{
348270
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
@@ -398,6 +320,7 @@ public function testIssueRefreshToken(): void
398320
$issueRefreshTokenMethod->setAccessible(true);
399321

400322
$accessToken = new AccessTokenEntity();
323+
$accessToken->setClient(new ClientEntity());
401324

402325
/** @var RefreshTokenEntityInterface $refreshToken */
403326
$refreshToken = $issueRefreshTokenMethod->invoke($grantMock, $accessToken);
@@ -423,6 +346,34 @@ public function testIssueNullRefreshToken(): void
423346
$issueRefreshTokenMethod->setAccessible(true);
424347

425348
$accessToken = new AccessTokenEntity();
349+
$accessToken->setClient(new ClientEntity());
350+
self::assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken));
351+
}
352+
353+
public function testIssueNullRefreshTokenUnauthorizedClient(): void
354+
{
355+
$client = $this->getMockBuilder(ClientEntity::class)->getMock();
356+
$client
357+
->expects(self::once())
358+
->method('supportsGrantType')
359+
->with('refresh_token')
360+
->willReturn(false);
361+
362+
$refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
363+
$refreshTokenRepoMock->expects(self::never())->method('getNewRefreshToken');
364+
365+
/** @var AbstractGrant $grantMock */
366+
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
367+
$grantMock->setRefreshTokenTTL(new DateInterval('PT1M'));
368+
$grantMock->setRefreshTokenRepository($refreshTokenRepoMock);
369+
370+
$abstractGrantReflection = new ReflectionClass($grantMock);
371+
$issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken');
372+
$issueRefreshTokenMethod->setAccessible(true);
373+
374+
$accessToken = new AccessTokenEntity();
375+
$accessToken->setClient($client);
376+
426377
self::assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken));
427378
}
428379

@@ -576,4 +527,30 @@ public function testCompleteAuthorizationRequest(): void
576527

577528
$grantMock->completeAuthorizationRequest(new AuthorizationRequest());
578529
}
530+
531+
public function testUnauthorizedClient(): void
532+
{
533+
$client = $this->getMockBuilder(ClientEntity::class)->getMock();
534+
$client->method('supportsGrantType')->willReturn(false);
535+
536+
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
537+
$clientRepositoryMock
538+
->expects(self::once())
539+
->method('getClientEntity')
540+
->with('foo')
541+
->willReturn($client);
542+
543+
/** @var AbstractGrant $grantMock */
544+
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
545+
$grantMock->setClientRepository($clientRepositoryMock);
546+
547+
$abstractGrantReflection = new ReflectionClass($grantMock);
548+
549+
$getClientEntityOrFailMethod = $abstractGrantReflection->getMethod('getClientEntityOrFail');
550+
$getClientEntityOrFailMethod->setAccessible(true);
551+
552+
$this->expectException(OAuthServerException::class);
553+
554+
$getClientEntityOrFailMethod->invoke($grantMock, 'foo', new ServerRequest());
555+
}
579556
}

0 commit comments

Comments
 (0)