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

Implemented JWKS endpoint and JWT generation. Continuing refactoring

parent 33f997e7
No related branches found
No related tags found
No related merge requests found
Showing
with 553 additions and 121 deletions
<?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;
}
}
...@@ -2,13 +2,64 @@ ...@@ -2,13 +2,64 @@
namespace RAP; namespace RAP;
use phpseclib\Crypt\RSA;
/** /**
* Manages the JWT Key Sets. * Manages the JWT Key Sets (currently only RSA .
*/ */
class JWKSHandler { class JWKSHandler {
private $locator;
public function __construct(Locator $locator) {
$this->locator = $locator;
}
public function generateKeyPair() { 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;
} }
} }
...@@ -10,7 +10,6 @@ class Locator { ...@@ -10,7 +10,6 @@ class Locator {
public $config; public $config;
private $serviceLogger; private $serviceLogger;
private $auditLogger; private $auditLogger;
private $dao;
private $session; private $session;
private $version; private $version;
...@@ -18,7 +17,6 @@ class Locator { ...@@ -18,7 +17,6 @@ class Locator {
$this->config = $config; $this->config = $config;
$this->setupLoggers(); $this->setupLoggers();
$this->setupDAO();
$this->version = file_get_contents(ROOT . '/version.txt'); $this->version = file_get_contents(ROOT . '/version.txt');
} }
...@@ -35,23 +33,53 @@ class Locator { ...@@ -35,23 +33,53 @@ class Locator {
} }
public function getDAO(): DAO { 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 { public function getCallbackHandler(): CallbackHandler {
return new \RAP\CallbackHandler($this); return new CallbackHandler($this);
} }
public function getUserHandler(): UserHandler { public function getUserHandler(): UserHandler {
return new \RAP\UserHandler($this->dao); return new UserHandler($this->getDAO());
} }
public function getMailSender(): MailSender { 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 { 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 { ...@@ -89,15 +117,4 @@ class Locator {
$this->auditLogger->pushHandler(new \Monolog\Handler\StreamHandler($this->config->auditLogFile, $logLevel)); $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');
}
}
} }
...@@ -24,69 +24,13 @@ ...@@ -24,69 +24,13 @@
namespace RAP; namespace RAP;
use PDO;
/** /**
* MySQL implementation of the DAO interface. See comments on the DAO interface. * MySQL implementation of the DAO interface. See comments on the DAO interface.
*/ */
class MySQLDAO implements DAO { class MySQLDAO extends BaseMySQLDAO 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 { public function __construct(Locator $locator) {
parent::__construct($locator);
$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 insertIdentity(Identity $identity, $userId) { public function insertIdentity(Identity $identity, $userId) {
...@@ -136,7 +80,7 @@ class MySQLDAO implements DAO { ...@@ -136,7 +80,7 @@ class MySQLDAO implements DAO {
return $identity; return $identity;
} }
public function findUserById($userId) { public function findUserById(string $userId): ?User {
if (!filter_var($userId, FILTER_VALIDATE_INT)) { if (!filter_var($userId, FILTER_VALIDATE_INT)) {
return null; return null;
......
...@@ -13,11 +13,11 @@ class OAuth2RequestHandler { ...@@ -13,11 +13,11 @@ class OAuth2RequestHandler {
public function handleAuthorizeRequest() { public function handleAuthorizeRequest() {
if (!isset($_REQUEST['client_id'])) { 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'])) { if (!isset($_REQUEST['redirect_uri'])) {
throw new \RAP\BadRequestException("Redirect URI is required"); throw new BadRequestException("Redirect URI is required");
} }
$clientId = $_REQUEST['client_id']; $clientId = $_REQUEST['client_id'];
...@@ -25,10 +25,10 @@ class OAuth2RequestHandler { ...@@ -25,10 +25,10 @@ class OAuth2RequestHandler {
$client = $this->locator->getDAO()->getOAuth2ClientByClientId($clientId); $client = $this->locator->getDAO()->getOAuth2ClientByClientId($clientId);
if ($client === null) { if ($client === null) {
throw new \RAP\BadRequestException("Invalid client id: " . $clientId); throw new BadRequestException("Invalid client id: " . $clientId);
} }
if ($client->redirectUrl !== $redirectUrl) { if ($client->redirectUrl !== $redirectUrl) {
throw new \RAP\BadRequestException("Invalid client redirect URI: " . $redirectUrl); throw new BadRequestException("Invalid client redirect URI: " . $redirectUrl);
} }
$alg; $alg;
...@@ -48,7 +48,7 @@ class OAuth2RequestHandler { ...@@ -48,7 +48,7 @@ class OAuth2RequestHandler {
private function executeStateFlow(OAuth2Client $client) { private function executeStateFlow(OAuth2Client $client) {
if (!isset($_REQUEST['state'])) { if (!isset($_REQUEST['state'])) {
throw new \RAP\BadRequestException("State is required"); throw new BadRequestException("State is required");
} }
// Storing OAuth2 data in session // Storing OAuth2 data in session
...@@ -65,16 +65,20 @@ class OAuth2RequestHandler { ...@@ -65,16 +65,20 @@ class OAuth2RequestHandler {
$session = $this->locator->getSession(); $session = $this->locator->getSession();
$code = base64_encode(bin2hex(openssl_random_pseudo_bytes(64))); $accessToken = new \RAP\AccessToken();
$accessToken = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $accessToken->code = base64_encode(bin2hex(openssl_random_pseudo_bytes(64)));
$state = $session->getOAuth2Data()->state; $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 $redirectUrl = $session->getOAuth2Data()->redirectUrl
. '?code=' . $code . '&scope=profile&state=' . $state; . '?code=' . $accessToken->code . '&scope=profile&state=' . $state;
return $redirectUrl; return $redirectUrl;
} }
...@@ -84,14 +88,24 @@ class OAuth2RequestHandler { ...@@ -84,14 +88,24 @@ class OAuth2RequestHandler {
$this->validateAccessTokenRequest(); $this->validateAccessTokenRequest();
$code = filter_input(INPUT_POST, 'code', FILTER_SANITIZE_STRING); $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(); $this->validateParametersMatching();
$token = []; $token = [];
$token['access_token'] = $accessToken; $token['access_token'] = $accessToken->token;
$token['token_type'] = 'bearer'; $token['token_type'] = 'bearer';
$token['expires_in'] = 300; $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; return $token;
} }
......
<?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;
}
...@@ -30,31 +30,6 @@ namespace RAP; ...@@ -30,31 +30,6 @@ namespace RAP;
*/ */
interface DAO { 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. * Create a new identity.
* @param type $userId the user ID associated to that identity * @param type $userId the user ID associated to that identity
...@@ -71,7 +46,7 @@ interface DAO { ...@@ -71,7 +46,7 @@ interface DAO {
/** /**
* @return RAP\User an user object, null if nothing was found. * @return RAP\User an user object, null if nothing was found.
*/ */
function findUserById($userId); function findUserById(string $userId): ?User;
function setPrimaryIdentity($userId, $identityId); function setPrimaryIdentity($userId, $identityId);
......
<?php
namespace RAP;
interface JWKSDAO {
public function getRSAKeyPairs(): array;
public function insertRSAKeyPair(RSAKeyPair $keyPair): RSAKeyPair;
public function getNewestKeyPair(): RSAKeyPair;
}
<?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;
}
}
<?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();
}
}
<?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;
}
}
<?php
namespace RAP;
class AccessToken {
public $token;
public $code;
public $userId;
public $creationTime;
public $expirationTime;
public $redirectUri;
public $clientId;
public $scope;
}
File moved
File moved
File moved
File moved
File moved
<?php
namespace RAP;
class RSAKeyPair {
public $keyId;
public $privateKey;
public $publicKey;
public $alg;
public $creationTime;
}
\ No newline at end of file
File moved
...@@ -38,7 +38,7 @@ class User { ...@@ -38,7 +38,7 @@ class User {
$this->identities = []; $this->identities = [];
} }
public function addIdentity(Identity $identity) { public function addIdentity(Identity $identity): void {
array_push($this->identities, $identity); array_push($this->identities, $identity);
} }
...@@ -52,4 +52,81 @@ class User { ...@@ -52,4 +52,81 @@ class User {
throw new \Exception("No primary identity defined for user " . $this->id); 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;
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment