diff --git a/classes/IdTokenBuilder.php b/classes/IdTokenBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..9b358514494fdbac54ee6359d4cd23d01900ba62
--- /dev/null
+++ b/classes/IdTokenBuilder.php
@@ -0,0 +1,57 @@
+<?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;
+    }
+
+}
diff --git a/classes/JWKSHandler.php b/classes/JWKSHandler.php
index eb49951642fa183fb97563e663f1e88d2a0ce307..18ec90aff788992b70f3fc8b706bc883e6589683 100644
--- a/classes/JWKSHandler.php
+++ b/classes/JWKSHandler.php
@@ -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;
     }
 
 }
diff --git a/classes/Locator.php b/classes/Locator.php
index bee9b493b244e1b0a27626ab47d59040685db9c5..a1e5942891b89601e66ad292862c684041768880 100644
--- a/classes/Locator.php
+++ b/classes/Locator.php
@@ -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');
-        }
-    }
-
 }
diff --git a/classes/MySQLDAO.php b/classes/MySQLDAO.php
index 7dc0fd5c911d8d4f8d54280727273538bf378f17..7208695b7467cc5b9c46f6921556dcb8a3d3e218 100644
--- a/classes/MySQLDAO.php
+++ b/classes/MySQLDAO.php
@@ -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");
-        }
-    }
-
-    public function findAccessToken(string $code): ?string {
-
-        $dbh = $this->getDBHandler();
+class MySQLDAO extends BaseMySQLDAO implements DAO {
 
-        $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;
diff --git a/classes/OAuth2RequestHandler.php b/classes/OAuth2RequestHandler.php
index de070dbd6477a04e329dc11f7e7dac2af4810a5e..5a3bbd92d3efa8930872c3bd20f3078a28886426 100644
--- a/classes/OAuth2RequestHandler.php
+++ b/classes/OAuth2RequestHandler.php
@@ -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;
     }
@@ -111,7 +125,7 @@ class OAuth2RequestHandler {
         if (!isset($_POST['redirect_uri'])) {
             throw new BadRequestException("Redirect URI is required");
         }
-        
+
         // Note: theorically the standard wants also the client_id here,
         // however some clients don't send it
     }
diff --git a/classes/User.php b/classes/User.php
deleted file mode 100644
index 21967ff717e1ab17a1dccb49f73912a5c3072d5b..0000000000000000000000000000000000000000
--- a/classes/User.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?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;
-
-/**
- * Data model for the user. An user is a set of identities.
- */
-class User {
-
-    // User ID
-    public $id;
-    // List of identities
-    public $identities;
-
-    public function __construct() {
-        $this->identities = [];
-    }
-
-    public function addIdentity(Identity $identity) {
-        array_push($this->identities, $identity);
-    }
-
-    public function getPrimaryEmail() {
-        foreach ($this->identities as $identity) {
-            if ($identity->primary) {
-                return $identity->email;
-            }
-        }
-        // A primary identity MUST be defined
-        throw new \Exception("No primary identity defined for user " . $this->id);
-    }
-
-}
diff --git a/classes/datalayer/AccessTokenDAO.php b/classes/datalayer/AccessTokenDAO.php
new file mode 100644
index 0000000000000000000000000000000000000000..e9f60337e45f08a7024541b546a40a58e9874685
--- /dev/null
+++ b/classes/datalayer/AccessTokenDAO.php
@@ -0,0 +1,23 @@
+<?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;
+}
diff --git a/classes/DAO.php b/classes/datalayer/DAO.php
similarity index 83%
rename from classes/DAO.php
rename to classes/datalayer/DAO.php
index 54bc5ff6204aae74e7dbb8e9b0176846766b7a26..4de0886db83f7d9859c8b09d15a6664e0e065577 100644
--- a/classes/DAO.php
+++ b/classes/datalayer/DAO.php
@@ -30,31 +30,6 @@ namespace RAP;
  */
 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.
      * @param type $userId the user ID associated to that identity
