Repository: thephpleague/oauth2-server (php)

Issue:
Support Public Clients for Password and Refresh Token Grant
Something that was confusing to me about this is the `AuthCodeGrant` [only calls `validateCredentials` for private clients](https://github.com/thephpleague/oauth2-server/blob/e1dc4d708c56fcfa205be4bb1862b6d525b4baac/src/Grant/AuthCodeGrant.php#L102) which made it seem like you no longer needed to check if the client was confidential before attempting to verify the secret.

However the refresh token grant and password grant both are supposed to support public clients according to the OAuth specification and do not check `Client::isConfidential` before calling `validateClient`.

Wouldn't that mean the `isConfidential` check in the `AuthCodeGrant` is unnecessary as you will need to check yourself in `validateClient` in order to support the other grant types?

_Originally posted by @matt-allan in https://github.com/thephpleague/oauth2-server/issues/1034#issuecomment-513974013_
Add Unauthorized_Client support
At the moment, the only error we do not handle is unauthorized_client. This is issued when a client tries to use a grant they have not been given permissions to. We should investigate the potential of using this in version 9.
RefreshTokenGrant requires client_secret also for non-confidential clients
RefreshTokenGrant::respondToAccessTokenRequest requires a client_secret, otherwise it will throw an exception (OAuthServerException::invalidClient). This does not make sense for non-confidential clients, as they are not able to send the client_secret.

Pull request:
Validate confidential clients and determine if the client handles the grant type
Fixes #1174
Fixes #1369
Fixes #1073
Closes #1036

This PR can be considered as a security enhancement and does 2 main changes:
1. Validate confidential clients:
   * [RFC6749 section 3.2.1](https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1) 
   * The "auth code" grant calls `AbstractGrant::validateClient()` only if the client is confidential ([RFC6749 section 4.1.3](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3:~:text=2Fcb%0A%0A%20%20%20The%20authorization%20server%20MUST%3A%0A%0A%20%20%20o-,require%20client%20authentication%20for%20confidential%20clients%20or%20for%20any%0A%20%20%20%20%20%20client%20that%20was%20issued%20client%20credentials%20(or%20with%20other%0A%20%20%20%20%20%20authentication%20requirements)%2C,-o%20%20authenticate%20the%20client%20if%20client)). But "device authorization", "refresh token" and "password" grants always validate the client secret. This PR allows public "device authorization" ([RFC8628 section 3.4](https://datatracker.ietf.org/doc/html/rfc8628#section-3.4:~:text=If%20the%20client%20was%20issued%20client%20credentials)), "refresh token" ([RFC6749 section 6](https://datatracker.ietf.org/doc/html/rfc6749#section-6:~:text=it%20was%20issued.-,If%20the%20client%20type%20is%20confidential,-or%0A%20%20%20the%20client)) and "password" ([RFC6749 section 4.3.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.3.2:~:text=6749%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20OAuth%202.0%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20October%202012%0A%0A%0A%20%20%20The%20authorization%20server%20MUST%3A%0A%0A%20%20%20o-,require%20client%20authentication%20for%20confidential%20clients%20or%20for%20any%0A%20%20%20%20%20%20client%20that%20was%20issued%20client%20credentials%20(or%20with%20other%0A%20%20%20%20%20%20authentication%20requirements)%2C,-o%20%20authenticate%20the%20client%20if%20client%20authentication%20is%20included%2C%20and)) clients by calling the `ClientRepositoryInterface::validateClient()` method only if the client is confidential.
   * Fixes #1073 
   * Fixes #1369
   * The "auth code" and "client credential" grants unnecessarily call `AbstractGrant::getClientEntityOrFail()` twice instead of just using the `AbstractGrant::validateClient()` return value, this PR fixes that.
   * All grants [except "client credentials" grant](https://github.com/thephpleague/oauth2-server/blob/1dc1ee861e8490a9e73d668b1ceccfa27436d861/src/Grant/ClientCredentialsGrant.php#L41-L45) ([RFC6749 section 4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4:~:text=The%20client%20credentials%20grant%20type%20MUST%20only%20be%20used%20by%20confidential%0A%20%20%20clients.)) can be non-confidential. It's already implemented this way on most community integrations:
     * [Passport](https://github.com/laravel/passport/blob/195058c12e035e332ae43f3d0b085f7c6e36795d/src/Bridge/ClientRepository.php#L56)
     * [Symfony](https://github.com/thephpleague/oauth2-server-bundle/blob/44272ff229da25305d4f5612546851bbcaa72d9a/src/Repository/ClientRepository.php#L52-L54)
     * [Mezzio](https://github.com/mezzio/mezzio-authentication-oauth2/blob/1b97f2a0e1bb07e0f6b2b4eda8f7d7806a73f1cf/src/Repository/Pdo/ClientRepository.php#L49-L51)
     * [Drupal](https://git.drupalcode.org/project/simple_oauth/-/blob/d8e843d89b90ec3a05f670f5d52f7369f662fe81/src/Repositories/ClientRepository.php#L60-77)
1. New `ClientTarit::supportsGrantType()` function:
    * [RFC6749 section 5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2:~:text=unauthorized_client%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20The%20authenticated%20client%20is%20not%20authorized%20to%20use%20this%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20authorization%20grant%20type.)
    * Fixes #1174  
    * This function is implemented on `ClientTrait` that returns `true` by default to avoid BC breaking changes.
    * Currently there is no way to check if the client handles the grant type before proceeding the request, e.g. We don't want to make auth code on "auth code grant" or make device code on "device code auth" grant or response with the access token on "implicit token" grant if the specified client doesn't handle the grant type. This PR makes this possible to avoid handling the requested grant type if the specified client doesn't supports that.
    * It also makes it possible for us to disable issuing refresh token if the client doesn't handle this grant.

Patch:
diff --git a/src/Entities/ClientEntityInterface.php b/src/Entities/ClientEntityInterface.php
index f3838b11..b0050ece 100644
--- a/src/Entities/ClientEntityInterface.php
+++ b/src/Entities/ClientEntityInterface.php
@@ -38,4 +38,11 @@ interface ClientEntityInterface
      * Returns true if the client is confidential.
      */
     public function isConfidential(): bool;
+
+    /*
+     * Returns true if the client supports the given grant type.
+     *
+     * To be added in a future major release.
+     */
+    // public function supportsGrantType(string $grantType): bool;
 }
diff --git a/src/Entities/Traits/ClientTrait.php b/src/Entities/Traits/ClientTrait.php
index b179cfac..ada53fa5 100644
--- a/src/Entities/Traits/ClientTrait.php
+++ b/src/Entities/Traits/ClientTrait.php
@@ -52,4 +52,12 @@ trait ClientTrait
     {
         return $this->isConfidential;
     }
+
+    /**
+     * Returns true if the client supports the given grant type.
+     */
+    public function supportsGrantType(string $grantType): bool
+    {
+        return true;
+    }
 }
diff --git a/src/Exception/OAuthServerException.php b/src/Exception/OAuthServerException.php
index 24a38d3f..df8b58a3 100644
--- a/src/Exception/OAuthServerException.php
+++ b/src/Exception/OAuthServerException.php
@@ -252,8 +252,17 @@ class OAuthServerException extends Exception
     }
 
     /**
+     * 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
+        );
     }
 
     /**
diff --git a/src/Grant/AbstractGrant.php b/src/Grant/AbstractGrant.php
index 5ab81ff7..7c27e95c 100644
--- a/src/Grant/AbstractGrant.php
+++ b/src/Grant/AbstractGrant.php
@@ -151,18 +151,18 @@ abstract class AbstractGrant implements GrantTypeInterface
     {
         [$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;
@@ -189,9 +189,22 @@ abstract class AbstractGrant implements GrantTypeInterface
             throw OAuthServerException::invalidClient($request);
         }
 
+        if ($this->supportsGrantType($client, $this->getIdentifier()) === false) {
+            throw OAuthServerException::unauthorizedClient();
+        }
+
         return $client;
     }
 
+    /**
+     * Returns true if the given client is authorized to use the given grant type.
+     */
+    protected function supportsGrantType(ClientEntityInterface $client, string $grantType): bool
+    {
+        return method_exists($client, 'supportsGrantType') === false
+            || $client->supportsGrantType($grantType) === true;
+    }
+
     /**
      * Gets the client credentials from the request from the request body or
      * the Http Basic Authorization header
@@ -484,6 +497,10 @@ abstract class AbstractGrant implements GrantTypeInterface
      */
     protected function issueRefreshToken(AccessTokenEntityInterface $accessToken): ?RefreshTokenEntityInterface
     {
+        if ($this->supportsGrantType($accessToken->getClient(), 'refresh_token') === false) {
+            return null;
+        }
+
         $refreshToken = $this->refreshTokenRepository->getNewRefreshToken();
 
         if ($refreshToken === null) {
diff --git a/src/Grant/AuthCodeGrant.php b/src/Grant/AuthCodeGrant.php
index 9fb5271c..9075f114 100644
--- a/src/Grant/AuthCodeGrant.php
+++ b/src/Grant/AuthCodeGrant.php
@@ -97,14 +97,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
         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);
 
diff --git a/src/Grant/ClientCredentialsGrant.php b/src/Grant/ClientCredentialsGrant.php
index bee6abaa..a24266c4 100644
--- a/src/Grant/ClientCredentialsGrant.php
+++ b/src/Grant/ClientCredentialsGrant.php
@@ -34,9 +34,7 @@ class ClientCredentialsGrant extends AbstractGrant
         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));
@@ -44,9 +42,6 @@ class ClientCredentialsGrant extends AbstractGrant
             throw OAuthServerException::invalidClient($request);
         }
 
-        // Validate request
-        $this->validateClient($request);
-
         $scopes = $this->validateScopes($this->getRequestParameter('scope', $request, $this->defaultScope));
 
         // Finalize the requested scopes


Test patch:
diff --git a/tests/Grant/AbstractGrantTest.php b/tests/Grant/AbstractGrantTest.php
index adfb880b..5711af10 100644
--- a/tests/Grant/AbstractGrantTest.php
+++ b/tests/Grant/AbstractGrantTest.php
@@ -265,84 +265,6 @@ class AbstractGrantTest extends TestCase
         $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();
@@ -398,6 +320,7 @@ class AbstractGrantTest extends TestCase
         $issueRefreshTokenMethod->setAccessible(true);
 
         $accessToken = new AccessTokenEntity();
+        $accessToken->setClient(new ClientEntity());
 
         /** @var RefreshTokenEntityInterface $refreshToken */
         $refreshToken = $issueRefreshTokenMethod->invoke($grantMock, $accessToken);
@@ -423,6 +346,34 @@ class AbstractGrantTest extends TestCase
         $issueRefreshTokenMethod->setAccessible(true);
 
         $accessToken = new AccessTokenEntity();
+        $accessToken->setClient(new ClientEntity());
+        self::assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken));
+    }
+
+    public function testIssueNullRefreshTokenUnauthorizedClient(): void
+    {
+        $client = $this->getMockBuilder(ClientEntity::class)->getMock();
+        $client
+            ->expects(self::once())
+            ->method('supportsGrantType')
+            ->with('refresh_token')
+            ->willReturn(false);
+
+        $refreshTokenRepoMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
+        $refreshTokenRepoMock->expects(self::never())->method('getNewRefreshToken');
+
+        /** @var AbstractGrant $grantMock */
+        $grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
+        $grantMock->setRefreshTokenTTL(new DateInterval('PT1M'));
+        $grantMock->setRefreshTokenRepository($refreshTokenRepoMock);
+
+        $abstractGrantReflection = new ReflectionClass($grantMock);
+        $issueRefreshTokenMethod = $abstractGrantReflection->getMethod('issueRefreshToken');
+        $issueRefreshTokenMethod->setAccessible(true);
+
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+
         self::assertNull($issueRefreshTokenMethod->invoke($grantMock, $accessToken));
     }
 
