diff --git a/classes/Locator.php b/classes/Locator.php index 75e183d2594ff42e145816831f9614dba5b162e0..826afd3694968d5b21776ed023a7c92d8d329808 100644 --- a/classes/Locator.php +++ b/classes/Locator.php @@ -114,6 +114,10 @@ class Locator { return $this->auditLogger; } + public function getJWKSHandler(): JWKSHandler { + return new JWKSHandler($this); + } + private function setupLoggers() { // Monolog require timezone to be set date_default_timezone_set($this->config->timeZone); diff --git a/classes/OAuth2RequestHandler.php b/classes/OAuth2RequestHandler.php index bdee406f5070014dba03239a4c99702753012758..08c996768bbc436996a5b47dedcb0ff9c4bfb158 100644 --- a/classes/OAuth2RequestHandler.php +++ b/classes/OAuth2RequestHandler.php @@ -94,7 +94,7 @@ class OAuth2RequestHandler { $result = []; $result['access_token'] = $accessToken->token; $result['token_type'] = 'Bearer'; - $result['expires_in'] = $this->getExpiresIn($accessToken); + $result['expires_in'] = $accessToken->expirationTime - time(); if ($accessToken->scope !== null && in_array('openid', $accessToken->scope)) { $result['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken); @@ -125,15 +125,11 @@ class OAuth2RequestHandler { public function handleCheckTokenRequest($token): array { - if (!isset($_POST['token'])) { - throw new BadRequestException("Access token id is required"); - } - $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($token); $user = $this->locator->getUserDAO()->findUserById($accessToken->userId); $result = []; - $result['exp'] = $this->getExpiresIn($accessToken); + $result['exp'] = $accessToken->expirationTime - time(); $result['user_name'] = $user->id; $result['client_id'] = $accessToken->clientId; @@ -147,12 +143,6 @@ class OAuth2RequestHandler { return $result; } - private function getExpiresIn(AccessToken $accessToken) { - $expTime = strtotime($accessToken->expirationTime); - $now = time(); - return $expTime - $now; - } - public function validateToken(): void { $headers = apache_request_headers(); diff --git a/classes/datalayer/JWKSDAO.php b/classes/datalayer/JWKSDAO.php index 54b0ce011e95dec26c1a2598addda7b0256af1fd..9cd83f49900a9cfccd8cbadc77caa4116da2e0f2 100644 --- a/classes/datalayer/JWKSDAO.php +++ b/classes/datalayer/JWKSDAO.php @@ -10,5 +10,5 @@ interface JWKSDAO { public function insertRSAKeyPair(RSAKeyPair $keyPair): RSAKeyPair; - public function getNewestKeyPair(): RSAKeyPair; + public function getNewestKeyPair(): ?RSAKeyPair; } diff --git a/classes/datalayer/mysql/MySQLAccessTokenDAO.php b/classes/datalayer/mysql/MySQLAccessTokenDAO.php index ee55315db4f728eb682f82e198d817af5a75ef08..28c4fad2bdcaac49d065472a3db20848cd05eba2 100644 --- a/classes/datalayer/mysql/MySQLAccessTokenDAO.php +++ b/classes/datalayer/mysql/MySQLAccessTokenDAO.php @@ -13,7 +13,7 @@ class MySQLAccessTokenDAO extends BaseMySQLDAO implements AccessTokenDAO { $dbh = $this->getDBHandler(); $stmt = $dbh->prepare("INSERT INTO access_token (token, code, user_id, redirect_uri, client_id, scope, expiration_time)" . " VALUES(:token, :code, :user_id, :redirect_uri, :client_id, :scope, " - . " TIMESTAMPADD(HOUR, 1, CURRENT_TIMESTAMP))"); + . " UNIX_TIMESTAMP(TIMESTAMPADD(HOUR, 1, CURRENT_TIMESTAMP)))"); $scope = null; if ($accessToken->scope !== null) { @@ -30,7 +30,6 @@ class MySQLAccessTokenDAO extends BaseMySQLDAO implements AccessTokenDAO { ); if ($stmt->execute($params)) { - $accessToken->expired = false; return $accessToken; } else { error_log($stmt->errorInfo()[2]); @@ -44,8 +43,8 @@ class MySQLAccessTokenDAO extends BaseMySQLDAO implements AccessTokenDAO { // Access token can be retrieved from code in 1 minute from the creation $stmt = $dbh->prepare("SELECT token, code, user_id, redirect_uri, client_id, creation_time, expiration_time, scope," - . " (expiration_time < CURRENT_TIMESTAMP) AS expired " - . " FROM access_token WHERE code = :code AND CURRENT_TIMESTAMP < TIMESTAMPADD(MINUTE, 1, creation_time)"); + . " (expiration_time < UNIX_TIMESTAMP()) AS expired " + . " FROM access_token WHERE code = :code AND UNIX_TIMESTAMP() < (creation_time + 60)"); $stmt->bindParam(':code', $code); $stmt->execute(); @@ -63,7 +62,7 @@ class MySQLAccessTokenDAO extends BaseMySQLDAO implements AccessTokenDAO { $dbh = $this->getDBHandler(); $stmt = $dbh->prepare("SELECT token, code, user_id, redirect_uri, client_id, creation_time, expiration_time, scope," - . " (expiration_time < CURRENT_TIMESTAMP) AS expired " + . " (expiration_time < UNIX_TIMESTAMP()) AS expired " . " FROM access_token WHERE token = :token"); $stmt->bindParam(':token', $token); diff --git a/classes/datalayer/mysql/MySQLJWKSDAO.php b/classes/datalayer/mysql/MySQLJWKSDAO.php index 4d6c713bbda6430531c63cd2bc7362bedf009a13..8e7a83ad188340b257950396055f12ceee49ec17 100644 --- a/classes/datalayer/mysql/MySQLJWKSDAO.php +++ b/classes/datalayer/mysql/MySQLJWKSDAO.php @@ -60,7 +60,7 @@ class MySQLJWKSDAO extends BaseMySQLDAO implements JWKSDAO { return null; } - public function getNewestKeyPair(): RSAKeyPair { + public function getNewestKeyPair(): ?RSAKeyPair { $dbh = $this->getDBHandler(); $query = "SELECT id, private_key, public_key, alg, creation_time FROM rsa_keypairs ORDER BY creation_time DESC LIMIT 1"; @@ -68,8 +68,11 @@ class MySQLJWKSDAO extends BaseMySQLDAO implements JWKSDAO { $stmt = $dbh->prepare($query); $stmt->execute(); - $row = $stmt->fetch(); - return $this->getRSAKeyPairFromResultRow($row); + foreach ($stmt->fetchAll() as $row) { + return $this->getRSAKeyPairFromResultRow($row); + } + + return null; } private function getRSAKeyPairFromResultRow(array $row): RSAKeyPair { diff --git a/include/admin.php b/include/admin.php index 06f6f87bc20e147e5b93bb087c6dc2b93153c388..2c344df4e6de47016091e119e7b7e88d06ef3647 100644 --- a/include/admin.php +++ b/include/admin.php @@ -21,10 +21,10 @@ function checkUser() { Flight::route('GET /admin', function() { checkUser(); - + global $VERSION; Flight::render('admin/index.php', array('title' => 'Admin panel', - 'version' => $VERSION)); + 'version' => $VERSION)); }); Flight::route('GET /admin/oauth2_clients', function() { @@ -68,6 +68,17 @@ Flight::route('DELETE /admin/oauth2_clients/@id', function($id) { Flight::halt(204); }); +Flight::route('POST /admin/keypair', function() { + + checkUser(); + global $locator; + + $keyPair = $locator->getJWKSHandler()->generateKeyPair(); + Flight::json([ + "id" => $keyPair->keyId + ]); +}); + function buildOAuth2ClientFromData() { $data = Flight::request()->data; diff --git a/include/front-controller.php b/include/front-controller.php index f65a202e2b7704da815bf912a54a974308d8d131..d47c05b809afe1192edc9368477559cce6c6e3b6 100644 --- a/include/front-controller.php +++ b/include/front-controller.php @@ -110,6 +110,10 @@ Flight::route('POST /auth/oauth2/check_token', function() { $token = filter_input(INPUT_POST, 'token', FILTER_SANITIZE_STRING); + if ($token === null) { + throw new BadRequestException("Access token id is required"); + } + $requestHandler = new \RAP\OAuth2RequestHandler($locator); $result = $requestHandler->handleCheckTokenRequest($token); diff --git a/sql/setup-database.sql b/sql/setup-database.sql index 4d3bf6b89ab8a4bacf3bfa3e7489049f44a72fce..f561c33d17affd003e7698451a6573c086986e05 100644 --- a/sql/setup-database.sql +++ b/sql/setup-database.sql @@ -47,8 +47,8 @@ CREATE TABLE `access_token` ( `token` text NOT NULL, `user_id` text NOT NULL, `code` text NOT NULL, - `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `expiration_time` timestamp, + `creation_time` BIGINT NOT NULL DEFAULT UNIX_TIMESTAMP(), + `expiration_time` BIGINT, `redirect_uri` text, `client_id` varchar(255), `scope` text, @@ -60,7 +60,7 @@ CREATE TABLE `rsa_keypairs` ( `public_key` text, `private_key` text, `alg` varchar(255), - `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `creation_time` BIGINT NOT NULL DEFAULT UNIX_TIMESTAMP(), PRIMARY KEY (`id`) ); diff --git a/tests/OAuth2RequestHandlerTest.php b/tests/OAuth2RequestHandlerTest.php index 216c4d17c1674b0732e0ccb54535ecde3b51db0f..ec58c2fd8db410c554fcbd0e65b43a38e7382970 100644 --- a/tests/OAuth2RequestHandlerTest.php +++ b/tests/OAuth2RequestHandlerTest.php @@ -68,26 +68,38 @@ final class OAuth2RequestHandlerTest extends TestCase { $requestHandler->handleAuthorizeRequest($params); } - public function testExpiresIn(): void { - - $locatorStub = $this->createMock(\RAP\Locator::class); - $requestHandler = new \RAP\OAuth2RequestHandler($locatorStub); + public function testHandleCheckTokenRequest(): void { $accessToken = new \RAP\AccessToken(); + $accessToken->clientId = 'my-client'; + $accessToken->scope = ['openid', 'email']; + $accessToken->userId = '123'; + $accessToken->expirationTime = time() + 3600; + + $tokenDaoStub = $this->createMock(\RAP\AccessTokenDAO::class); + $tokenDaoStub->method('getAccessToken')->willReturn($accessToken); - $expDate = new \DateTime(); - $expDate->add(new \DateInterval('PT1H')); + $user = new \RAP\User(); + $user->id = '123'; + $userDaoStub = $this->createMock(\RAP\UserDAO::class); + $userDaoStub->method('findUserById')->willReturn($user); - $accessToken->expirationTime = $expDate->format("Y-m-d H:i:s"); + $idTokenBuilderStub = $this->createMock(\RAP\IdTokenBuilder::class); + $idTokenBuilderStub->method('getIdToken')->willReturn('id-token'); - // testing private method using reflection - $reflection = new \ReflectionClass(get_class($requestHandler)); - $method = $reflection->getMethod('getExpiresIn'); - $method->setAccessible(true); + $locatorStub = $this->createMock(\RAP\Locator::class); + $locatorStub->method('getAccessTokenDAO')->willReturn($tokenDaoStub); + $locatorStub->method('getUserDAO')->willReturn($userDaoStub); + $locatorStub->method('getIdTokenBuilder')->willReturn($idTokenBuilderStub); + + $requestHandler = new \RAP\OAuth2RequestHandler($locatorStub); - $exp = $method->invokeArgs($requestHandler, [$accessToken]); + $result = $requestHandler->handleCheckTokenRequest('abc'); - $this->assertEquals(3600, $exp); + $this->assertEquals(3600, $result['exp']); + $this->assertEquals('123', $result['user_name']); + $this->assertEquals('my-client', $result['client_id']); + $this->assertEquals('id-token', $result['id_token']); } }