Skip to content
Snippets Groups Projects
Commit 9be006f4 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Added token exchange endpoint

parent e45fe334
No related branches found
No related tags found
No related merge requests found
......@@ -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;
}
}
......@@ -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
......
<?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;
}
}
......@@ -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);
}
......@@ -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();
}
}
<?php
namespace RAP;
class PublicJWK {
public $kid;
public $key;
public $url;
public $updateTime;
}
......@@ -56,5 +56,6 @@
"scope": "read:fileserver read:rap"
}],
"lifespans": [1, 6, 12, 24]
}
},
"jwksUrls": ["http://service/jwks"]
}
......@@ -370,7 +370,8 @@ Flight::route('POST /token-issuer', function () {
}
$tokenBuilder = $locator->getTokenBuilder();
$token = $tokenBuilder->generateNewToken($postData['lifespan'], $postData['audit']);
$userId = $this->locator->getSession()->getUser()->id;
$token = $tokenBuilder->generateNewToken($userId, $postData['lifespan'], $postData['audit']);
header('Content-Type: text/plain');
header("Content-disposition: attachment; filename=\"token.txt\"");
......
......@@ -89,3 +89,14 @@ Flight::route('POST ' . $WS_PREFIX . '/user', function() {
Flight::json($user);
});
Flight::route('POST ' . $WS_PREFIX . '/exchange', function() {
global $locator;
$subjectToken = Flight::request()->data['subject_token'];
$exchanger = new \RAP\TokenExchanger($locator);
Flight::json($exchanger->exchangeToken($subjectToken));
});
......@@ -83,6 +83,14 @@ CREATE TABLE `rsa_keypairs` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `public_jwk` (
`kid` varchar(255) NOT NULL,
`key` text,
`url` text,
`update_time` BIGINT NOT NULL,
PRIMARY KEY (`kid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `rap_permissions` (
`user_id` bigint NOT NULL,
`permission` varchar(255) NOT NULL,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment