Loading classes/JWKSHandler.php +63 −0 Original line number Diff line number Diff line Loading @@ -77,4 +77,67 @@ class JWKSHandler { return $matches[1]; } public function loadAllJWKS(): array { foreach ($this->locator->config->jwksUrls as $url) { $this->loadJWKS($url); } $dao = $this->locator->getJWKSDAO(); return $dao->getAllPublicJWK(); } private function loadJWKS($url) { $dao = $this->locator->getJWKSDAO(); $conn = curl_init($url); curl_setopt($conn, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($conn, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($conn); $info = curl_getinfo($conn); if ($info['http_code'] === 200) { $jwks = json_decode($result, TRUE); foreach ($jwks['keys'] as $key) { $key['url'] = $url; $jwk = $this->getPublicJWK($key); $dao->updatePublicJWK($jwk); } } else { $errorMessage = 'Error while retrieving JWKS: ' . curl_error($conn); error_log($result); curl_close($conn); http_response_code(500); die($errorMessage); } curl_close($conn); } private function getPublicJWK($data): PublicJWK { // Convert Base64 uri-safe variant to default (needed for JWKS) $n = strtr($data['n'], '-_', '+/'); $rsa = new RSA(); $key = "<RSAKeyPair>" . "<Modulus>" . $n . "</Modulus>" . "<Exponent>" . $data['e'] . "</Exponent>" . "</RSAKeyPair>"; $rsa->loadKey($key, RSA::PUBLIC_FORMAT_XML); $jwk = new PublicJWK(); $jwk->kid = $data['kid']; $jwk->key = $rsa; $jwk->url = $data['url']; $jwk->updateTime = time(); return $jwk; } } classes/TokenBuilder.php +2 −4 Original line number Diff line number Diff line Loading @@ -101,17 +101,15 @@ class TokenBuilder { * @param int $lifespan in hours * @param string $audience target service */ public function generateNewToken(int $lifespan, string $audience) { public function generateNewToken(string $subject, int $lifespan, string $audience) { $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); $user = $this->locator->getSession()->getUser(); $iat = time(); $exp = $iat + $lifespan * 3600; $payload = array( 'iss' => $this->locator->config->jwtIssuer, 'sub' => strval($user->id), 'sub' => strval($subject), 'iat' => $iat, 'exp' => $exp, 'aud' => $audience Loading classes/TokenExchanger.php 0 → 100644 +86 −0 Original line number Diff line number Diff line <?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; use \Firebase\JWT\JWT; class TokenExchanger { private $locator; public function __construct(Locator $locator) { $this->locator = $locator; } public function exchangeToken(string $token) { $key = $this->getKeyForToken($token); $decoded = JWT::decode($token, $key->key, ['RS256']); $subject = $decoded->sub; $lifespan = ($decoded->exp - time()); $data = []; $data['access_token'] = $this->locator->getTokenBuilder()->generateNewToken($subject, $lifespan / 3600, "gms"); $data['issued_token_type'] = "urn:ietf:params:oauth:token-type:access_token"; $data['token_type'] = 'Bearer'; $data['expires_in'] = $lifespan; return $data; } private function getKeyForToken(string $token): PublicJWK { $keys = $this->locator->getJWKSDAO()->getAllPublicJWK(); $parts = explode('.', $token); $head = JWT::jsonDecode(JWT::urlsafeB64Decode($parts[0])); $kid = $head->kid; $key = $this->getKeyByKid($keys, $kid); if ($key === null) { $keys = $this->locator->getJWKSHandler()->loadAllJWKS(); } $key = $this->getKeyByKid($keys, $kid); if ($key !== null) { return $key; } throw new \Exception("Invalid kid"); } private function getKeyByKid(array $keys, string $kid): ?PublicJWK { foreach ($keys as $key) { if ($key->kid === $kid) { return $key; } } return null; } } classes/datalayer/JWKSDAO.php +4 −0 Original line number Diff line number Diff line Loading @@ -11,4 +11,8 @@ interface JWKSDAO { public function insertRSAKeyPair(RSAKeyPair $keyPair): RSAKeyPair; public function getNewestKeyPair(): ?RSAKeyPair; public function getAllPublicJWK(): array; public function updatePublicJWK(PublicJWK $jwk); } classes/datalayer/mysql/MySQLJWKSDAO.php +44 −0 Original line number Diff line number Diff line Loading @@ -85,4 +85,48 @@ class MySQLJWKSDAO extends BaseMySQLDAO implements JWKSDAO { return $keyPair; } public function getAllPublicJWK(): array { $dbh = $this->getDBHandler(); $query = "SELECT `kid`, `key`, `url`, `update_time` FROM public_jwk"; $stmt = $dbh->prepare($query); $stmt->execute(); $keys = []; foreach ($stmt->fetchAll() as $row) { array_push($keys, $this->getPublicJWKFromResultRow($row)); } return $keys; } private function getPublicJWKFromResultRow($row): PublicJWK { $jwk = new PublicJWK (); $jwk->key = $row['key']; $jwk->kid = $row['kid']; $jwk->url = $row['url']; $jwk->updateTime = $row['update_time']; return $jwk; } public function updatePublicJWK(PublicJWK $jwk) { $dbh = $this->getDBHandler(); $query = "INSERT INTO public_jwk(kid, `key`, `url`, update_time) VALUES (:kid, :key, :url, :update_time)" . " ON DUPLICATE KEY UPDATE `key`=:key, `url`=:url, update_time=:update_time"; $stmt = $dbh->prepare($query); $stmt->bindParam(':kid', $jwk->kid); $stmt->bindParam(':key', $jwk->key); $stmt->bindParam(':url', $jwk->url); $stmt->bindParam(':update_time', $jwk->updateTime); $stmt->execute(); } } Loading
classes/JWKSHandler.php +63 −0 Original line number Diff line number Diff line Loading @@ -77,4 +77,67 @@ class JWKSHandler { return $matches[1]; } public function loadAllJWKS(): array { foreach ($this->locator->config->jwksUrls as $url) { $this->loadJWKS($url); } $dao = $this->locator->getJWKSDAO(); return $dao->getAllPublicJWK(); } private function loadJWKS($url) { $dao = $this->locator->getJWKSDAO(); $conn = curl_init($url); curl_setopt($conn, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($conn, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($conn); $info = curl_getinfo($conn); if ($info['http_code'] === 200) { $jwks = json_decode($result, TRUE); foreach ($jwks['keys'] as $key) { $key['url'] = $url; $jwk = $this->getPublicJWK($key); $dao->updatePublicJWK($jwk); } } else { $errorMessage = 'Error while retrieving JWKS: ' . curl_error($conn); error_log($result); curl_close($conn); http_response_code(500); die($errorMessage); } curl_close($conn); } private function getPublicJWK($data): PublicJWK { // Convert Base64 uri-safe variant to default (needed for JWKS) $n = strtr($data['n'], '-_', '+/'); $rsa = new RSA(); $key = "<RSAKeyPair>" . "<Modulus>" . $n . "</Modulus>" . "<Exponent>" . $data['e'] . "</Exponent>" . "</RSAKeyPair>"; $rsa->loadKey($key, RSA::PUBLIC_FORMAT_XML); $jwk = new PublicJWK(); $jwk->kid = $data['kid']; $jwk->key = $rsa; $jwk->url = $data['url']; $jwk->updateTime = time(); return $jwk; } }
classes/TokenBuilder.php +2 −4 Original line number Diff line number Diff line Loading @@ -101,17 +101,15 @@ class TokenBuilder { * @param int $lifespan in hours * @param string $audience target service */ public function generateNewToken(int $lifespan, string $audience) { public function generateNewToken(string $subject, int $lifespan, string $audience) { $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); $user = $this->locator->getSession()->getUser(); $iat = time(); $exp = $iat + $lifespan * 3600; $payload = array( 'iss' => $this->locator->config->jwtIssuer, 'sub' => strval($user->id), 'sub' => strval($subject), 'iat' => $iat, 'exp' => $exp, 'aud' => $audience Loading
classes/TokenExchanger.php 0 → 100644 +86 −0 Original line number Diff line number Diff line <?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; use \Firebase\JWT\JWT; class TokenExchanger { private $locator; public function __construct(Locator $locator) { $this->locator = $locator; } public function exchangeToken(string $token) { $key = $this->getKeyForToken($token); $decoded = JWT::decode($token, $key->key, ['RS256']); $subject = $decoded->sub; $lifespan = ($decoded->exp - time()); $data = []; $data['access_token'] = $this->locator->getTokenBuilder()->generateNewToken($subject, $lifespan / 3600, "gms"); $data['issued_token_type'] = "urn:ietf:params:oauth:token-type:access_token"; $data['token_type'] = 'Bearer'; $data['expires_in'] = $lifespan; return $data; } private function getKeyForToken(string $token): PublicJWK { $keys = $this->locator->getJWKSDAO()->getAllPublicJWK(); $parts = explode('.', $token); $head = JWT::jsonDecode(JWT::urlsafeB64Decode($parts[0])); $kid = $head->kid; $key = $this->getKeyByKid($keys, $kid); if ($key === null) { $keys = $this->locator->getJWKSHandler()->loadAllJWKS(); } $key = $this->getKeyByKid($keys, $kid); if ($key !== null) { return $key; } throw new \Exception("Invalid kid"); } private function getKeyByKid(array $keys, string $kid): ?PublicJWK { foreach ($keys as $key) { if ($key->kid === $kid) { return $key; } } return null; } }
classes/datalayer/JWKSDAO.php +4 −0 Original line number Diff line number Diff line Loading @@ -11,4 +11,8 @@ interface JWKSDAO { public function insertRSAKeyPair(RSAKeyPair $keyPair): RSAKeyPair; public function getNewestKeyPair(): ?RSAKeyPair; public function getAllPublicJWK(): array; public function updatePublicJWK(PublicJWK $jwk); }
classes/datalayer/mysql/MySQLJWKSDAO.php +44 −0 Original line number Diff line number Diff line Loading @@ -85,4 +85,48 @@ class MySQLJWKSDAO extends BaseMySQLDAO implements JWKSDAO { return $keyPair; } public function getAllPublicJWK(): array { $dbh = $this->getDBHandler(); $query = "SELECT `kid`, `key`, `url`, `update_time` FROM public_jwk"; $stmt = $dbh->prepare($query); $stmt->execute(); $keys = []; foreach ($stmt->fetchAll() as $row) { array_push($keys, $this->getPublicJWKFromResultRow($row)); } return $keys; } private function getPublicJWKFromResultRow($row): PublicJWK { $jwk = new PublicJWK (); $jwk->key = $row['key']; $jwk->kid = $row['kid']; $jwk->url = $row['url']; $jwk->updateTime = $row['update_time']; return $jwk; } public function updatePublicJWK(PublicJWK $jwk) { $dbh = $this->getDBHandler(); $query = "INSERT INTO public_jwk(kid, `key`, `url`, update_time) VALUES (:kid, :key, :url, :update_time)" . " ON DUPLICATE KEY UPDATE `key`=:key, `url`=:url, update_time=:update_time"; $stmt = $dbh->prepare($query); $stmt->bindParam(':kid', $jwk->kid); $stmt->bindParam(':key', $jwk->key); $stmt->bindParam(':url', $jwk->url); $stmt->bindParam(':update_time', $jwk->updateTime); $stmt->execute(); } }