diff --git a/composer.json b/composer.json index a3823af..b7699a9 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "php": "^7.4 || ^8.0", "bedita/php-sdk": "^2.1.0", "cakephp/cakephp": "^4.2.2", + "firebase/php-jwt": "^6.9", "cakephp/twig-view": "^1.3.0" }, "require-dev": { diff --git a/src/Authenticator/OAuth2Authenticator.php b/src/Authenticator/OAuth2Authenticator.php index 62b9c84..96986ff 100644 --- a/src/Authenticator/OAuth2Authenticator.php +++ b/src/Authenticator/OAuth2Authenticator.php @@ -22,6 +22,7 @@ use Cake\Log\LogTrait; use Cake\Routing\Router; use Cake\Utility\Hash; +use Firebase\JWT\JWT; use Psr\Http\Message\ServerRequestInterface; /** @@ -85,6 +86,11 @@ public function authenticate(ServerRequestInterface $request): ResultInterface { // extract provider from request $provider = basename($request->getUri()->getPath()); + // leeway is needed for clock skew + $leeway = (int)$this->getConfig(sprintf('providers.%s.clientOptions.jwtLeeway', $provider), 0); + if ($leeway) { + JWT::$leeway = $leeway; + } $connect = $this->providerConnect($provider, $request); if (!empty($connect[static::AUTH_URL_KEY])) { @@ -97,6 +103,7 @@ public function authenticate(ServerRequestInterface $request): ResultInterface 'provider_username' => Hash::get($connect, sprintf('user.%s', $usernameField)), 'access_token' => Hash::get($connect, 'token.access_token'), 'provider_userdata' => (array)Hash::get($connect, 'user'), + 'id_token' => Hash::get($connect, 'token.id_token'), ]; $user = $this->_identifier->identify($data); @@ -119,7 +126,11 @@ protected function providerConnect(string $provider, ServerRequestInterface $req { $this->initProvider($provider, $request); - $query = $request->getQueryParams(); + if ($request->getMethod() === 'GET') { + $query = $request->getQueryParams(); + } else { + $query = $request->getParsedBody(); + } $sessionKey = $this->getConfig('sessionKey'); /** @var \Cake\Http\Session $session */ $session = $request->getAttribute('session'); @@ -134,7 +145,10 @@ protected function providerConnect(string $provider, ServerRequestInterface $req } // Check given state against previously stored one to mitigate CSRF attack - if (empty($query['state']) || ($query['state'] !== $session->read($sessionKey))) { + if ( + (empty($query['state']) || $query['state'] !== $session->read($sessionKey)) + && $request->getMethod() === 'GET' + ) { $session->delete($sessionKey); throw new BadRequestException('Invalid state'); } diff --git a/tests/TestCase/Authenticator/OAuth2AuthenticatorTest.php b/tests/TestCase/Authenticator/OAuth2AuthenticatorTest.php index 5ac50da..d6a3541 100644 --- a/tests/TestCase/Authenticator/OAuth2AuthenticatorTest.php +++ b/tests/TestCase/Authenticator/OAuth2AuthenticatorTest.php @@ -23,6 +23,7 @@ use Cake\Http\Session; use Cake\TestSuite\TestCase; use Cake\Utility\Hash; +use Firebase\JWT\JWT; /** * {@see \BEdita\WebTools\Authenticator\OAuth2Authenticator} Test Case @@ -50,6 +51,7 @@ public function authenticateProvider(): array 'status' => Result::SUCCESS, ], [ + 'environment' => ['REQUEST_METHOD' => 'POST'], 'url' => '/ext/login/gustavo', ], [ @@ -186,4 +188,52 @@ public function getErrors(): array static::assertNotNull($result); static::assertEquals($expected['status'], $result->getStatus()); } + + /** + * Test JWT leeway config in `authenticate` method + * + * @return void + * @covers ::authenticate() + */ + public function testAuthenticateLeeway(): void + { + $identifier = new class () implements IdentifierInterface { + public function identify(array $credentials) + { + return $credentials; + } + + public function getErrors(): array + { + return []; + } + }; + $reqConfig = [ + 'url' => '/ext/login/gustavo', + ]; + $request = new ServerRequest($reqConfig); + $session = new Session(); + $session->write(Hash::get($reqConfig, 'data')); + $request = $request->withAttribute('session', $session); + + $authenticator = new OAuth2Authenticator($identifier, [ + 'urlResolver' => fn () => '', + 'providers' => [ + 'gustavo' => [ + 'class' => TestProvider::class, + 'setup' => [ + 'clientId' => '', + ], + 'clientOptions' => [ + 'jwtLeeway' => 10, + ], + ], + ], + ]); + $result = $authenticator->authenticate($request); + + static::assertNotNull($result); + static::assertEquals(Result::SUCCESS, $result->getStatus()); + static::assertEquals(JWT::$leeway, 10); + } } diff --git a/tests/TestCase/View/Helper/HtmlHelperTest.php b/tests/TestCase/View/Helper/HtmlHelperTest.php index 0d4bd8e..17cd891 100644 --- a/tests/TestCase/View/Helper/HtmlHelperTest.php +++ b/tests/TestCase/View/Helper/HtmlHelperTest.php @@ -143,11 +143,11 @@ public function metaDescriptionProvider(): array ], 'dummy description' => [ 'dummy', - '', + '', ], 'description with special chars and tags' => [ 'dummy <> & dummy', - '', + '', ], ]; } @@ -185,11 +185,11 @@ public function metaAuthorProvider(): array ], 'dummy creator' => [ 'dummy', - '', + '', ], 'creator with special chars and tags' => [ 'dummy <> & dummy', - '', + '', ], ]; } @@ -219,7 +219,7 @@ public function metaCssProvider(): array return [ 'empty docType' => [ '', - '', + '', ], 'html5 docType' => [ 'html5', @@ -266,14 +266,14 @@ public function metaGeneratorProvider(): array [ 'name' => 'Dummy', ], - '', + '', ], 'project and version' => [ [ 'name' => 'Dummy', 'version' => '1.0', ], - '', + '', ], ]; } @@ -303,7 +303,7 @@ public function metaAllProvider(): array return [ 'empty data' => [ [], - '', + '', ], 'full data' => [ [ @@ -317,7 +317,7 @@ public function metaAllProvider(): array 'version' => '2.0', ], ], - '', + '', ], ]; } @@ -356,7 +356,7 @@ public function metaOpenGraphProvider(): array 'description' => 'a dummy data for test', 'image' => 'an image', ], - '', + '', ], ]; } @@ -397,7 +397,7 @@ public function metaTwitterProvider(): array 'description' => 'a dummy data for test', 'image' => 'an image', ], - '', + '', ], ]; }