From dd6c80b4f1bb5cb14aed146ea63b5a4793338e05 Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Tue, 23 Jul 2019 17:26:24 +0200
Subject: [PATCH] Handled JWT verification for WS endpoints

---
 classes/IdTokenBuilder.php               |  2 +-
 classes/OAuth2RequestHandler.php         | 30 +++++++++++++++++++++++-
 classes/datalayer/JWKSDAO.php            |  2 ++
 classes/datalayer/mysql/MySQLJWKSDAO.php | 17 ++++++++++++++
 index.php                                |  2 ++
 5 files changed, 51 insertions(+), 2 deletions(-)

diff --git a/classes/IdTokenBuilder.php b/classes/IdTokenBuilder.php
index cef3321..1ea499d 100644
--- a/classes/IdTokenBuilder.php
+++ b/classes/IdTokenBuilder.php
@@ -29,7 +29,7 @@ class IdTokenBuilder {
             'iss' => $this->locator->config->jwtIssuer,
             'sub' => $user->id,
             'iat' => time(),
-            'exp' => time() + 120,
+            'exp' => time() + 3600,
             'name' => $user->getCompleteName()
         );
 
diff --git a/classes/OAuth2RequestHandler.php b/classes/OAuth2RequestHandler.php
index c481f98..bdee406 100644
--- a/classes/OAuth2RequestHandler.php
+++ b/classes/OAuth2RequestHandler.php
@@ -2,6 +2,8 @@
 
 namespace RAP;
 
+use \Firebase\JWT\JWT;
+
 class OAuth2RequestHandler {
 
     private $locator;
@@ -166,7 +168,33 @@ class OAuth2RequestHandler {
         }
 
         $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($bearer_token);
-        if ($accessToken->expired) {
+        if ($accessToken === null) {
+            $this->attemptJWTTokenValidation($bearer_token);
+        } else if ($accessToken->expired) {
+            throw new UnauthorizedException("Access token is expired");
+        }
+    }
+
+    private function attemptJWTTokenValidation($jwt): void {
+
+        $jwtParts = explode('.', $jwt);
+        if (count($jwtParts) === 0) {
+            throw new UnauthorizedException("Invalid token");
+        }
+
+        $header = JWT::jsonDecode(JWT::urlsafeB64Decode($jwtParts[0]));
+        if (!isset($header->kid)) {
+            throw new UnauthorizedException("Invalid token: missing kid in header");
+        }
+
+        $keyPair = $this->locator->getJWKSDAO()->getRSAKeyPairById($header->kid);
+        if ($keyPair === null) {
+            throw new UnauthorizedException("Invalid kid: no key found");
+        }
+
+        try {
+            JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]);
+        } catch (\Firebase\JWT\ExpiredException $ex) {
             throw new UnauthorizedException("Access token is expired");
         }
     }
diff --git a/classes/datalayer/JWKSDAO.php b/classes/datalayer/JWKSDAO.php
index c2821ea..54b0ce0 100644
--- a/classes/datalayer/JWKSDAO.php
+++ b/classes/datalayer/JWKSDAO.php
@@ -6,6 +6,8 @@ interface JWKSDAO {
 
     public function getRSAKeyPairs(): array;
 
+    public function getRSAKeyPairById(string $id): ?RSAKeyPair;
+
     public function insertRSAKeyPair(RSAKeyPair $keyPair): RSAKeyPair;
 
     public function getNewestKeyPair(): RSAKeyPair;
diff --git a/classes/datalayer/mysql/MySQLJWKSDAO.php b/classes/datalayer/mysql/MySQLJWKSDAO.php
index cb4cf4b..4d6c713 100644
--- a/classes/datalayer/mysql/MySQLJWKSDAO.php
+++ b/classes/datalayer/mysql/MySQLJWKSDAO.php
@@ -43,6 +43,23 @@ class MySQLJWKSDAO extends BaseMySQLDAO implements JWKSDAO {
         return $keyPairs;
     }
 
+    public function getRSAKeyPairById(string $id): ?RSAKeyPair {
+
+        $dbh = $this->getDBHandler();
+
+        $query = "SELECT id, private_key, public_key, alg, creation_time FROM rsa_keypairs WHERE id = :id";
+
+        $stmt = $dbh->prepare($query);
+        $stmt->bindParam(':id', $id);
+        $stmt->execute();
+
+        foreach ($stmt->fetchAll() as $row) {
+            return $this->getRSAKeyPairFromResultRow($row);
+        }
+
+        return null;
+    }
+
     public function getNewestKeyPair(): RSAKeyPair {
         $dbh = $this->getDBHandler();
 
diff --git a/index.php b/index.php
index 76b64a8..5fd9f6d 100644
--- a/index.php
+++ b/index.php
@@ -37,12 +37,14 @@ Flight::map('error', function($ex) {
         http_response_code(401);
         echo "Unauthorized: " . $ex->message;
     } else if ($ex instanceof \Exception) {
+        http_response_code(500);
         if ($ex->getMessage() !== null) {
             echo $ex->getMessage();
         } else {
             echo $ex->getTraceAsString();
         }
     } else {
+        http_response_code(500);
         throw $ex;
     }
 });
-- 
GitLab