@@ -576,4 +527,30 @@ class AbstractGrantTest extends TestCase
 
         $grantMock->completeAuthorizationRequest(new AuthorizationRequest());
     }
+
+    public function testUnauthorizedClient(): void
+    {
+        $client = $this->getMockBuilder(ClientEntity::class)->getMock();
+        $client->method('supportsGrantType')->willReturn(false);
+
+        $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
+        $clientRepositoryMock
+            ->expects(self::once())
+            ->method('getClientEntity')
+            ->with('foo')
+            ->willReturn($client);
+
+        /** @var AbstractGrant $grantMock */
+        $grantMock = $this->getMockForAbstractClass(AbstractGrant::class);
+        $grantMock->setClientRepository($clientRepositoryMock);
+
+        $abstractGrantReflection = new ReflectionClass($grantMock);
+
+        $getClientEntityOrFailMethod = $abstractGrantReflection->getMethod('getClientEntityOrFail');
+        $getClientEntityOrFailMethod->setAccessible(true);
+
+        $this->expectException(OAuthServerException::class);
+
+        $getClientEntityOrFailMethod->invoke($grantMock, 'foo', new ServerRequest());
+    }
 }
diff --git a/tests/Grant/AuthCodeGrantTest.php b/tests/Grant/AuthCodeGrantTest.php
index 39000172..393359cf 100644
--- a/tests/Grant/AuthCodeGrantTest.php
+++ b/tests/Grant/AuthCodeGrantTest.php
@@ -616,7 +616,9 @@ class AuthCodeGrantTest extends TestCase
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -647,6 +649,7 @@ class AuthCodeGrantTest extends TestCase
             [
                 'grant_type'   => 'authorization_code',
                 'client_id'    => 'foo',
+                'client_secret' => 'bar',
                 'redirect_uri' => self::REDIRECT_URI,
                 'code'         => $this->cryptStub->doEncrypt(
                     json_encode([
@@ -685,8 +688,11 @@ class AuthCodeGrantTest extends TestCase
         $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity);
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -717,6 +723,7 @@ class AuthCodeGrantTest extends TestCase
             [
                 'grant_type'   => 'authorization_code',
                 'client_id'    => 'foo',
+                'client_secret' => 'bar',
                 'code'         => $this->cryptStub->doEncrypt(
                     json_encode([
                         'auth_code_id' => uniqid(),
@@ -743,13 +750,16 @@ class AuthCodeGrantTest extends TestCase
         $client->setIdentifier('foo');
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
         $clientRepositoryMock->method('getClientEntity')->willReturn($client);
+        $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
         $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity());
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
         $refreshTokenRepositoryMock->method('getNewRefreshToken')->willReturn(new RefreshTokenEntity());
@@ -806,6 +816,7 @@ class AuthCodeGrantTest extends TestCase
         $client->setRedirectUri(self::REDIRECT_URI);
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
         $clientRepositoryMock->method('getClientEntity')->willReturn($client);
+        $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
         $scopeEntity = new ScopeEntity();
@@ -813,7 +824,9 @@ class AuthCodeGrantTest extends TestCase
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -871,14 +884,18 @@ class AuthCodeGrantTest extends TestCase
         $client->setRedirectUri(self::REDIRECT_URI);
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
         $clientRepositoryMock->method('getClientEntity')->willReturn($client);
+        $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
         $scopeEntity = new ScopeEntity();
         $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity);
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -948,7 +965,9 @@ class AuthCodeGrantTest extends TestCase
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -980,6 +999,7 @@ class AuthCodeGrantTest extends TestCase
             [
                 'grant_type'    => 'authorization_code',
                 'client_id'     => 'foo',
+                'client_secret' => 'bar',
                 'redirect_uri'  => self::REDIRECT_URI,
                 'code_verifier' => self::CODE_VERIFIER,
                 'code'          => $this->cryptStub->doEncrypt(
@@ -1022,7 +1042,9 @@ class AuthCodeGrantTest extends TestCase
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -1054,6 +1076,7 @@ class AuthCodeGrantTest extends TestCase
             [
                 'grant_type'    => 'authorization_code',
                 'client_id'     => 'foo',
+                'client_secret' => 'bar',
                 'redirect_uri'  => self::REDIRECT_URI,
                 'code_verifier' => self::CODE_VERIFIER,
                 'code'          => $this->cryptStub->doEncrypt(
@@ -1358,6 +1381,7 @@ class AuthCodeGrantTest extends TestCase
 
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
         $clientRepositoryMock->method('getClientEntity')->willReturn($client);
+        $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $grant = new AuthCodeGrant(
             $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(),
@@ -1447,6 +1471,7 @@ class AuthCodeGrantTest extends TestCase
 
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
         $clientRepositoryMock->method('getClientEntity')->willReturn($client);
+        $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $grant = new AuthCodeGrant(
             $this->getMockBuilder(AuthCodeRepositoryInterface::class)->getMock(),
@@ -1535,6 +1560,7 @@ class AuthCodeGrantTest extends TestCase
             [
                 'grant_type'   => 'authorization_code',
                 'client_id'    => 'foo',
+                'client_secret' => 'bar',
                 'redirect_uri' => self::REDIRECT_URI,
                 'code'         => $this->cryptStub->doEncrypt(
                     json_encode([
@@ -1599,6 +1625,7 @@ class AuthCodeGrantTest extends TestCase
             [
                 'grant_type'   => 'authorization_code',
                 'client_id'    => 'foo',
+                'client_secret' => 'bar',
                 'redirect_uri' => self::REDIRECT_URI,
                 'code'         => $this->cryptStub->doEncrypt(
                     json_encode([
@@ -1662,6 +1689,7 @@ class AuthCodeGrantTest extends TestCase
             [
                 'grant_type'   => 'authorization_code',
                 'client_id'    => 'foo',
+                'client_secret' => 'bar',
                 'redirect_uri' => self::REDIRECT_URI,
                 'code'         => 'badCode',
             ]
@@ -1717,6 +1745,7 @@ class AuthCodeGrantTest extends TestCase
             [
             'grant_type'   => 'authorization_code',
             'client_id'    => 'foo',
+            'client_secret' => 'bar',
             'redirect_uri' => self::REDIRECT_URI,
             'code'         => 'badCode',
             ]
@@ -1737,7 +1766,6 @@ class AuthCodeGrantTest extends TestCase
 
         $client->setIdentifier('foo');
         $client->setRedirectUri(self::REDIRECT_URI);
-        $client->setConfidential();
 
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
 
@@ -1812,7 +1840,6 @@ class AuthCodeGrantTest extends TestCase
 
         $client->setIdentifier('foo');
         $client->setRedirectUri(self::REDIRECT_URI);
-        $client->setConfidential();
 
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
 
@@ -1887,7 +1914,6 @@ class AuthCodeGrantTest extends TestCase
 
         $client->setIdentifier('foo');
         $client->setRedirectUri(self::REDIRECT_URI);
-        $client->setConfidential();
 
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
 
@@ -1962,7 +1988,6 @@ class AuthCodeGrantTest extends TestCase
 
         $client->setIdentifier('foo');
         $client->setRedirectUri(self::REDIRECT_URI);
-        $client->setConfidential();
 
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
 
@@ -2037,7 +2062,6 @@ class AuthCodeGrantTest extends TestCase
 
         $client->setIdentifier('foo');
         $client->setRedirectUri(self::REDIRECT_URI);
-        $client->setConfidential();
 
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
 
@@ -2207,6 +2231,7 @@ class AuthCodeGrantTest extends TestCase
         $client->setRedirectUri(self::REDIRECT_URI);
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
         $clientRepositoryMock->method('getClientEntity')->willReturn($client);
+        $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
         $scopeEntity = new ScopeEntity();
@@ -2214,7 +2239,9 @@ class AuthCodeGrantTest extends TestCase
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -2282,6 +2309,7 @@ class AuthCodeGrantTest extends TestCase
         $client->setRedirectUri(self::REDIRECT_URI);
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
         $clientRepositoryMock->method('getClientEntity')->willReturn($client);
+        $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
         $scopeEntity = new ScopeEntity();
@@ -2289,7 +2317,9 @@ class AuthCodeGrantTest extends TestCase
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -2350,6 +2380,7 @@ class AuthCodeGrantTest extends TestCase
         $client->setRedirectUri(self::REDIRECT_URI);
         $clientRepositoryMock = $this->getMockBuilder(ClientRepositoryInterface::class)->getMock();
         $clientRepositoryMock->method('getClientEntity')->willReturn($client);
+        $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
         $scopeEntity = new ScopeEntity();
@@ -2357,7 +2388,9 @@ class AuthCodeGrantTest extends TestCase
         $scopeRepositoryMock->method('finalizeScopes')->willReturnArgument(0);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
diff --git a/tests/Grant/DeviceCodeGrantTest.php b/tests/Grant/DeviceCodeGrantTest.php
index 4a9cebbd..c02c1de0 100644
--- a/tests/Grant/DeviceCodeGrantTest.php
+++ b/tests/Grant/DeviceCodeGrantTest.php
@@ -349,6 +349,7 @@ class DeviceCodeGrantTest extends TestCase
             ->willReturn($deviceCodeEntity);
 
         $accessTokenEntity = new AccessTokenEntity();
+        $accessTokenEntity->setClient($client);
         $accessTokenEntity->addScope($scope);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
diff --git a/tests/Grant/PasswordGrantTest.php b/tests/Grant/PasswordGrantTest.php
index 8c60a8c7..5c91c94f 100644
--- a/tests/Grant/PasswordGrantTest.php
+++ b/tests/Grant/PasswordGrantTest.php
@@ -46,7 +46,9 @@ class PasswordGrantTest extends TestCase
         $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock();
@@ -91,8 +93,11 @@ class PasswordGrantTest extends TestCase
         $clientRepositoryMock->method('getClientEntity')->willReturn($client);
         $clientRepositoryMock->method('validateClient')->willReturn(true);
 
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $userRepositoryMock = $this->getMockBuilder(UserRepositoryInterface::class)->getMock();
@@ -167,9 +172,14 @@ class PasswordGrantTest extends TestCase
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
 
+        $scopeRepositoryMock = $this->getMockBuilder(ScopeRepositoryInterface::class)->getMock();
+        $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn(new ScopeEntity());
+
         $grant = new PasswordGrant($userRepositoryMock, $refreshTokenRepositoryMock);
         $grant->setClientRepository($clientRepositoryMock);
         $grant->setAccessTokenRepository($accessTokenRepositoryMock);
+        $grant->setDefaultScope(self::DEFAULT_SCOPE);
+        $grant->setScopeRepository($scopeRepositoryMock);
 
         $serverRequest = (new ServerRequest())->withParsedBody([
             'client_id'     => 'foo',
diff --git a/tests/Grant/RefreshTokenGrantTest.php b/tests/Grant/RefreshTokenGrantTest.php
index b2dbbadd..1fe1eada 100644
--- a/tests/Grant/RefreshTokenGrantTest.php
+++ b/tests/Grant/RefreshTokenGrantTest.php
@@ -61,7 +61,9 @@ class RefreshTokenGrantTest extends TestCase
         $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -125,8 +127,11 @@ class RefreshTokenGrantTest extends TestCase
         $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity);
         $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]);
 
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -183,7 +188,9 @@ class RefreshTokenGrantTest extends TestCase
         $clientRepositoryMock->method('validateClient')->willReturn(true);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -569,10 +576,12 @@ class RefreshTokenGrantTest extends TestCase
             ->with($scopes, $grant->getIdentifier(), $client)
             ->willReturn($finalizedScopes);
 
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
         $accessTokenRepositoryMock
             ->method('getNewToken')
             ->with($client, $finalizedScopes)
-            ->willReturn(new AccessTokenEntity());
+            ->willReturn($accessToken);
 
         $oldRefreshToken = json_encode(
             [
@@ -624,8 +633,11 @@ class RefreshTokenGrantTest extends TestCase
         $scopeRepositoryMock->method('getScopeEntityByIdentifier')->willReturn($scopeEntity);
         $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]);
 
+        $accessToken = new AccessTokenEntity();
+        $accessToken->setClient($client);
+
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessToken);
         $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();
@@ -772,7 +784,9 @@ class RefreshTokenGrantTest extends TestCase
         $scopeRepositoryMock->method('finalizeScopes')->willReturn([$scopeEntity]);
 
         $accessTokenRepositoryMock = $this->getMockBuilder(AccessTokenRepositoryInterface::class)->getMock();
-        $accessTokenRepositoryMock->method('getNewToken')->willReturn(new AccessTokenEntity());
+        $accessTokenEntity = new AccessTokenEntity();
+        $accessTokenEntity->setClient($client);
+        $accessTokenRepositoryMock->method('getNewToken')->willReturn($accessTokenEntity);
         $accessTokenRepositoryMock->expects(self::once())->method('persistNewAccessToken')->willReturnSelf();
 
         $refreshTokenRepositoryMock = $this->getMockBuilder(RefreshTokenRepositoryInterface::class)->getMock();