diff --git a/classes/IdTokenBuilder.php b/classes/IdTokenBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..9b358514494fdbac54ee6359d4cd23d01900ba62 --- /dev/null +++ b/classes/IdTokenBuilder.php @@ -0,0 +1,57 @@ +<?php + +namespace RAP; + +use \Firebase\JWT\JWT; + +class IdTokenBuilder { + + private $locator; + + public function __construct(Locator $locator) { + $this->locator = $locator; + } + + public function getIdToken(AccessToken $accessToken, string $alg): string { + + $head = array("alg" => $alg, "typ" => "JWT"); + + $header = base64_encode(json_encode($head)); + + $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); + + $payloadArr = $this->createPayloadArray($accessToken); + $payloadArr['kid'] = $keyPair->keyId; + + $payload = base64_encode(json_encode($payloadArr)); + + $token_value = $header . "." . $payload; + + return JWT::encode($token_value, $keyPair->privateKey, $alg); + } + + private function createPayloadArray(AccessToken $accessToken) { + + $user = $this->locator->getDAO()->findUserById($accessToken->userId); + + $payloadArr = array( + 'iss' => $this->locator->config->jwtIssuer, + 'sub' => $user->id, + 'iat' => time(), + 'exp' => time() + 120, + 'name' => $user->getCompleteName() + ); + + if (in_array("email", $accessToken->scope)) { + $payloadArr['email'] = $user->getPrimaryEmail(); + } + if (in_array("profile", $accessToken->scope)) { + $payloadArr['given_name'] = $user->getName(); + $payloadArr['family_name'] = $user->getSurname(); + $payloadArr['org'] = $user->getInstitution(); + } + + return $payloadArr; + } + +} diff --git a/classes/JWKSHandler.php b/classes/JWKSHandler.php index eb49951642fa183fb97563e663f1e88d2a0ce307..18ec90aff788992b70f3fc8b706bc883e6589683 100644 --- a/classes/JWKSHandler.php +++ b/classes/JWKSHandler.php @@ -2,13 +2,64 @@ namespace RAP; +use phpseclib\Crypt\RSA; + /** - * Manages the JWT Key Sets. + * Manages the JWT Key Sets (currently only RSA . */ class JWKSHandler { + private $locator; + + public function __construct(Locator $locator) { + $this->locator = $locator; + } + public function generateKeyPair() { - + + $rsa = new RSA(); + + $rsa->setPrivateKeyFormat(RSA::PRIVATE_FORMAT_PKCS1); + $rsa->setPublicKeyFormat(RSA::PUBLIC_FORMAT_PKCS1); + $result = $rsa->createKey(); + + $keyPair = new RSAKeyPair(); + $keyPair->alg = 'RS256'; + $keyPair->privateKey = $result['privatekey']; + $keyPair->publicKey = $result['publickey']; + $keyPair->keyId = bin2hex(random_bytes(8)); + + $dao = $this->locator->getJWKSDAO(); + $dao->insertRSAKeyPair($keyPair); + + return $keyPair; + } + + public function getJWKS() { + + $dao = $this->locator->getJWKSDAO(); + + $keyPairs = $dao->getRSAKeyPairs(); + + $jwks = []; + foreach ($keyPairs as $keyPair) { + + $publicKey = str_replace("\n", "", $keyPair->publicKey); + $publicKey = str_replace("\r", "", $publicKey); + $publicKey = str_replace('-----BEGIN RSA PUBLIC KEY-----', '', $publicKey); + $publicKey = str_replace('-----END RSA PUBLIC KEY-----', '', $publicKey); + + $jwk = []; + $jwk['kty'] = "RSA"; + $jwk['kid'] = $keyPair->id; + $jwk['use'] = "sig"; + $jwk['n'] = $publicKey; + $jwk['e'] = "AQAB"; + + array_push($jwks, $jwk); + } + + return $jwks; } } diff --git a/classes/Locator.php b/classes/Locator.php index bee9b493b244e1b0a27626ab47d59040685db9c5..a1e5942891b89601e66ad292862c684041768880 100644 --- a/classes/Locator.php +++ b/classes/Locator.php @@ -10,7 +10,6 @@ class Locator { public $config; private $serviceLogger; private $auditLogger; - private $dao; private $session; private $version; @@ -18,7 +17,6 @@ class Locator { $this->config = $config; $this->setupLoggers(); - $this->setupDAO(); $this->version = file_get_contents(ROOT . '/version.txt'); } @@ -35,23 +33,53 @@ class Locator { } public function getDAO(): DAO { - return $this->dao; + $databaseConfig = $this->config->databaseConfig; + switch ($databaseConfig->dbtype) { + case 'MySQL': + return new MySQLDAO($this); + default: + throw new \Exception($databaseConfig->dbtype . ' not supported yet'); + } + } + + public function getJWKSDAO(): JWKSDAO { + $databaseConfig = $this->config->databaseConfig; + switch ($databaseConfig->dbtype) { + case 'MySQL': + return new MySQLJWKSDAO($this); + default: + throw new \Exception($databaseConfig->dbtype . ' not supported yet'); + } } + public function getAccessTokenDAO(): AccessTokenDAO { + $databaseConfig = $this->config->databaseConfig; + switch ($databaseConfig->dbtype) { + case 'MySQL': + return new MySQLAccessTokenDAO($this); + default: + throw new \Exception($databaseConfig->dbtype . ' not supported yet'); + } + } + public function getCallbackHandler(): CallbackHandler { - return new \RAP\CallbackHandler($this); + return new CallbackHandler($this); } public function getUserHandler(): UserHandler { - return new \RAP\UserHandler($this->dao); + return new UserHandler($this->getDAO()); } public function getMailSender(): MailSender { - return new \RAP\MailSender($_SERVER['HTTP_HOST'], $this->getBasePath()); + return new MailSender($_SERVER['HTTP_HOST'], $this->getBasePath()); } public function getOAuth2RequestHandler(): OAuth2RequestHandler { - return new \RAP\OAuth2RequestHandler($this); + return new OAuth2RequestHandler($this); + } + + public function getIdTokenBuilder(): IdTokenBuilder { + return new IdTokenBuilder($this); } /** @@ -89,15 +117,4 @@ class Locator { $this->auditLogger->pushHandler(new \Monolog\Handler\StreamHandler($this->config->auditLogFile, $logLevel)); } - private function setupDAO() { - $databaseConfig = $this->config->databaseConfig; - switch ($databaseConfig->dbtype) { - case 'MySQL': - $this->dao = new \RAP\MySQLDAO($databaseConfig); - break; - default: - throw new Exception($databaseConfig->dbtype . ' not supported yet'); - } - } - } diff --git a/classes/MySQLDAO.php b/classes/MySQLDAO.php index 7dc0fd5c911d8d4f8d54280727273538bf378f17..7208695b7467cc5b9c46f6921556dcb8a3d3e218 100644 --- a/classes/MySQLDAO.php +++ b/classes/MySQLDAO.php @@ -24,69 +24,13 @@ namespace RAP; -use PDO; - /** * MySQL implementation of the DAO interface. See comments on the DAO interface. */ -class MySQLDAO implements DAO { - - private $config; - - public function __construct($config) { - $this->config = $config; - } - - public function getDBHandler() { - $connectionString = "mysql:host=" . $this->config->hostname . ";dbname=" . $this->config->dbname; - $dbh = new PDO($connectionString, $this->config->username, $this->config->password); - // For transaction errors (see https://stackoverflow.com/a/9659366/771431) - $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $dbh; - } - - public function createAccessToken(string $token, string $code, string $userId): string { - - $dbh = $this->getDBHandler(); - $stmt = $dbh->prepare("INSERT INTO access_token (token, code, user_id) VALUES(:token, :code, :user_id)"); - - $params = array( - ':token' => $token, - ':code' => $code, - ':user_id' => $userId - ); - - if ($stmt->execute($params)) { - return $token; - } else { - error_log($stmt->errorInfo()[2]); - throw new \Exception("SQL error while storing user token"); - } - } - - public function findAccessToken(string $code): ?string { - - $dbh = $this->getDBHandler(); +class MySQLDAO extends BaseMySQLDAO implements DAO { - $stmt = $dbh->prepare("SELECT token FROM access_token WHERE code = :code");// AND CURRENT_TIMESTAMP < TIMESTAMPADD(MINUTE,1,creation_time)"); - $stmt->bindParam(':code', $code); - - $stmt->execute(); - - foreach ($stmt->fetchAll() as $row) { - return $row['token']; - } - - return null; - } - - public function deleteAccessToken($token): void { - - $dbh = $this->getDBHandler(); - - $stmt = $dbh->prepare("DELETE FROM access_token WHERE token = :token"); - $stmt->bindParam(':token', $token); - $stmt->execute(); + public function __construct(Locator $locator) { + parent::__construct($locator); } public function insertIdentity(Identity $identity, $userId) { @@ -136,7 +80,7 @@ class MySQLDAO implements DAO { return $identity; } - public function findUserById($userId) { + public function findUserById(string $userId): ?User { if (!filter_var($userId, FILTER_VALIDATE_INT)) { return null; diff --git a/classes/OAuth2RequestHandler.php b/classes/OAuth2RequestHandler.php index de070dbd6477a04e329dc11f7e7dac2af4810a5e..5a3bbd92d3efa8930872c3bd20f3078a28886426 100644 --- a/classes/OAuth2RequestHandler.php +++ b/classes/OAuth2RequestHandler.php @@ -13,11 +13,11 @@ class OAuth2RequestHandler { public function handleAuthorizeRequest() { if (!isset($_REQUEST['client_id'])) { - throw new \RAP\BadRequestException("Client id is required"); + throw new BadRequestException("Client id is required"); } if (!isset($_REQUEST['redirect_uri'])) { - throw new \RAP\BadRequestException("Redirect URI is required"); + throw new BadRequestException("Redirect URI is required"); } $clientId = $_REQUEST['client_id']; @@ -25,10 +25,10 @@ class OAuth2RequestHandler { $client = $this->locator->getDAO()->getOAuth2ClientByClientId($clientId); if ($client === null) { - throw new \RAP\BadRequestException("Invalid client id: " . $clientId); + throw new BadRequestException("Invalid client id: " . $clientId); } if ($client->redirectUrl !== $redirectUrl) { - throw new \RAP\BadRequestException("Invalid client redirect URI: " . $redirectUrl); + throw new BadRequestException("Invalid client redirect URI: " . $redirectUrl); } $alg; @@ -48,7 +48,7 @@ class OAuth2RequestHandler { private function executeStateFlow(OAuth2Client $client) { if (!isset($_REQUEST['state'])) { - throw new \RAP\BadRequestException("State is required"); + throw new BadRequestException("State is required"); } // Storing OAuth2 data in session @@ -65,16 +65,20 @@ class OAuth2RequestHandler { $session = $this->locator->getSession(); - $code = base64_encode(bin2hex(openssl_random_pseudo_bytes(64))); - $accessToken = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); - $state = $session->getOAuth2Data()->state; + $accessToken = new \RAP\AccessToken(); + $accessToken->code = base64_encode(bin2hex(openssl_random_pseudo_bytes(64))); + $accessToken->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); + $accessToken->userId = $session->user->id; + $accessToken->clientId = $session->getOAuth2Data()->clientId; + $accessToken->redirectUri = $session->getOAuth2Data()->redirectUrl; + //$accessToken->scope = - $userId = $session->user->id; + $this->locator->getAccessTokenDAO()->createAccessToken($accessToken); - $this->locator->getDAO()->createAccessToken($accessToken, $code, $userId); + $state = $session->getOAuth2Data()->state; $redirectUrl = $session->getOAuth2Data()->redirectUrl - . '?code=' . $code . '&scope=profile&state=' . $state; + . '?code=' . $accessToken->code . '&scope=profile&state=' . $state; return $redirectUrl; } @@ -84,14 +88,24 @@ class OAuth2RequestHandler { $this->validateAccessTokenRequest(); $code = filter_input(INPUT_POST, 'code', FILTER_SANITIZE_STRING); - $accessToken = $this->locator->getDAO()->findAccessToken($code); - + $accessToken = $this->locator->getAccessTokenDAO()->retrieveAccessTokenFromCode($code); + + if($accessToken === null) { + throw new BadRequestException("No token for given code"); + } + $this->validateParametersMatching(); $token = []; - $token['access_token'] = $accessToken; + $token['access_token'] = $accessToken->token; $token['token_type'] = 'bearer'; $token['expires_in'] = 300; + error_log($accessToken->creationTime); + error_log($accessToken->expirationTime); + + if ($accessToken->scope !== null) { + $token['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken->userId, 'RS256'); + } return $token; } @@ -111,7 +125,7 @@ class OAuth2RequestHandler { if (!isset($_POST['redirect_uri'])) { throw new BadRequestException("Redirect URI is required"); } - + // Note: theorically the standard wants also the client_id here, // however some clients don't send it } diff --git a/classes/User.php b/classes/User.php deleted file mode 100644 index 21967ff717e1ab17a1dccb49f73912a5c3072d5b..0000000000000000000000000000000000000000 --- a/classes/User.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php - -/* ---------------------------------------------------------------------------- - * INAF - National Institute for Astrophysics - * IRA - Radioastronomical Institute - Bologna - * OATS - Astronomical Observatory - Trieste - * ---------------------------------------------------------------------------- - * - * Copyright (C) 2016 Istituto Nazionale di Astrofisica - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License Version 3 as published by the - * Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - * details. - * - * You should have received a copy of the GNU General Public License along with - * this program; if not, write to the Free Software Foundation, Inc., 51 - * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -namespace RAP; - -/** - * Data model for the user. An user is a set of identities. - */ -class User { - - // User ID - public $id; - // List of identities - public $identities; - - public function __construct() { - $this->identities = []; - } - - public function addIdentity(Identity $identity) { - array_push($this->identities, $identity); - } - - public function getPrimaryEmail() { - foreach ($this->identities as $identity) { - if ($identity->primary) { - return $identity->email; - } - } - // A primary identity MUST be defined - throw new \Exception("No primary identity defined for user " . $this->id); - } - -} diff --git a/classes/datalayer/AccessTokenDAO.php b/classes/datalayer/AccessTokenDAO.php new file mode 100644 index 0000000000000000000000000000000000000000..e9f60337e45f08a7024541b546a40a58e9874685 --- /dev/null +++ b/classes/datalayer/AccessTokenDAO.php @@ -0,0 +1,23 @@ +<?php + +namespace RAP; + +interface AccessTokenDAO { + + /** + * Store a new login token into the database. + * @param type $token login token + * @param type $userId + */ + function createAccessToken(AccessToken $accessToken): AccessToken; + + function retrieveAccessTokenFromCode(string $code): ?AccessToken; + + /** + * Delete an access token from the database. This happens when the caller + * application has received the token and used it for retrieving user + * information from the token using the RAP REST web service. + * @param type $token login token + */ + function deleteAccessToken(string $token): void; +} diff --git a/classes/DAO.php b/classes/datalayer/DAO.php similarity index 83% rename from classes/DAO.php rename to classes/datalayer/DAO.php index 54bc5ff6204aae74e7dbb8e9b0176846766b7a26..4de0886db83f7d9859c8b09d15a6664e0e065577 100644 --- a/classes/DAO.php +++ b/classes/datalayer/DAO.php @@ -30,31 +30,6 @@ namespace RAP; */ interface DAO { - /** - * @return type PDO object for accessing the database - */ - function getDBHandler(); - - /** - * Store a new login token into the database. - * @param type $token login token - * @param type $userId - */ - function createAccessToken(string $token, string $code, string $userId): string; - - /** - * Retrieve the access token value from the code. - */ - function findAccessToken(string $code): ?string; - - /** - * Delete an access token from the database. This happens when the caller - * application has received the token and used it for retrieving user - * information from the token using the RAP REST web service. - * @param type $token login token - */ - function deleteAccessToken(string $token): void; - /** * Create a new identity. * @param type $userId the user ID associated to that identity @@ -71,7 +46,7 @@ interface DAO { /** * @return RAP\User an user object, null if nothing was found. */ - function findUserById($userId); + function findUserById(string $userId): ?User; function setPrimaryIdentity($userId, $identityId); diff --git a/classes/datalayer/JWKSDAO.php b/classes/datalayer/JWKSDAO.php new file mode 100644 index 0000000000000000000000000000000000000000..c2821ea90b72ff65153f4fd525a63cea481065ea --- /dev/null +++ b/classes/datalayer/JWKSDAO.php @@ -0,0 +1,12 @@ +<?php + +namespace RAP; + +interface JWKSDAO { + + public function getRSAKeyPairs(): array; + + public function insertRSAKeyPair(RSAKeyPair $keyPair): RSAKeyPair; + + public function getNewestKeyPair(): RSAKeyPair; +} diff --git a/classes/datalayer/mysql/BaseMySQLDAO.php b/classes/datalayer/mysql/BaseMySQLDAO.php new file mode 100644 index 0000000000000000000000000000000000000000..3741c956700b258629ac435b31bbf82e5e99783f --- /dev/null +++ b/classes/datalayer/mysql/BaseMySQLDAO.php @@ -0,0 +1,27 @@ +<?php + +namespace RAP; + +use PDO; + +abstract class BaseMySQLDAO { + + private $locator; + + public function __construct(Locator $locator) { + $this->locator = $locator; + } + + /** + * @return type PDO object for accessing the database + */ + public function getDBHandler(): PDO { + $config = $this->locator->config->databaseConfig; + $connectionString = "mysql:host=" . $config->hostname . ";dbname=" . $config->dbname; + $dbh = new PDO($connectionString, $config->username, $config->password); + // For transaction errors (see https://stackoverflow.com/a/9659366/771431) + $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + return $dbh; + } + +} diff --git a/classes/datalayer/mysql/MySQLAccessTokenDAO.php b/classes/datalayer/mysql/MySQLAccessTokenDAO.php new file mode 100644 index 0000000000000000000000000000000000000000..6b90c12f9517368fe73317fd77114cbccb452da7 --- /dev/null +++ b/classes/datalayer/mysql/MySQLAccessTokenDAO.php @@ -0,0 +1,84 @@ +<?php + +namespace RAP; + +class MySQLAccessTokenDAO extends BaseMySQLDAO implements AccessTokenDAO { + + public function __construct(Locator $locator) { + parent::__construct($locator); + } + + public function createAccessToken(AccessToken $accessToken): AccessToken { + + $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))"); + + $scope = null; + if ($accessToken->scope !== null) { + $scope = join(',', $accessToken->scope); + } + + $params = array( + ':token' => $accessToken->token, + ':code' => $accessToken->code, + ':user_id' => $accessToken->userId, + ':redirect_uri' => $accessToken->redirectUri, + ':client_id' => $accessToken->clientId, + ':scope' => $scope + ); + + if ($stmt->execute($params)) { + return $accessToken; + } else { + error_log($stmt->errorInfo()[2]); + throw new \Exception("SQL error while storing user token"); + } + } + + public function retrieveAccessTokenFromCode(string $code): ?AccessToken { + + $dbh = $this->getDBHandler(); + + // 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" + . " FROM access_token WHERE code = :code AND CURRENT_TIMESTAMP < TIMESTAMPADD(MINUTE, 1, creation_time)"); + $stmt->bindParam(':code', $code); + + $stmt->execute(); + + foreach ($stmt->fetchAll() as $row) { + $token = new AccessToken(); + $token->token = $row['token']; + $token->code = $row['code']; + $token->userId = $row['user_id']; + $token->redirectUri = $row['redirect_uri']; + $token->clientId = $row['client_id']; + $token->creationTime = $row['creation_time']; + $token->expirationTime = $row['expiration_time']; + + $scope = null; + if (isset($row['scope'])) { + $scope = $row['scope']; + } + if ($scope !== null && $scope !== '') { + $token->scope = explode(',', $scope); + } + + return $token; + } + + return null; + } + + public function deleteAccessToken($token): void { + + $dbh = $this->getDBHandler(); + + $stmt = $dbh->prepare("DELETE FROM access_token WHERE token = :token"); + $stmt->bindParam(':token', $token); + $stmt->execute(); + } + +} diff --git a/classes/datalayer/mysql/MySQLJWKSDAO.php b/classes/datalayer/mysql/MySQLJWKSDAO.php new file mode 100644 index 0000000000000000000000000000000000000000..65618cac6c773dc59d95d9bf8a500cdd9c61ee48 --- /dev/null +++ b/classes/datalayer/mysql/MySQLJWKSDAO.php @@ -0,0 +1,68 @@ +<?php + +namespace RAP; + +class MySQLJWKSDAO extends BaseMySQLDAO implements JWKSDAO { + + public function __construct($config) { + parent::__construct($config); + } + + public function insertRSAKeyPair(RSAKeyPair $keyPair): RSAKeyPair { + + $dbh = $this->getDBHandler(); + + $query = "INSERT INTO rsa_keypairs(id, private_key, public_key, alg) VALUES (:id, :private_key, :public_key, :alg)"; + + $stmt = $dbh->prepare($query); + $stmt->bindParam(':id', $keyPair->keyId); + $stmt->bindParam(':private_key', $keyPair->privateKey); + $stmt->bindParam(':public_key', $keyPair->publicKey); + $stmt->bindParam(':alg', $keyPair->alg); + + $stmt->execute(); + + return $keyPair; + } + + public function getRSAKeyPairs(): array { + + $dbh = $this->getDBHandler(); + + $query = "SELECT id, private_key, public_key, alg, creation_time FROM rsa_keypairs"; + + $stmt = $dbh->prepare($query); + $stmt->execute(); + + $keyPairs = []; + foreach ($stmt->fetchAll() as $row) { + $keyPair = $this->getRSAKeyPairFromResultRow($row); + array_push($keyPairs, $keyPair); + } + + return $keyPairs; + } + + 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"; + + $stmt = $dbh->prepare($query); + $stmt->execute(); + + $row = $stmt->fetch(); + return $this->getRSAKeyPairFromResultRow($row); + } + + private function getRSAKeyPairFromResultRow(array $row): RSAKeyPair { + $keyPair = new RSAKeyPair(); + $keyPair->id = $row['id']; + $keyPair->privateKey = $row['private_key']; + $keyPair->publicKey = $row['public_key']; + $keyPair->alg = $row['alg']; + $keyPair->creationTime = $row['creation_time']; + return $keyPair; + } + +} diff --git a/classes/model/AccessToken.php b/classes/model/AccessToken.php new file mode 100644 index 0000000000000000000000000000000000000000..e7fc0f1764a272722c0aa73e676c1d23b2c75ffd --- /dev/null +++ b/classes/model/AccessToken.php @@ -0,0 +1,16 @@ +<?php + +namespace RAP; + +class AccessToken { + + public $token; + public $code; + public $userId; + public $creationTime; + public $expirationTime; + public $redirectUri; + public $clientId; + public $scope; + +} diff --git a/classes/AuthPageModel.php b/classes/model/AuthPageModel.php similarity index 100% rename from classes/AuthPageModel.php rename to classes/model/AuthPageModel.php diff --git a/classes/InternalClient.php b/classes/model/InternalClient.php similarity index 100% rename from classes/InternalClient.php rename to classes/model/InternalClient.php diff --git a/classes/OAuth2Client.php b/classes/model/OAuth2Client.php similarity index 100% rename from classes/OAuth2Client.php rename to classes/model/OAuth2Client.php diff --git a/classes/OAuth2Data.php b/classes/model/OAuth2Data.php similarity index 100% rename from classes/OAuth2Data.php rename to classes/model/OAuth2Data.php diff --git a/classes/RAPClient.php b/classes/model/RAPClient.php similarity index 100% rename from classes/RAPClient.php rename to classes/model/RAPClient.php diff --git a/classes/model/RSAKeyPair.php b/classes/model/RSAKeyPair.php new file mode 100644 index 0000000000000000000000000000000000000000..ac224585c130348be20a92f6152a8c9ac55faac5 --- /dev/null +++ b/classes/model/RSAKeyPair.php @@ -0,0 +1,12 @@ +<?php + +namespace RAP; + +class RSAKeyPair { + + public $keyId; + public $privateKey; + public $publicKey; + public $alg; + public $creationTime; +} \ No newline at end of file diff --git a/classes/SessionData.php b/classes/model/SessionData.php similarity index 100% rename from classes/SessionData.php rename to classes/model/SessionData.php diff --git a/classes/model/User.php b/classes/model/User.php new file mode 100644 index 0000000000000000000000000000000000000000..c1b11a3f24a1751e5d79a68423cc7519416768d7 --- /dev/null +++ b/classes/model/User.php @@ -0,0 +1,132 @@ +<?php + +/* ---------------------------------------------------------------------------- + * INAF - National Institute for Astrophysics + * IRA - Radioastronomical Institute - Bologna + * OATS - Astronomical Observatory - Trieste + * ---------------------------------------------------------------------------- + * + * Copyright (C) 2016 Istituto Nazionale di Astrofisica + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License Version 3 as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 51 + * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +namespace RAP; + +/** + * Data model for the user. An user is a set of identities. + */ +class User { + + // User ID + public $id; + // List of identities + public $identities; + + public function __construct() { + $this->identities = []; + } + + public function addIdentity(Identity $identity): void { + array_push($this->identities, $identity); + } + + public function getPrimaryEmail() { + foreach ($this->identities as $identity) { + if ($identity->primary) { + return $identity->email; + } + } + // A primary identity MUST be defined + throw new \Exception("No primary identity defined for user " . $this->id); + } + + /** + * Returns name and surname if they are present, preferring the primary identity data. + */ + public function getCompleteName(): ?string { + + $completeName = null; + + foreach ($this->identities as $identity) { + if ($identity->name !== null && $identity->surname !== null) { + $completeName = $identity->name . ' ' . $identity->surname; + } + if ($identity->primary && $completeName !== null) { + break; + } + } + + return $completeName; + } + + /** + * Returns the user name if it is present, preferring the primary identity data. + */ + public function getName(): ?string { + + $name = null; + + foreach ($this->identities as $identity) { + if ($identity->name !== null) { + $name = $identity->name; + } + if ($identity->primary && $name !== null) { + break; + } + } + + return $name; + } + + /** + * Returns the user surname if it is present, preferring the primary identity data. + */ + public function getSurname(): ?string { + + $surname = null; + + foreach ($this->identities as $identity) { + if ($identity->surname !== null) { + $surname = $identity->surname; + } + if ($identity->primary && $surname !== null) { + break; + } + } + + return $surname; + } + + /** + * Returns the user institution if it is present, preferring the primary identity data. + */ + public function getInstitution(): ?string { + + + $institution = null; + + foreach ($this->identities as $identity) { + if ($identity->institution !== null) { + $institution = $identity->institution; + } + if ($identity->primary && $institution !== null) { + break; + } + } + + return $institution; + } + +} diff --git a/config-example.json b/config-example.json index f73a89ee9e4d51dc28b64c5226584609c11c6e9d..36e73d850e28a665f4f4fd97bcca19645b900a01 100644 --- a/config-example.json +++ b/config-example.json @@ -4,6 +4,7 @@ "auditLogFile": "/var/www/html/rap-ia2/logs/rap-audit.log", "timeZone": "Europe/Rome", "logLevel": "DEBUG", + "jwtIssuer": "sso.ia2.inaf.it", "databaseConfig": { "dbtype": "MySQL", "hostname": "localhost", diff --git a/include/front-controller.php b/include/front-controller.php index 8ab6e953554a3b273ac026101eab13b78409ca9a..efd208d12596630a007d9779530a10cd8dfe4f5f 100644 --- a/include/front-controller.php +++ b/include/front-controller.php @@ -85,10 +85,10 @@ Flight::route('GET /auth/oidc/jwks', function() { global $locator; - $requestHandler = new \RAP\OAuth2RequestHandler($locator); - $token = $requestHandler->handleCheckTokenRequest(); + $jwksHandler = new \RAP\JWKSHandler($locator); + $jwks = $jwksHandler->getJWKS(); - Flight::json($token); + Flight::json($jwks); }); Flight::route('GET /logout', function() { diff --git a/include/init.php b/include/init.php index ca98a295f8055497aca32ec23799ff15ee54a2e1..e0c98813b73b7cacc6dbc691e3838487d65e2bc0 100644 --- a/include/init.php +++ b/include/init.php @@ -36,7 +36,7 @@ spl_autoload_register(function ($class) { $fileName = substr($class, $len, strlen($class) - $len); - $classDirectories = ['/', '/social/', '/exceptions/']; + $classDirectories = ['/', '/social/', '/exceptions/', '/datalayer/', '/model/']; foreach ($classDirectories as $directory) { $classpath = ROOT . '/classes' . $directory . $fileName . '.php'; if (file_exists($classpath)) { diff --git a/index.php b/index.php index 777c43d366a87a7484aa56c5f065e1e1a748014b..653a6ef0f2dad64ea7a895d6b0e45afd6918c2e0 100644 --- a/index.php +++ b/index.php @@ -28,9 +28,6 @@ include './include/front-controller.php'; include './include/gui-backend.php'; include './include/rest-web-service.php'; -// Starting Flight framework -Flight::start(); - // Error handling Flight::map('error', function(Exception $ex) { if ($ex instanceof \RAP\BadRequestException) { @@ -41,3 +38,6 @@ Flight::map('error', function(Exception $ex) { } }); +// Starting Flight framework +Flight::start(); + diff --git a/sql/setup-database.sql b/sql/setup-database.sql index 11f927025053b39e7f559d800680e860ab05f59a..a390e5a521568c6804c5fda198a4c9e0da19a691 100644 --- a/sql/setup-database.sql +++ b/sql/setup-database.sql @@ -48,6 +48,10 @@ CREATE TABLE `access_token` ( `user_id` text NOT NULL, `code` text NOT NULL, `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expiration_time` timestamp, + `redirect_uri` text, + `client_id` varchar(255), + `scope` text, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; @@ -62,6 +66,15 @@ CREATE TABLE `join_request` ( FOREIGN KEY (`target_user_id`) REFERENCES `user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE `rsa_keypairs` ( + `id` varchar(255) NOT NULL, + `public_key` text, + `private_key` text, + `alg` varchar(255), + `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + CREATE EVENT login_tokens_cleanup ON SCHEDULE EVERY 1 MINUTE diff --git a/tests/IdTokenBuilderTest.php b/tests/IdTokenBuilderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..015617ca1ef2aaab5634c2d76c07a69e2ade3358 --- /dev/null +++ b/tests/IdTokenBuilderTest.php @@ -0,0 +1,45 @@ +<?php + +use PHPUnit\Framework\TestCase; + +final class IdTokenBuilderTest extends TestCase { + + public function testJWTCreation() { + + $jwksDAOStub = $this->createMock(\RAP\JWKSDAO::class); + + $locatorStub = $this->createMock(\RAP\Locator::class); + $locatorStub->method('getJWKSDAO')->willReturn($jwksDAOStub); + + $jwksHandler = new \RAP\JWKSHandler($locatorStub); + $keyPair = $jwksHandler->generateKeyPair(); + + $jwksDAOStub->method('getNewestKeyPair')->willReturn($keyPair); + + $user = new \RAP\User(); + $user->id = "user_id"; + $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN); + $identity->email = "name@inaf.it"; + $identity->name = "Name"; + $identity->surname = "Surname"; + $identity->primary = true; + $user->addIdentity($identity); + + $daoStub = $this->createMock(\RAP\DAO::class); + $locatorStub->method('getDAO')->willReturn($daoStub); + $daoStub->method('findUserById')->willReturn($user); + + $locatorStub->config = json_decode('{"jwtIssuer": "issuer"}'); + + $accessToken = new \RAP\AccessToken(); + $accessToken->token = "ttt"; + $accessToken->scope = ["email", "profile"]; + $accessToken->userId = "user_id"; + + $tokenBuilder = new \RAP\IdTokenBuilder($locatorStub); + $result = $tokenBuilder->getIdToken($accessToken, 'RS256'); + + $this->assertNotNull($result); + } + +} diff --git a/tests/JWKSHandlerTest.php b/tests/JWKSHandlerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7901dc7645ec6c1cefdac7658e3b9fe89750ef3a --- /dev/null +++ b/tests/JWKSHandlerTest.php @@ -0,0 +1,26 @@ +<?php + +use PHPUnit\Framework\TestCase; + +final class JWKSHandlerTest extends TestCase { + + public function testKeyPairCreation(): void { + + $daoStub = $this->createMock(\RAP\JWKSDAO::class); + + $locatorStub = $this->createMock(\RAP\Locator::class); + $locatorStub->method('getJWKSDAO')->willReturn($daoStub); + + $JWKSHandler = new \RAP\JWKSHandler($locatorStub); + + $daoStub->expects($this->once()) + ->method('insertRSAKeyPair')->with($this->anything()); + + $result = $JWKSHandler->generateKeyPair(); + + $this->assertNotNull($result->keyId); + $this->assertNotNull($result->privateKey); + $this->assertNotNull($result->publicKey); + } + +}