@@ -71,7 +46,7 @@ interface DAO {
     /**
      * @return RAP\User an user object, null if nothing was found.
      */
-    function findUserById($userId);
+    function findUserById(string $userId): ?User;
 
     function setPrimaryIdentity($userId, $identityId);
 
diff --git a/classes/datalayer/JWKSDAO.php b/classes/datalayer/JWKSDAO.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2821ea90b72ff65153f4fd525a63cea481065ea
--- /dev/null
+++ b/classes/datalayer/JWKSDAO.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace RAP;
+
+interface JWKSDAO {
+
+    public function getRSAKeyPairs(): array;
+
+    public function insertRSAKeyPair(RSAKeyPair $keyPair): RSAKeyPair;
+
+    public function getNewestKeyPair(): RSAKeyPair;
+}
diff --git a/classes/datalayer/mysql/BaseMySQLDAO.php b/classes/datalayer/mysql/BaseMySQLDAO.php
new file mode 100644
index 0000000000000000000000000000000000000000..3741c956700b258629ac435b31bbf82e5e99783f
--- /dev/null
+++ b/classes/datalayer/mysql/BaseMySQLDAO.php
@@ -0,0 +1,27 @@
+<?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;
+    }
+
+}
diff --git a/classes/datalayer/mysql/MySQLAccessTokenDAO.php b/classes/datalayer/mysql/MySQLAccessTokenDAO.php
new file mode 100644
index 0000000000000000000000000000000000000000..6b90c12f9517368fe73317fd77114cbccb452da7
--- /dev/null
+++ b/classes/datalayer/mysql/MySQLAccessTokenDAO.php
@@ -0,0 +1,84 @@
+<?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();
+    }
+
+}
diff --git a/classes/datalayer/mysql/MySQLJWKSDAO.php b/classes/datalayer/mysql/MySQLJWKSDAO.php
new file mode 100644
index 0000000000000000000000000000000000000000..65618cac6c773dc59d95d9bf8a500cdd9c61ee48
--- /dev/null
+++ b/classes/datalayer/mysql/MySQLJWKSDAO.php
@@ -0,0 +1,68 @@
+<?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;
+    }
+
+}
diff --git a/classes/model/AccessToken.php b/classes/model/AccessToken.php
new file mode 100644
index 0000000000000000000000000000000000000000..e7fc0f1764a272722c0aa73e676c1d23b2c75ffd
--- /dev/null
+++ b/classes/model/AccessToken.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace RAP;
+
+class AccessToken {
+
+    public $token;
+    public $code;
+    public $userId;
+    public $creationTime;
+    public $expirationTime;
+    public $redirectUri;
+    public $clientId;
+    public $scope;
+
+}
diff --git a/classes/AuthPageModel.php b/classes/model/AuthPageModel.php
similarity index 100%
rename from classes/AuthPageModel.php
rename to classes/model/AuthPageModel.php
diff --git a/classes/InternalClient.php b/classes/model/InternalClient.php
similarity index 100%
rename from classes/InternalClient.php
rename to classes/model/InternalClient.php
diff --git a/classes/OAuth2Client.php b/classes/model/OAuth2Client.php
similarity index 100%
rename from classes/OAuth2Client.php
rename to classes/model/OAuth2Client.php
diff --git a/classes/OAuth2Data.php b/classes/model/OAuth2Data.php
similarity index 100%
rename from classes/OAuth2Data.php
rename to classes/model/OAuth2Data.php
diff --git a/classes/RAPClient.php b/classes/model/RAPClient.php
similarity index 100%
rename from classes/RAPClient.php
rename to classes/model/RAPClient.php
diff --git a/classes/model/RSAKeyPair.php b/classes/model/RSAKeyPair.php
new file mode 100644
index 0000000000000000000000000000000000000000..ac224585c130348be20a92f6152a8c9ac55faac5
--- /dev/null
+++ b/classes/model/RSAKeyPair.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace RAP;
+
+class RSAKeyPair {
+    
+    public $keyId;
+    public $privateKey;
+    public $publicKey;
+    public $alg;
+    public $creationTime;
+}
\ No newline at end of file
diff --git a/classes/SessionData.php b/classes/model/SessionData.php
similarity index 100%
rename from classes/SessionData.php
rename to classes/model/SessionData.php
diff --git a/classes/model/User.php b/classes/model/User.php
new file mode 100644
index 0000000000000000000000000000000000000000..c1b11a3f24a1751e5d79a68423cc7519416768d7
--- /dev/null
+++ b/classes/model/User.php
@@ -0,0 +1,132 @@
+<?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;
+
+/**
+ * Data model for the user. An user is a set of identities.
+ */
+class User {
+
+    // User ID
+    public $id;
+    // List of identities
+    public $identities;
+
+    public function __construct() {
+        $this->identities = [];
+    }
+
+    public function addIdentity(Identity $identity): void {
+        array_push($this->identities, $identity);
+    }
+
+    public function getPrimaryEmail() {
+        foreach ($this->identities as $identity) {
+            if ($identity->primary) {
+                return $identity->email;
+            }
+        }
+        // A primary identity MUST be defined
+        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;
+    }
+
+}
diff --git a/config-example.json b/config-example.json
index f73a89ee9e4d51dc28b64c5226584609c11c6e9d..36e73d850e28a665f4f4fd97bcca19645b900a01 100644
--- a/config-example.json
+++ b/config-example.json
@@ -4,6 +4,7 @@
     "auditLogFile": "/var/www/html/rap-ia2/logs/rap-audit.log",
     "timeZone": "Europe/Rome",
     "logLevel": "DEBUG",
+    "jwtIssuer": "sso.ia2.inaf.it",
     "databaseConfig": {
         "dbtype": "MySQL",
         "hostname": "localhost",
diff --git a/include/front-controller.php b/include/front-controller.php
index 8ab6e953554a3b273ac026101eab13b78409ca9a..efd208d12596630a007d9779530a10cd8dfe4f5f 100644
--- a/include/front-controller.php
+++ b/include/front-controller.php
@@ -85,10 +85,10 @@ Flight::route('GET /auth/oidc/jwks', function() {
     
     global $locator;
 
-    $requestHandler = new \RAP\OAuth2RequestHandler($locator);
-    $token = $requestHandler->handleCheckTokenRequest();
+    $jwksHandler = new \RAP\JWKSHandler($locator);
+    $jwks = $jwksHandler->getJWKS();
 
-    Flight::json($token);
+    Flight::json($jwks);
 });
 
 Flight::route('GET /logout', function() {
diff --git a/include/init.php b/include/init.php
index ca98a295f8055497aca32ec23799ff15ee54a2e1..e0c98813b73b7cacc6dbc691e3838487d65e2bc0 100644
--- a/include/init.php
+++ b/include/init.php
@@ -36,7 +36,7 @@ spl_autoload_register(function ($class) {
 
         $fileName = substr($class, $len, strlen($class) - $len);
 
-        $classDirectories = ['/', '/social/', '/exceptions/'];
+        $classDirectories = ['/', '/social/', '/exceptions/', '/datalayer/', '/model/'];
         foreach ($classDirectories as $directory) {
             $classpath = ROOT . '/classes' . $directory . $fileName . '.php';
             if (file_exists($classpath)) {
diff --git a/index.php b/index.php
index 777c43d366a87a7484aa56c5f065e1e1a748014b..653a6ef0f2dad64ea7a895d6b0e45afd6918c2e0 100644
--- a/index.php
+++ b/index.php
@@ -28,9 +28,6 @@ include './include/front-controller.php';
 include './include/gui-backend.php';
 include './include/rest-web-service.php';
 
-// Starting Flight framework
-Flight::start();
-
 // Error handling
 Flight::map('error', function(Exception $ex) {
     if ($ex instanceof \RAP\BadRequestException) {
@@ -41,3 +38,6 @@ Flight::map('error', function(Exception $ex) {
     }
 });
 
+// Starting Flight framework
+Flight::start();
+
diff --git a/sql/setup-database.sql b/sql/setup-database.sql
index 11f927025053b39e7f559d800680e860ab05f59a..a390e5a521568c6804c5fda198a4c9e0da19a691 100644
--- a/sql/setup-database.sql
+++ b/sql/setup-database.sql
@@ -48,6 +48,10 @@ CREATE TABLE `access_token` (
   `user_id` text NOT NULL,
   `code` text NOT NULL,
   `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `expiration_time` timestamp,
+  `redirect_uri` text,
+  `client_id` varchar(255),
+  `scope` text,
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
@@ -62,6 +66,15 @@ CREATE TABLE `join_request` (
   FOREIGN KEY (`target_user_id`) REFERENCES `user`(`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+CREATE TABLE `rsa_keypairs` (
+  `id` varchar(255) NOT NULL,
+  `public_key` text,
+  `private_key` text,
+  `alg` varchar(255),
+  `creation_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`)
+);
+
 CREATE EVENT login_tokens_cleanup
     ON SCHEDULE
       EVERY 1 MINUTE
diff --git a/tests/IdTokenBuilderTest.php b/tests/IdTokenBuilderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..015617ca1ef2aaab5634c2d76c07a69e2ade3358
--- /dev/null
+++ b/tests/IdTokenBuilderTest.php
@@ -0,0 +1,45 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+final class IdTokenBuilderTest extends TestCase {
+
+    public function testJWTCreation() {
+
+        $jwksDAOStub = $this->createMock(\RAP\JWKSDAO::class);
+
+        $locatorStub = $this->createMock(\RAP\Locator::class);
+        $locatorStub->method('getJWKSDAO')->willReturn($jwksDAOStub);
+
+        $jwksHandler = new \RAP\JWKSHandler($locatorStub);
+        $keyPair = $jwksHandler->generateKeyPair();
+
+        $jwksDAOStub->method('getNewestKeyPair')->willReturn($keyPair);
+
+        $user = new \RAP\User();
+        $user->id = "user_id";
+        $identity = new \RAP\Identity(\RAP\Identity::EDU_GAIN);
+        $identity->email = "name@inaf.it";
+        $identity->name = "Name";
+        $identity->surname = "Surname";
+        $identity->primary = true;
+        $user->addIdentity($identity);
+
+        $daoStub = $this->createMock(\RAP\DAO::class);
+        $locatorStub->method('getDAO')->willReturn($daoStub);
+        $daoStub->method('findUserById')->willReturn($user);
+
+        $locatorStub->config = json_decode('{"jwtIssuer": "issuer"}');
+
+        $accessToken = new \RAP\AccessToken();
+        $accessToken->token = "ttt";
+        $accessToken->scope = ["email", "profile"];
+        $accessToken->userId = "user_id";
+
+        $tokenBuilder = new \RAP\IdTokenBuilder($locatorStub);
+        $result = $tokenBuilder->getIdToken($accessToken, 'RS256');
+
+        $this->assertNotNull($result);
+    }
+
+}
diff --git a/tests/JWKSHandlerTest.php b/tests/JWKSHandlerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7901dc7645ec6c1cefdac7658e3b9fe89750ef3a
--- /dev/null
+++ b/tests/JWKSHandlerTest.php
@@ -0,0 +1,26 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+final class JWKSHandlerTest extends TestCase {
+
+    public function testKeyPairCreation(): void {
+
+        $daoStub = $this->createMock(\RAP\JWKSDAO::class);
+
+        $locatorStub = $this->createMock(\RAP\Locator::class);
+        $locatorStub->method('getJWKSDAO')->willReturn($daoStub);
+
+        $JWKSHandler = new \RAP\JWKSHandler($locatorStub);
+
+        $daoStub->expects($this->once())
+                ->method('insertRSAKeyPair')->with($this->anything());
+
+        $result = $JWKSHandler->generateKeyPair();
+
+        $this->assertNotNull($result->keyId);
+        $this->assertNotNull($result->privateKey);
+        $this->assertNotNull($result->publicKey);
+    }
+
+}