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',
],
- '',
+ '',
],
];
}