Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate confidential clients and determine if the client handles the grant type #1420

Merged
merged 14 commits into from
Feb 6, 2025
Merged
5 changes: 5 additions & 0 deletions src/Entities/ClientEntityInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ public function getRedirectUri(): string|array;
* Returns true if the client is confidential.
*/
public function isConfidential(): bool;

/**
* Returns true if the client handles the given grant type.
*/
public function hasGrantType(string $grantType): bool;
}
8 changes: 8 additions & 0 deletions src/Entities/Traits/ClientTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,12 @@ public function isConfidential(): bool
{
return $this->isConfidential;
}

/**
* Returns true if the client handles the given grant type.
*/
public function hasGrantType(string $grantType): bool
{
return true;
}
}
11 changes: 10 additions & 1 deletion src/Exception/OAuthServerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,17 @@ public static function slowDown(string $hint = '', Throwable $previous = null):
}

/**
* Unauthorized client error.
*/
public static function unauthorizedClient(?string $hint = null): static
{
return $this->errorType;
return new static(
'The authenticated client is not authorized to use this authorization grant type.',
14,
'unauthorized_client',
400,
$hint
);
}

/**
Expand Down
26 changes: 17 additions & 9 deletions src/Grant/AbstractGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,18 +151,18 @@ protected function validateClient(ServerRequestInterface $request): ClientEntity
{
[$clientId, $clientSecret] = $this->getClientCredentials($request);

if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));

throw OAuthServerException::invalidClient($request);
}
$client = $this->getClientEntityOrFail($clientId, $request);

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

if ($redirectUri !== null) {
$this->validateRedirectUri($redirectUri, $client, $request);
if ($this->clientRepository->validateClient($clientId, $clientSecret, $this->getIdentifier()) === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::CLIENT_AUTHENTICATION_FAILED, $request));

throw OAuthServerException::invalidClient($request);
}
}

return $client;
Expand All @@ -189,6 +189,10 @@ protected function getClientEntityOrFail(string $clientId, ServerRequestInterfac
throw OAuthServerException::invalidClient($request);
}

if (!$client->hasGrantType($this->getIdentifier())) {
throw OAuthServerException::unauthorizedClient();
}

return $client;
}

Expand Down Expand Up @@ -484,6 +488,10 @@ protected function issueAuthCode(
*/
protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface
{
if (!$accessToken->getClient()->hasGrantType('refresh_token')) {
return null;
}

$refreshToken = $this->refreshTokenRepository->getNewRefreshToken();

if ($refreshToken === null) {
Expand Down
9 changes: 1 addition & 8 deletions src/Grant/AuthCodeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,7 @@ public function respondToAccessTokenRequest(
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
): ResponseTypeInterface {
list($clientId) = $this->getClientCredentials($request);

$client = $this->getClientEntityOrFail($clientId, $request);

// Only validate the client if it is confidential
if ($client->isConfidential()) {
$this->validateClient($request);
}
$client = $this->validateClient($request);

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

Expand Down
7 changes: 1 addition & 6 deletions src/Grant/ClientCredentialsGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,14 @@ public function respondToAccessTokenRequest(
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
): ResponseTypeInterface {
list($clientId) = $this->getClientCredentials($request);

$client = $this->getClientEntityOrFail($clientId, $request);
$client = $this->validateClient($request);

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

throw OAuthServerException::invalidClient($request);
}

// Validate request
$this->validateClient($request);

$scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));

// Finalize the requested scopes
Expand Down
80 changes: 2 additions & 78 deletions tests/Grant/AbstractGrantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -265,84 +265,6 @@ public function testValidateClientInvalidClientSecret(): void
$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
}

public function testValidateClientInvalidRedirectUri(): void
{
$client = new ClientEntity();
$client->setRedirectUri('http://foo/bar');
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);

/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setClientRepository($clientRepositoryMock);

$abstractGrantReflection = new ReflectionClass($grantMock);

$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'redirect_uri' => 'http://bar/foo',
]);

$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);

$this->expectException(OAuthServerException::class);

$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
}

public function testValidateClientInvalidRedirectUriArray(): void
{
$client = new ClientEntity();
$client->setRedirectUri(['http://foo/bar']);
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);

/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setClientRepository($clientRepositoryMock);

$abstractGrantReflection = new ReflectionClass($grantMock);

$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'redirect_uri' => 'http://bar/foo',
]);

$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);

$this->expectException(OAuthServerException::class);

$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
}

public function testValidateClientMalformedRedirectUri(): void
{
$client = new ClientEntity();
$client->setRedirectUri('http://foo/bar');
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
$clientRepositoryMock->method('getClientEntity')->willReturn($client);

/** @var AbstractGrant $grantMock */
$grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
$grantMock->setClientRepository($clientRepositoryMock);

$abstractGrantReflection = new ReflectionClass($grantMock);

$serverRequest = (new ServerRequest())->withParsedBody([
'client_id' => 'foo',
'redirect_uri' => ['not', 'a', 'string'],
]);

$validateClientMethod = $abstractGrantReflection->getMethod('validateClient');
$validateClientMethod->setAccessible(true);

$this->expectException(OAuthServerException::class);

$validateClientMethod->invoke($grantMock, $serverRequest, true, true);
}

public function testValidateClientBadClient(): void
{
$clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
Expand Down Expand Up @@ -398,6 +320,7 @@ public function testIssueRefreshToken(): void
$issueRefreshTokenMethod->setAccessible(true);

$accessToken = new AccessTokenEntity();
$accessToken->setClient(new ClientEntity());

/** @var RefreshTokenEntityInterface $refreshToken */
$refreshToken = $issueRefreshTokenMethod->invoke($grantMock, $accessToken);
Expand All @@ -423,6 +346,7 @@ public function testIssueNullRefreshToken(): void
$issueRefreshTokenMethod->setAccessible(true);

$accessToken = new AccessTokenEntity();
$accessToken->setClient(new ClientEntity());
self::assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken));
}

Expand Down
Loading