From 291099cf4d31e110827c702dffec33fbea1fd6af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Gonz=C3=A1lez?= Date: Thu, 7 Sep 2023 12:54:30 +0100 Subject: [PATCH 01/11] add openid connect service --- src/Social/Service/OpenIDConnectService.php | 85 +++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/Social/Service/OpenIDConnectService.php diff --git a/src/Social/Service/OpenIDConnectService.php b/src/Social/Service/OpenIDConnectService.php new file mode 100644 index 0000000..d855c45 --- /dev/null +++ b/src/Social/Service/OpenIDConnectService.php @@ -0,0 +1,85 @@ + [ + 'baseUrl' => 'https://www.linkedin.com/', + 'url' => 'https://www.linkedin.com/oauth/.well-known/openid-configuration', + 'jwk' => [ + 'defaultAlgorithm' => 'RS256', + ], + ], + ]; + + public function getUser(ServerRequestInterface $request): array + { + if (!$request instanceof ServerRequest) { + throw new \BadMethodCallException('Request must be an instance of ServerRequest'); + } + if (!$this->validate($request)) { + throw new BadRequestException('Invalid OAuth2 state'); + } + + $code = $request->getQuery('code'); + /** @var \League\OAuth2\Client\Token\AccessToken $token */ + $token = $this->provider->getAccessToken('authorization_code', ['code' => $code]); + $tokenValues = $token->getValues(); + $idToken = $tokenValues['id_token'] ?? null; + if (!$idToken) { + throw new BadRequestException('Missing id_token in response'); + } + try { + $idTokenDecoded = JWT::decode($idToken, $this->getIdTokenKeys()); + + return ['token' => $token] + (array)$idTokenDecoded; + } catch (\Exception $ex) { + throw new BadRequestException('Invalid id token. ' . $ex->getMessage()); + } + } + + protected function getIdTokenKeys(): array + { + $discoverData = $this->discover(); + $jwksUri = $discoverData['jwks_uri'] ?? null; + if (!$jwksUri) { + throw new BadRequestException( + 'No `jwks_uri` in discover data. Unable to retrieve the JWT signature public key' + ); + } + if (strpos($jwksUri, $this->getConfig('openid.baseUrl')) !== 0) { + throw new BadRequestException( + 'Invalid `jwks_uri` in discover data. It is not pointing to ' . + $this->getConfig('openid.baseUrl') + ); + } + $client = new Client(); + $jwksData = $client->get($jwksUri)->getJson(); + if (!$jwksData) { + throw new BadRequestException( + 'Unable to retrieve jwks. Not found in the `jwks_uri` contents' + ); + } + + return JWK::parseKeySet($jwksData, $this->getConfig('openid.jwk.defaultAlgorithm')); + } + + public function discover(): array + { + $openidUrl = $this->getConfig('openid.url'); + $client = new Client(); + + return $client->get($openidUrl)->getJson(); + } +} From 4c0a660f9925ea17281d3702b04a7fb1a9280757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Gonz=C3=A1lez?= Date: Thu, 7 Sep 2023 12:54:30 +0100 Subject: [PATCH 02/11] fix cs --- src/Social/Service/OpenIDConnectService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Social/Service/OpenIDConnectService.php b/src/Social/Service/OpenIDConnectService.php index d855c45..3cac93f 100644 --- a/src/Social/Service/OpenIDConnectService.php +++ b/src/Social/Service/OpenIDConnectService.php @@ -6,7 +6,6 @@ use Cake\Http\Client; use Cake\Http\Exception\BadRequestException; use Cake\Http\ServerRequest; -use CakeDC\Auth\Social\Service\OAuth2Service; use Firebase\JWT\JWK; use Firebase\JWT\JWT; use Psr\Http\Message\ServerRequestInterface; From f522e5999ac5dd63e6089459c1a289be06fbc38f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Gonz=C3=A1lez?= Date: Thu, 7 Sep 2023 12:57:49 +0100 Subject: [PATCH 03/11] #openid-connect-linkedin add linkedin connect mapper --- src/Social/Mapper/LinkedInOpenIDConnect.php | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/Social/Mapper/LinkedInOpenIDConnect.php diff --git a/src/Social/Mapper/LinkedInOpenIDConnect.php b/src/Social/Mapper/LinkedInOpenIDConnect.php new file mode 100644 index 0000000..7efd449 --- /dev/null +++ b/src/Social/Mapper/LinkedInOpenIDConnect.php @@ -0,0 +1,39 @@ + 'picture', + 'first_name' => 'given_name', + 'last_name' => 'family_name', + 'email' => 'email', + 'link' => 'link', + 'id' => 'sub', + ]; + + protected function _link(): string { + // no way to retrieve the public url from the users profile + + return 'https://www.linkedin.com'; + } +} From aee0010b0a6da699b3320823ece06abaa01b28f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Gonz=C3=A1lez?= Date: Thu, 7 Sep 2023 12:57:49 +0100 Subject: [PATCH 04/11] fix cs --- src/Social/Mapper/LinkedInOpenIDConnect.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Social/Mapper/LinkedInOpenIDConnect.php b/src/Social/Mapper/LinkedInOpenIDConnect.php index 7efd449..cd190ab 100644 --- a/src/Social/Mapper/LinkedInOpenIDConnect.php +++ b/src/Social/Mapper/LinkedInOpenIDConnect.php @@ -13,8 +13,6 @@ namespace CakeDC\Auth\Social\Mapper; -use CakeDC\Auth\Social\Mapper\AbstractMapper; - class LinkedInOpenIDConnect extends AbstractMapper { /** @@ -31,7 +29,8 @@ class LinkedInOpenIDConnect extends AbstractMapper 'id' => 'sub', ]; - protected function _link(): string { + protected function _link(): string + { // no way to retrieve the public url from the users profile return 'https://www.linkedin.com'; From 7df2287cc65e0af804b7ed9e1098a18b143704e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Gonz=C3=A1lez?= Date: Thu, 7 Sep 2023 13:22:00 +0100 Subject: [PATCH 05/11] #openid-connect-linkedin update docs --- Docs/Documentation/Social.md | 13 +++++++------ config/auth.php | 12 ++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/Docs/Documentation/Social.md b/Docs/Documentation/Social.md index c63d374..e5da800 100644 --- a/Docs/Documentation/Social.md +++ b/Docs/Documentation/Social.md @@ -1,7 +1,7 @@ Social Layer ============ The social layer provide a easier way to handle social provider authentication -with provides using OAuth1 or OAuth2. The idea is to provide a base +with provides using OAuth1 or OAuth2. The idea is to provide a base interface for both OAuth and OAuth2. ***Make sure to load the bootstap.php file of this plugin!*** @@ -12,9 +12,10 @@ We have mappers to allow you a quick start with these providers: - Facebook - Google - Instagram -- LinkedIn +- LinkedIn (Deprecated, they switched to OpenID-Connect) +- LinkedInOpenIDConnect (New, OIDC based authentication) - Pinterest -- Tumblr +- Tumblr - Twitter You must define 'options.redirectUri', 'options.clientId' and @@ -57,7 +58,7 @@ use CakeDC\Auth\Social\Service\ServiceFactory; ->getAuthorizationUrl($this->request) ); } - + /** * Callback to get user information from provider * @@ -80,7 +81,7 @@ use CakeDC\Auth\Social\Service\ServiceFactory; } $data = $server->getUser($this->request); $data = (new MapUser())($server, $data); - + //your code } catch (\Exception $e) { $this->log($log); @@ -92,4 +93,4 @@ Working with cakephp/authentication If you're using the new cakephp/authentication we recommend you to use the SocialAuthenticator and SocialMiddleware provided in this plugin. For more details of how to handle social authentication with cakephp/authentication, please check -how we implemented at CakeDC/Users plugins. \ No newline at end of file +how we implemented at CakeDC/Users plugins. diff --git a/config/auth.php b/config/auth.php index 82fbef5..7d0a460 100644 --- a/config/auth.php +++ b/config/auth.php @@ -35,6 +35,7 @@ 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/twitter', ] ], + // Deprecated, LinkedIn switched to OpenID-Connect and OAuth2 is no longer working properly 'linkedIn' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', 'className' => 'League\OAuth2\Client\Provider\LinkedIn', @@ -45,6 +46,17 @@ 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedIn', ] ], + 'linkedInOpenIDConnect' => [ + 'service' => 'CakeDC\Auth\Social\Service\OpenIDConnectService', + 'className' => 'League\OAuth2\Client\Provider\LinkedIn', + 'mapper' => 'CakeDC\Auth\Social\Mapper\LinkedInOpenIDConnect', + 'options' => [ + 'redirectUri' => Router::fullBaseUrl() . '/auth/linkedInOpenIDConnect', + 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/linkedInOpenIDConnect', + 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedInOpenIDConnect', + 'defaultScopes' => ['email', 'openid', 'profile'], + ], + ], 'instagram' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', 'className' => 'League\OAuth2\Client\Provider\Instagram', From 978370b126cd06bcfd0cecd8994fd9cc1aad4a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Gonz=C3=A1lez?= Date: Thu, 7 Sep 2023 13:22:01 +0100 Subject: [PATCH 06/11] fix cs --- config/auth.php | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/config/auth.php b/config/auth.php index 7d0a460..94d3cf3 100644 --- a/config/auth.php +++ b/config/auth.php @@ -10,6 +10,7 @@ */ use Cake\Routing\Router; + return [ 'OAuth.path' => ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialLogin', 'prefix' => null], 'OAuth.providers' => [ @@ -23,7 +24,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/facebook', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/facebook', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/facebook', - ] + ], ], 'twitter' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth1Service', @@ -33,7 +34,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/twitter', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/twitter', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/twitter', - ] + ], ], // Deprecated, LinkedIn switched to OpenID-Connect and OAuth2 is no longer working properly 'linkedIn' => [ @@ -44,7 +45,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/linkedIn', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/linkedIn', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedIn', - ] + ], ], 'linkedInOpenIDConnect' => [ 'service' => 'CakeDC\Auth\Social\Service\OpenIDConnectService', @@ -65,7 +66,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/instagram', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/instagram', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/instagram', - ] + ], ], 'google' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -76,7 +77,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/google', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/google', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/google', - ] + ], ], 'amazon' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -86,7 +87,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/amazon', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/amazon', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/amazon', - ] + ], ], 'azure' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -96,7 +97,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/azure', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/azure', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/azure', - ] + ], ], ], 'OneTimePasswordAuthenticator' => [ @@ -118,7 +119,7 @@ // QR-code provider (more on this later) 'qrcodeprovider' => null, // Random Number Generator provider (more on this later) - 'rngprovider' => null + 'rngprovider' => null, ], 'U2f' => [ 'enabled' => false, @@ -128,7 +129,7 @@ 'controller' => 'Users', 'action' => 'u2f', 'prefix' => false, - ] + ], ], 'Webauthn2fa' => [ 'enabled' => false, @@ -140,6 +141,6 @@ 'controller' => 'Users', 'action' => 'webauthn2fa', 'prefix' => false, - ] - ] + ], + ], ]; From 8ee81454a4c8cc063cc831c48e0cd42718ef02aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Gonz=C3=A1lez?= Date: Thu, 7 Sep 2023 13:32:47 +0100 Subject: [PATCH 07/11] #openid-connect-linkedin add firebase requirement for OIDC --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f308a6c..3a049ff 100644 --- a/composer.json +++ b/composer.json @@ -44,7 +44,8 @@ "cakephp/cakephp-codesniffer": "^4.0", "cakephp/authentication": "^2.0", "yubico/u2flib-server": "^1.0", - "php-coveralls/php-coveralls": "^2.4" + "php-coveralls/php-coveralls": "^2.4", + "firebase/php-jwt": "^v6.8" }, "suggest": { }, From a54200a8e04c9f1e985b6756045a6b26a5748e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Gonz=C3=A1lez?= Date: Tue, 12 Sep 2023 15:49:52 +0100 Subject: [PATCH 08/11] #openid-connect-linkedin update ci workflow --- .github/workflows/ci.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 944cf05..ad8f452 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,11 @@ on: jobs: testsuite: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: - php-version: ['7.3', '7.4', '8.0'] + php-version: ['7.3', '7.4', '8.0', '8.1', '8.2'] db-type: [sqlite, mysql, pgsql] prefer-lowest: [''] @@ -24,7 +24,7 @@ jobs: if: matrix.db-type == 'pgsql' run: docker run --rm --name=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=cakephp -p 5432:5432 -d postgres - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -43,7 +43,7 @@ jobs: run: echo "::set-output name=date::$(date +'%Y-%m')" - name: Cache composer dependencies - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} @@ -57,7 +57,7 @@ jobs: fi - name: Setup problem matchers for PHPUnit - if: matrix.php-version == '7.4' && matrix.db-type == 'mysql' + if: matrix.php-version == '8.1' && matrix.db-type == 'mysql' run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Run PHPUnit @@ -65,27 +65,27 @@ jobs: if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export DB_URL='sqlite:///:memory:'; fi if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp'; fi if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi - if [[ ${{ matrix.php-version }} == '7.4' ]]; then - export CODECOVERAGE=1 && vendor/bin/phpunit --verbose --coverage-clover=coverage.xml + if [[ ${{ matrix.php-version }} == '8.1' ]]; then + export CODECOVERAGE=1 && vendor/bin/phpunit --stderr --verbose --coverage-clover=coverage.xml else - vendor/bin/phpunit + vendor/bin/phpunit --stderr fi - name: Submit code coverage - if: matrix.php-version == '7.4' - uses: codecov/codecov-action@v1 + if: matrix.php-version == '8.1' + uses: codecov/codecov-action@v3 cs-stan: name: Coding Standard & Static Analysis - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' extensions: mbstring, intl, apcu, memcached, redis tools: cs2pr coverage: none @@ -99,7 +99,7 @@ jobs: run: echo "::set-output name=date::$(date +'%Y-%m')" - name: Cache composer dependencies - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }} From 4e994117611f5a62d02ba430b555c667c3501eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Gonz=C3=A1lez?= Date: Tue, 12 Sep 2023 15:53:29 +0100 Subject: [PATCH 09/11] update php min version to 7.4 to match requirement by cakephp/cakephp --- .github/workflows/ci.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad8f452..8fe329f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - php-version: ['7.3', '7.4', '8.0', '8.1', '8.2'] + php-version: ['7.4', '8.0', '8.1', '8.2'] db-type: [sqlite, mysql, pgsql] prefer-lowest: [''] diff --git a/composer.json b/composer.json index 3a049ff..ca5872c 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ }, "-minimum-stability": "dev", "require": { - "php": ">=7.2.0", + "php": ">=7.4.0", "cakephp/cakephp": "^4.3" }, "require-dev": { From d3f825e1a40d9b8dacc59d088efaf907deadc6c5 Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Thu, 14 Sep 2023 15:29:37 -0400 Subject: [PATCH 10/11] feat: tests for OpenID. WIP --- .../Mapper/LinkedInOpenIDConnectTest.php | 81 +++++ .../Service/OpenIDConnectServiceTest.php | 280 ++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 tests/TestCase/Social/Mapper/LinkedInOpenIDConnectTest.php create mode 100644 tests/TestCase/Social/Service/OpenIDConnectServiceTest.php diff --git a/tests/TestCase/Social/Mapper/LinkedInOpenIDConnectTest.php b/tests/TestCase/Social/Mapper/LinkedInOpenIDConnectTest.php new file mode 100644 index 0000000..5e95926 --- /dev/null +++ b/tests/TestCase/Social/Mapper/LinkedInOpenIDConnectTest.php @@ -0,0 +1,81 @@ + 'test-token', + 'expires' => 1490988496, + ]); + $rawData = [ + 'sub' => '1', + 'token' => $token, + 'email' => 'test@gmail.com', + 'given_name' => 'Test', + 'family_name' => 'User', + 'industry' => 'Computer Software', + 'location' => [ + 'country' => [ + 'code' => 'es', + ], + 'name' => 'Spain', + ], + 'picture' => 'https://media.licdn.com/mpr/mprx/test.jpg', + + + 'bio' => 'The best test user in the world.', + 'publicProfileUrl' => 'https://www.linkedin.com/in/test', + ]; + $providerMapper = new LinkedInOpenIDConnect(); + $user = $providerMapper($rawData); + + $this->assertEquals([ + 'id' => '1', + 'username' => null, + 'full_name' => null, + 'first_name' => 'Test', + 'last_name' => 'User', + 'email' => 'test@gmail.com', + 'avatar' => 'https://media.licdn.com/mpr/mprx/test.jpg', + 'gender' => null, + 'link' => 'https://www.linkedin.com', + 'bio' => 'The best test user in the world.', + 'locale' => null, + 'validated' => true, + 'credentials' => [ + 'token' => 'test-token', + 'secret' => null, + 'expires' => 1490988496, + ], + 'raw' => $rawData, + ], $user); + } +} diff --git a/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php b/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php new file mode 100644 index 0000000..1d22ffc --- /dev/null +++ b/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php @@ -0,0 +1,280 @@ +Provider = $this->getMockBuilder( + \League\OAuth2\Client\Provider\LinkedIn::class + )->setConstructorArgs([ + [ + 'id' => '1', + 'firstName' => 'first', + 'lastName' => 'last', + ], + [], + ])->setMethods([ + 'getAccessToken', 'getState', 'getAuthorizationUrl', 'getResourceOwner', + ])->getMock(); + + $config = [ + 'service' => OpenIDConnectService::class, + 'className' => $this->Provider, + 'mapper' => \CakeDC\Auth\Social\Mapper\LinkedInOpenIDConnect::class, + 'authParams' => ['scope' => ['public_profile', 'email', 'user_birthday', 'user_gender', 'user_link']], + 'options' => [ + 'state' => '__TEST_STATE__', + ], + 'collaborators' => [], + 'signature' => null, + 'mapFields' => [], + 'path' => [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'socialLogin', + 'prefix' => null, + ], + ]; + + $this->Service = $this->getMockBuilder( + \CakeDC\Auth\Social\Service\OpenIDConnectService::class + )->setConstructorArgs([ + $config, + ])->onlyMethods([ + 'getIdTokenKeys', + ])->getMock(); + + //new OpenIDConnectService($config); + $this->Request = ServerRequestFactory::fromGlobals(); + } + + /** + * teardown any static object changes and restore them. + * + * @return void + */ + public function tearDown(): void + { + parent::tearDown(); + + unset($this->Provider, $this->Service, $this->Request); + } + + /** + * Test construct + * + * @return void + */ + public function testConstruct() + { + $service = new OpenIDConnectService([ + 'className' => \League\OAuth2\Client\Provider\LinkedIn::class, + 'mapper' => \CakeDC\Auth\Social\Mapper\LinkedInOpenIDConnect::class, + 'authParams' => ['scope' => ['public_profile', 'email', 'user_birthday', 'user_gender', 'user_link']], + 'options' => [], + 'collaborators' => [], + 'signature' => null, + 'mapFields' => [], + 'path' => [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'socialLogin', + 'prefix' => null, + ], + ]); + $this->assertInstanceOf(ServiceInterface::class, $service); + } + + /** + * Test GetUser InvalidRequest + * + * @return void + */ + public function testGetUserInvalidRequest() + { + $this->Request = $this->Request->withQueryParams([ + 'code' => 'ZPO9972j3092304230', + 'state' => '__TEST_STATE__', + ]); + + $this->expectException(BadRequestException::class, 'Invalid OAuth2 state'); + + $this->Service->getUser($this->Request); + } + + /** + * Test GetUser MisingIdToken + * + * @return void + */ + public function testGetUserMisingIdToken() + { + $this->Request = $this->Request->withQueryParams([ + 'code' => 'ZPO9972j3092304230', + 'state' => '__TEST_STATE__', + ]); + + $this->Request->getSession()->write('oauth2state', '__TEST_STATE__'); + + $token = new \League\OAuth2\Client\Token\AccessToken([ + 'access_token' => 'test-token', + 'expires' => 1490988496, + ]); + + $this->Provider->expects($this->once()) + ->method('getAccessToken') + ->with( + $this->equalTo('authorization_code'), + $this->equalTo(['code' => 'ZPO9972j3092304230']) + ) + ->will($this->returnValue($token)); + + $this->expectException(BadRequestException::class, 'Missing id_token in response'); + + $this->Service->getUser($this->Request); + } + + /** + * Test GetUser InvalidIdToken + * + * @return void + */ + public function testGetUserInvalidIdToken() + { + $this->Request = $this->Request->withQueryParams([ + 'code' => 'ZPO9972j3092304230', + 'state' => '__TEST_STATE__', + ]); + + $this->Request->getSession()->write('oauth2state', '__TEST_STATE__'); + + $token = new \League\OAuth2\Client\Token\AccessToken([ + 'access_token' => 'test-token', + 'expires' => 1490988496, + 'id_token' => 'invalid-jwt', + ]); + + $this->Provider->expects($this->once()) + ->method('getAccessToken') + ->with( + $this->equalTo('authorization_code'), + $this->equalTo(['code' => 'ZPO9972j3092304230']) + ) + ->will($this->returnValue($token)); + + $this->expectException(BadRequestException::class, 'Invalid id token. Key may not be empty'); + + $this->Service->getUser($this->Request); + } + + /** + * Test GetUser + * + * @return void + */ + public function testGetUser() + { + $this->Request = $this->Request->withQueryParams([ + 'code' => 'ZPO9972j3092304230', + 'state' => '__TEST_STATE__', + ]); + + $this->Request->getSession()->write('oauth2state', '__TEST_STATE__'); + + $key = 'example_key'; + $alg = 'HS256'; + $payload = [ + 'iat' => 1490988496, + 'iss' => 'https://www.linkedin.com/', + ]; + $kid = 'd929668a-bab1-4c69-9598-4373149723ff'; + $jwt = JWT::encode($payload, $key, $alg, $kid); + + $token = new \League\OAuth2\Client\Token\AccessToken([ + 'access_token' => 'test-token', + 'expires' => 1490988496, + 'id_token' => $jwt, + ]); + + $this->Provider->expects($this->once()) + ->method('getAccessToken') + ->with( + $this->equalTo('authorization_code'), + $this->equalTo(['code' => 'ZPO9972j3092304230']) + ) + ->will($this->returnValue($token)); + + $jwksData = [ + $kid => new Key($key, $alg), + ]; + + $this->Service->expects($this->once()) + ->method('getIdTokenKeys') + ->will($this->returnValue($jwksData)); + + $actual = $this->Service->getUser($this->Request); + + $expected = [ + 'token' => $token, + 'iat' => 1490988496, + 'iss' => 'https://www.linkedin.com/', + ]; + + $this->assertEquals($expected, $actual); + } +} From ad6e98b127692bdf3f4c4a71c91eb321de10d568 Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Sat, 30 Sep 2023 13:26:21 -0400 Subject: [PATCH 11/11] test: add test --- src/Social/Service/OpenIDConnectService.php | 9 ++++- .../Service/OpenIDConnectServiceTest.php | 37 ++++++++++++++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/Social/Service/OpenIDConnectService.php b/src/Social/Service/OpenIDConnectService.php index 3cac93f..f797168 100644 --- a/src/Social/Service/OpenIDConnectService.php +++ b/src/Social/Service/OpenIDConnectService.php @@ -63,7 +63,7 @@ protected function getIdTokenKeys(): array $this->getConfig('openid.baseUrl') ); } - $client = new Client(); + $client = $this->getHttpClient(); $jwksData = $client->get($jwksUri)->getJson(); if (!$jwksData) { throw new BadRequestException( @@ -77,8 +77,13 @@ protected function getIdTokenKeys(): array public function discover(): array { $openidUrl = $this->getConfig('openid.url'); - $client = new Client(); + $client = $this->getHttpClient(); return $client->get($openidUrl)->getJson(); } + + protected function getHttpClient(): Client + { + return new Client(); + } } diff --git a/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php b/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php index 1d22ffc..14a56d6 100644 --- a/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php +++ b/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php @@ -34,7 +34,7 @@ class OpenIDConnectServiceTest extends TestCase { /** - * @var \CakeDC\Auth\Social\Service\OAuth2Service + * @var \CakeDC\Auth\Social\Service\OpenIDConnectService */ public $Service; @@ -48,6 +48,11 @@ class OpenIDConnectServiceTest extends TestCase */ public $Request; + /** + * @var \Cake\Http\Client + */ + public $Client; + /** * Setup the test case, backup the static object values so they can be restored. * Specifically backs up the contents of Configure and paths in App if they have @@ -59,6 +64,12 @@ public function setUp(): void { parent::setUp(); + $this->Client = $this->getMockBuilder( + \Cake\Http\Client::class + )->onlyMethods([ + 'get', + ])->getMock(); + $this->Provider = $this->getMockBuilder( \League\OAuth2\Client\Provider\LinkedIn::class )->setConstructorArgs([ @@ -97,8 +108,13 @@ public function setUp(): void $config, ])->onlyMethods([ 'getIdTokenKeys', + 'getHttpClient', ])->getMock(); + $this->Service->expects($this->any()) + ->method('getHttpClient') + ->will($this->returnValue($this->Client)); + //new OpenIDConnectService($config); $this->Request = ServerRequestFactory::fromGlobals(); } @@ -277,4 +293,23 @@ public function testGetUser() $this->assertEquals($expected, $actual); } + + /** + * Test discover + * + * @return void + */ + public function testDiscover() + { + $arrayTest = ['test' => 'test']; + $response = new \Cake\Http\Client\Response([], json_encode($arrayTest)); + + $this->Client->expects($this->once()) + ->method('get') + ->will($this->returnValue($response)); + + $actual = $this->Service->discover(); + + $this->assertEquals($arrayTest, $actual); + } }