Loading classes/IdTokenBuilder.php 0 → 100644 +57 −0 Original line number Diff line number Diff line <?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; } } classes/JWKSHandler.php +53 −2 Original line number Diff line number Diff line Loading @@ -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; } } classes/Locator.php +35 −18 Original line number Diff line number Diff line Loading @@ -10,7 +10,6 @@ class Locator { public $config; private $serviceLogger; private $auditLogger; private $dao; private $session; private $version; Loading @@ -18,7 +17,6 @@ class Locator { $this->config = $config; $this->setupLoggers(); $this->setupDAO(); $this->version = file_get_contents(ROOT . '/version.txt'); } Loading @@ -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); } /** Loading Loading @@ -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'); } } } classes/MySQLDAO.php +4 −60 Original line number Diff line number Diff line Loading @@ -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"); } } class MySQLDAO extends BaseMySQLDAO implements DAO { public function findAccessToken(string $code): ?string { $dbh = $this->getDBHandler(); $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) { Loading Loading @@ -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; Loading classes/OAuth2RequestHandler.php +29 −15 Original line number Diff line number Diff line Loading @@ -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']; Loading @@ -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; Loading @@ -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 Loading @@ -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; } Loading @@ -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; } Loading Loading
classes/IdTokenBuilder.php 0 → 100644 +57 −0 Original line number Diff line number Diff line <?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; } }
classes/JWKSHandler.php +53 −2 Original line number Diff line number Diff line Loading @@ -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; } }
classes/Locator.php +35 −18 Original line number Diff line number Diff line Loading @@ -10,7 +10,6 @@ class Locator { public $config; private $serviceLogger; private $auditLogger; private $dao; private $session; private $version; Loading @@ -18,7 +17,6 @@ class Locator { $this->config = $config; $this->setupLoggers(); $this->setupDAO(); $this->version = file_get_contents(ROOT . '/version.txt'); } Loading @@ -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); } /** Loading Loading @@ -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'); } } }
classes/MySQLDAO.php +4 −60 Original line number Diff line number Diff line Loading @@ -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"); } } class MySQLDAO extends BaseMySQLDAO implements DAO { public function findAccessToken(string $code): ?string { $dbh = $this->getDBHandler(); $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) { Loading Loading @@ -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; Loading
classes/OAuth2RequestHandler.php +29 −15 Original line number Diff line number Diff line Loading @@ -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']; Loading @@ -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; Loading @@ -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 Loading @@ -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; } Loading @@ -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; } Loading