From bf4128e5dbf10011b8f5e6da1a2d6d187e7dcf1a Mon Sep 17 00:00:00 2001 From: Bojan Bogdanovic <info@bojanbogdanovic.nl> Date: Mon, 30 Dec 2024 15:03:34 +0100 Subject: [PATCH 1/3] Limit the allowed scopes by default scopes via the consumer for the client credentials grant type --- simple_oauth.module | 2 +- src/Repositories/ScopeRepository.php | 19 +++++++-- tests/src/Kernel/ClientCredentialsTest.php | 47 +++++++++++++++++++++- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/simple_oauth.module b/simple_oauth.module index ebf7145..61349e1 100644 --- a/simple_oauth.module +++ b/simple_oauth.module @@ -95,7 +95,7 @@ function simple_oauth_entity_base_field_info(EntityTypeInterface $entity_type) { $fields['scopes'] = BaseFieldDefinition::create('oauth2_scope_reference') ->setLabel(new TranslatableMarkup('Scopes')) - ->setDescription(new TranslatableMarkup('Here you can select scopes that would be the default scopes when authorizing.')) + ->setDescription(new TranslatableMarkup('Selecting scopes here will be set as default when authorizing (so no need to specify the scope parameter on the token request), also the selection of scopes will limit the allowed scopes on the consumer/client.')) ->setDisplayOptions('view', [ 'label' => 'inline', 'type' => 'oauth2_scope_reference_label', diff --git a/src/Repositories/ScopeRepository.php b/src/Repositories/ScopeRepository.php index 4cf5d4e..3824b4c 100644 --- a/src/Repositories/ScopeRepository.php +++ b/src/Repositories/ScopeRepository.php @@ -66,11 +66,24 @@ class ScopeRepository implements ScopeRepositoryInterface { } $default_scopes = []; + $allowed_scopes = []; $client_drupal_entity = $clientEntity->getDrupalEntity(); if (!$client_drupal_entity->get('scopes')->isEmpty()) { - $default_scopes = array_map(function (Oauth2ScopeInterface $scope) { - return $this->scopeFactory($scope); - }, $client_drupal_entity->get('scopes')->getScopes()); + foreach ($client_drupal_entity->get('scopes')->getScopes() as $scope) { + $default_scope = $this->scopeFactory($scope); + $default_scopes[] = $default_scope; + $allowed_scopes[] = $default_scope->getIdentifier(); + } + + // Limit the scopes if default scopes are set on the consumer for the + // client credentials grant type. + if ($grantType === 'client_credentials') { + foreach ($scopes as $scope) { + if (!in_array($scope->getIdentifier(), $allowed_scopes)) { + throw OAuthServerException::invalidScope($scope->getIdentifier()); + } + } + } } $finalized_scopes = !empty($scopes) ? $scopes : $default_scopes; diff --git a/tests/src/Kernel/ClientCredentialsTest.php b/tests/src/Kernel/ClientCredentialsTest.php index 965bbb1..61ee88f 100644 --- a/tests/src/Kernel/ClientCredentialsTest.php +++ b/tests/src/Kernel/ClientCredentialsTest.php @@ -4,6 +4,9 @@ namespace Drupal\Tests\simple_oauth\Kernel; use Drupal\Component\Serialization\Json; use Drupal\TestTools\Random; +use Drupal\Core\Session\AccountInterface; +use Drupal\simple_oauth\Entity\Oauth2Scope; +use Drupal\simple_oauth\Oauth2ScopeInterface; use Symfony\Component\HttpFoundation\Request; /** @@ -197,7 +200,6 @@ class ClientCredentialsTest extends AuthorizedRequestBase { * @dataProvider publicClientCredentialsGrantProvider */ public function testPublicClientCredentialsGrant(bool $confidential, string|null $client_secret, string $error, int $code): void { - // Configure the client for the test. $this->client ->set('confidential', $confidential) @@ -218,4 +220,47 @@ class ClientCredentialsTest extends AuthorizedRequestBase { $this->assertSame($error, $parsed_response['error'], sprintf('Correct error code %s', $error)); } + /** + * Test the not allowed scopes set on the client. + */ + public function testNotAllowedScopes(): void { + $not_allowed_scope = Oauth2Scope::create([ + 'name' => 'test:scope3', + 'description' => 'Test scope 3 description', + 'grant_types' => [ + 'client_credentials' => [ + 'status' => TRUE, + 'description' => 'Test scope 3 description client_credentials', + ], + ], + 'umbrella' => FALSE, + 'granularity_id' => Oauth2ScopeInterface::GRANULARITY_ROLE, + 'granularity_configuration' => [ + 'role' => AccountInterface::AUTHENTICATED_ROLE, + ], + ]); + $not_allowed_scope->save(); + + $parameters = [ + 'grant_type' => 'client_credentials', + 'client_id' => $this->client->getClientId(), + 'client_secret' => $this->clientSecret, + 'scope' => $not_allowed_scope->getName(), + ]; + $request = Request::create($this->url->toString(), 'POST', $parameters); + $response = $this->httpKernel->handle($request); + $parsed_response = Json::decode((string) $response->getContent()); + + $this->assertEquals(400, $response->getStatusCode()); + $this->assertEquals('The requested scope is invalid, unknown, or malformed', $parsed_response['message']); + + $parameters['scope'] = "{$this->scope} {$not_allowed_scope->getName()}"; + $request = Request::create($this->url->toString(), 'POST', $parameters); + $response = $this->httpKernel->handle($request); + $parsed_response = Json::decode((string) $response->getContent()); + + $this->assertEquals(400, $response->getStatusCode()); + $this->assertEquals('The requested scope is invalid, unknown, or malformed', $parsed_response['message']); + } + } -- GitLab From 86674470421c396343986b97f59f0b86de4fc0df Mon Sep 17 00:00:00 2001 From: Bojan Bogdanovic <info@bojanbogdanovic.nl> Date: Mon, 30 Dec 2024 15:25:14 +0100 Subject: [PATCH 2/3] Update assertion --- tests/src/Kernel/ClientCredentialsTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/src/Kernel/ClientCredentialsTest.php b/tests/src/Kernel/ClientCredentialsTest.php index 61ee88f..70b088c 100644 --- a/tests/src/Kernel/ClientCredentialsTest.php +++ b/tests/src/Kernel/ClientCredentialsTest.php @@ -252,7 +252,7 @@ class ClientCredentialsTest extends AuthorizedRequestBase { $parsed_response = Json::decode((string) $response->getContent()); $this->assertEquals(400, $response->getStatusCode()); - $this->assertEquals('The requested scope is invalid, unknown, or malformed', $parsed_response['message']); + $this->assertEquals('The requested scope is invalid, unknown, or malformed', $parsed_response['error_description']); $parameters['scope'] = "{$this->scope} {$not_allowed_scope->getName()}"; $request = Request::create($this->url->toString(), 'POST', $parameters); @@ -260,7 +260,7 @@ class ClientCredentialsTest extends AuthorizedRequestBase { $parsed_response = Json::decode((string) $response->getContent()); $this->assertEquals(400, $response->getStatusCode()); - $this->assertEquals('The requested scope is invalid, unknown, or malformed', $parsed_response['message']); + $this->assertEquals('The requested scope is invalid, unknown, or malformed', $parsed_response['error_description']); } } -- GitLab From a51210c1135b65e22b9a7a73d2d67dffa743b85c Mon Sep 17 00:00:00 2001 From: Bojan Bogdanovic <info@bojanbogdanovic.nl> Date: Thu, 23 Jan 2025 14:13:54 +0100 Subject: [PATCH 3/3] Make scope parameter required in the token endpoint when dealing with client_credentials and no default scopes are set --- src/Controller/Oauth2Token.php | 12 ++++++++++++ tests/src/Kernel/ClientCredentialsTest.php | 13 ++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Controller/Oauth2Token.php b/src/Controller/Oauth2Token.php index c0f31e2..a3d8e6e 100644 --- a/src/Controller/Oauth2Token.php +++ b/src/Controller/Oauth2Token.php @@ -113,6 +113,8 @@ class Oauth2Token extends ControllerBase { $server_request = $this->httpMessageFactory->createRequest($request); $server_response = new Response(); $client_id = $request->get('client_id'); + $grant_type = $request->get('grant_type'); + $scopes = $request->get('scope'); $lock_key = $this->createLockKey($request); @@ -135,6 +137,16 @@ class Oauth2Token extends ControllerBase { } $client_drupal_entity = $client_entity->getDrupalEntity(); + // Omitting scopes is not allowed when dealing with client_credentials + // and no default scopes are set. + if ( + $grant_type === 'client_credentials' && + empty($scopes) && + $client_drupal_entity->get('scopes')->isEmpty() + ) { + throw OAuthServerException::invalidRequest('scope'); + } + // Respond to the incoming request and fill in the response. $server = $this->authorizationServerFactory->get($client_drupal_entity); $response = $server->respondToAccessTokenRequest($server_request, $server_response); diff --git a/tests/src/Kernel/ClientCredentialsTest.php b/tests/src/Kernel/ClientCredentialsTest.php index 70b088c..e57ac36 100644 --- a/tests/src/Kernel/ClientCredentialsTest.php +++ b/tests/src/Kernel/ClientCredentialsTest.php @@ -71,21 +71,26 @@ class ClientCredentialsTest extends AuthorizedRequestBase { */ public static function missingClientCredentialsProvider(): array { return [ - 'grant_type' => [ + [ 'grant_type', 'unsupported_grant_type', 400, ], - 'client_id' => [ + [ 'client_id', 'invalid_request', 400, ], - 'client_secret' => [ + [ 'client_secret', 'invalid_client', 401, ], + [ + 'scope', + 'invalid_request', + 400, + ], ]; } @@ -102,6 +107,8 @@ class ClientCredentialsTest extends AuthorizedRequestBase { 'scope' => $this->scope, ]; unset($parameters[$key]); + $this->client->set('scopes', []); + $this->client->save(); $request = Request::create($this->url->toString(), 'POST', $parameters); $response = $this->httpKernel->handle($request); $parsed_response = Json::decode((string) $response->getContent()); -- GitLab