Commit 47a4929d authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Implemented JWKS endpoint and JWT generation. Continuing refactoring

parent 33f997e7
Loading
Loading
Loading
Loading
+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;
    }

}
+53 −2
Original line number Diff line number Diff line
@@ -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;
    }

}
+35 −18
Original line number Diff line number Diff line
@@ -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');
        }
    }

}
+4 −60
Original line number Diff line number Diff line
@@ -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) {
@@ -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;
+29 −15
Original line number Diff line number Diff line
@@ -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;
    }
Loading