diff --git a/README.md b/README.md
index b24ffabb655089f92c7ac210511edb8ca04531c1..462ca07ab2fd345a295176f54a21d473458c9174 100644
--- a/README.md
+++ b/README.md
@@ -128,3 +128,13 @@ Create the logs directory and assign ownership to the Apache user (usually www-d
 ## Additional information and developer guide
 
 See the wiki: https://www.ict.inaf.it/gitlab/zorba/rap-ia2/wikis/home
+
+## Troubleshooting
+
+### Class not found while developing
+
+If you see a message like this:
+
+    PHP Fatal error:  Uncaught Error: Class 'RAP\\[...]' not found
+
+probably you have to regenerate the PHP autoload calling `composer dumpautoload` in RAP root directory.
diff --git a/classes/IdTokenBuilder.php b/classes/IdTokenBuilder.php
deleted file mode 100644
index 0f9ea1719f69b34ccc9e03630e09ce24db52b186..0000000000000000000000000000000000000000
--- a/classes/IdTokenBuilder.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<?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 $nonce = null): string {
-
-        $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair();
-
-        $payload = $this->createPayloadArray($accessToken, $nonce);
-
-        return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId);
-    }
-
-    private function createPayloadArray(AccessToken $accessToken, string $nonce = null) {
-
-        $user = $this->locator->getUserDAO()->findUserById($accessToken->userId);
-
-        $payloadArr = array(
-            'iss' => $this->locator->config->jwtIssuer,
-            'sub' => strval($user->id),
-            'iat' => intval($accessToken->creationTime),
-            'exp' => intval($accessToken->expirationTime),
-            'name' => $user->getCompleteName(),
-            'aud' => $accessToken->clientId
-        );
-
-        if ($nonce !== null) {
-            $payloadArr['nonce'] = $nonce;
-        }
-
-        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();
-            if ($user->getInstitution() !== null) {
-                $payloadArr['org'] = $user->getInstitution();
-            }
-        }
-
-        if ($accessToken->joinUser !== null) {
-            $payloadArr['alt_sub'] = strval($accessToken->joinUser);
-        }
-
-        return $payloadArr;
-    }
-
-    /**
-     * @param int $lifespan in hours
-     * @param string $audit target service
-     */
-    public function generateNewToken(int $lifespan, string $audit) {
-        $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),
-            'iat' => $iat,
-            'exp' => $exp,
-            'aud' => $audit
-        );
-
-        return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId);
-    }
-
-}
diff --git a/classes/Locator.php b/classes/Locator.php
index f2ea9da0c6346e197508c0901f0dd017e43f4916..6cc736c8a6869cfc0ebf91da009113642c3f7f8f 100644
--- a/classes/Locator.php
+++ b/classes/Locator.php
@@ -98,8 +98,8 @@ class Locator {
         return new OAuth2RequestHandler($this);
     }
 
-    public function getIdTokenBuilder(): IdTokenBuilder {
-        return new IdTokenBuilder($this);
+    public function getTokenBuilder(): TokenBuilder {
+        return new TokenBuilder($this);
     }
 
     /**
diff --git a/classes/OAuth2RequestHandler.php b/classes/OAuth2RequestHandler.php
index 4d4c1243db2e5a1bd1e64dea88bd90f5b6baf623..c656d120194e79498da8bff092944e99bb2fd94c 100644
--- a/classes/OAuth2RequestHandler.php
+++ b/classes/OAuth2RequestHandler.php
@@ -43,7 +43,7 @@ class OAuth2RequestHandler {
         }
 
         // Storing OAuth2 data in session
-        $oauth2Data = new \RAP\OAuth2Data();
+        $oauth2Data = new OAuth2RequestData();
         $oauth2Data->clientId = $client->client;
         $oauth2Data->redirectUrl = $client->redirectUrl;
         $oauth2Data->state = $state;
@@ -55,34 +55,37 @@ class OAuth2RequestHandler {
         }
 
         $session = $this->locator->getSession();
-        $session->setOAuth2Data($oauth2Data);
+        $session->setOAuth2RequestData($oauth2Data);
     }
 
     public function getRedirectResponseUrl(): string {
 
         $session = $this->locator->getSession();
 
-        $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->getUser()->id;
-        $accessToken->clientId = $session->getOAuth2Data()->clientId;
-        $accessToken->redirectUri = $session->getOAuth2Data()->redirectUrl;
-        $accessToken->scope = $session->getOAuth2Data()->scope;
+        $code = base64_encode(bin2hex(openssl_random_pseudo_bytes(64)));
+
+        $tokenData = new AccessTokenData();
+        // Code is stored in hashed format inside the database, as a basic
+        // security measure in order to prevent issues in case of data breach.
+        $tokenData->codeHash = hash('sha256', $code);
+        $tokenData->userId = $session->getUser()->id;
+        $tokenData->clientId = $session->getOAuth2RequestData()->clientId;
+        $tokenData->redirectUri = $session->getOAuth2RequestData()->redirectUrl;
+        $tokenData->scope = $session->getOAuth2RequestData()->scope;
 
-        $this->locator->getAccessTokenDAO()->createAccessToken($accessToken);
+        $this->locator->getAccessTokenDAO()->createTokenData($tokenData);
 
-        $state = $session->getOAuth2Data()->state;
-        $nonce = $session->getOAuth2Data()->nonce;
+        $state = $session->getOAuth2RequestData()->state;
+        $nonce = $session->getOAuth2RequestData()->nonce;
 
         if ($state !== null) {
             // Authorization code grant flow
-            $redirectUrl = $session->getOAuth2Data()->redirectUrl
-                    . '?code=' . $accessToken->code . '&scope=profile&state=' . $state;
+            $redirectUrl = $session->getOAuth2RequestData()->redirectUrl
+                    . '?code=' . $code . '&scope=profile&state=' . $state;
         } else {
             // Implicit grant flow
-            $idToken = $this->locator->getIdTokenBuilder()->getIdToken($accessToken, $nonce);
-            $redirectUrl = $session->getOAuth2Data()->redirectUrl . "#id_token=" . $idToken;
+            $idToken = $this->locator->getTokenBuilder()->getIdToken($tokenData, $nonce);
+            $redirectUrl = $session->getOAuth2RequestData()->redirectUrl . "#id_token=" . $idToken;
         }
 
         return $redirectUrl;
@@ -99,19 +102,23 @@ class OAuth2RequestHandler {
         }
 
         // Note: theorically the standard wants also the client_id here,
-        // however some clients don't send it
+        // however some clients don't send it (e.g. Spring Security library)
+        //
+        $codeHash = hash('sha256', $params['code']);
 
-        $accessToken = $this->locator->getAccessTokenDAO()->retrieveAccessTokenFromCode($params['code']);
+        $tokenData = $this->locator->getAccessTokenDAO()->retrieveTokenDataFromCode($codeHash);
 
-        if ($accessToken === null) {
+        if ($tokenData === null) {
             throw new BadRequestException("No token for given code");
         }
 
-        if ($accessToken->redirectUri !== $params['redirect_uri']) {
+        if ($tokenData->redirectUri !== $params['redirect_uri']) {
             throw new BadRequestException("Invalid redirect URI: " . $params['redirect_uri']);
         }
 
-        return $this->getAccessTokenResponse($accessToken);
+        $response = $this->getAccessTokenResponse($tokenData);
+        $this->locator->getAccessTokenDAO()->deleteTokenData($codeHash);
+        return $response;
     }
 
     public function handleRefreshTokenRequest($params): array {
@@ -120,7 +127,7 @@ class OAuth2RequestHandler {
             throw new BadRequestException("refresh_token is required");
         }
 
-        $refreshToken = $this->locator->getRefreshTokenDAO()->getRefreshToken($params['refresh_token']);
+        $refreshToken = $this->locator->getRefreshTokenDAO()->getRefreshTokenData($params['refresh_token']);
 
         if ($refreshToken === null || $refreshToken->isExpired()) {
             throw new UnauthorizedException("Invalid refresh token");
@@ -129,7 +136,7 @@ class OAuth2RequestHandler {
         $scope = $this->getScope($params, $refreshToken);
 
         // Generating a new access token
-        $accessToken = new AccessToken();
+        $accessToken = new AccessTokenData();
         $accessToken->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128)));
         $accessToken->clientId = $refreshToken->clientId;
         $accessToken->userId = $refreshToken->userId;
@@ -167,40 +174,49 @@ class OAuth2RequestHandler {
 
             $scope = $newScopeValues;
         }
-
+            
         return $scope;
     }
 
-    private function getAccessTokenResponse(AccessToken $accessToken) {
+    private function getAccessTokenResponse(AccessTokenData $tokenData) {
 
         $result = [];
-        $result['access_token'] = $accessToken->token;
+        $result['access_token'] = $this->locator->getTokenBuilder()->getAccessToken($tokenData);
         $result['token_type'] = 'Bearer';
-        $result['expires_in'] = $accessToken->expirationTime - time();
-        $result['refresh_token'] = $this->getNewRefreshToken($accessToken);
+        $result['expires_in'] = $tokenData->expirationTime - time();
 
-        if ($accessToken->scope !== null && in_array('openid', $accessToken->scope)) {
-            $result['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken);
+        $refreshToken = base64_encode(bin2hex(openssl_random_pseudo_bytes(128)));
+        $refreshTokenHash = hash('sha256', $refreshToken);
+        $this->storeRefreshTokenData($tokenData, $refreshTokenHash);
+        $result['refresh_token'] = $refreshToken;
+
+        if ($tokenData->scope !== null && in_array('openid', $tokenData->scope)) {
+            $result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($tokenData);
         }
 
         return $result;
     }
 
-    private function getNewRefreshToken(AccessToken $accessToken): string {
-
-        $refreshToken = new RefreshToken();
-        $refreshToken->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128)));
-        $refreshToken->clientId = $accessToken->clientId;
-        $refreshToken->userId = $accessToken->userId;
-        $refreshToken->scope = $accessToken->scope;
+    private function storeRefreshTokenData(AccessTokenData $accessTokenData, string $refreshTokenHash): void {
 
-        $this->locator->getRefreshTokenDAO()->createRefreshToken($refreshToken);
+        $refreshToken = new RefreshTokenData();
+        $refreshToken->tokenHash = $refreshTokenHash;
+        $refreshToken->clientId = $accessTokenData->clientId;
+        $refreshToken->userId = $accessTokenData->userId;
+        $refreshToken->scope = $accessTokenData->scope;
 
-        return $refreshToken->token;
+        $this->locator->getRefreshTokenDAO()->createRefreshTokenData($refreshToken);
     }
 
+    /**
+     * Token introspection endpoint shouldn't be necessary when using OIDC (since
+     * tokens are self-contained JWT). This function is kept here for compatibility
+     * with some libraries (e.g. Spring Security) but it could be removed in the
+     * future.
+     */
     public function handleCheckTokenRequest($token): array {
 
+        // TODO: validate the token and expose data
         $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($token);
         if ($accessToken === null) {
             throw new UnauthorizedException("Invalid access token");
@@ -212,12 +228,12 @@ class OAuth2RequestHandler {
         $result['exp'] = $accessToken->expirationTime - time();
         $result['user_name'] = $user->id;
         $result['client_id'] = $accessToken->clientId;
-        $result['refresh_token'] = $this->getNewRefreshToken($accessToken);
+        $result['refresh_token'] = $this->storeRefreshTokenData($accessToken);
 
         if ($accessToken->scope !== null) {
             $result['scope'] = $accessToken->scope;
             if (in_array('openid', $accessToken->scope)) {
-                $result['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken);
+                $result['id_token'] = $this->locator->getTokenBuilder()->getIdToken($accessToken);
             }
         }
 
diff --git a/classes/TokenBuilder.php b/classes/TokenBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..75c6a04e6b36892634ff127c225729389a8f9aa2
--- /dev/null
+++ b/classes/TokenBuilder.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace RAP;
+
+use \Firebase\JWT\JWT;
+
+class TokenBuilder {
+
+    private $locator;
+
+    public function __construct(Locator $locator) {
+        $this->locator = $locator;
+    }
+
+    public function getIdToken(AccessTokenData $tokenData, string $nonce = null): string {
+
+        $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair();
+
+        $payload = $this->createIdTokenPayloadArray($tokenData, $nonce);
+
+        return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId);
+    }
+
+    private function createIdTokenPayloadArray(AccessTokenData $tokenData, string $nonce = null) {
+
+        $user = $this->locator->getUserDAO()->findUserById($tokenData->userId);
+
+        $payloadArr = array(
+            'iss' => $this->locator->config->jwtIssuer,
+            'sub' => strval($user->id),
+            'iat' => intval($tokenData->creationTime),
+            'exp' => intval($tokenData->expirationTime),
+            'name' => $user->getCompleteName(),
+            'aud' => $tokenData->clientId
+        );
+
+        if ($nonce !== null) {
+            $payloadArr['nonce'] = $nonce;
+        }
+
+        if (in_array("email", $tokenData->scope)) {
+            $payloadArr['email'] = $user->getPrimaryEmail();
+        }
+        if (in_array("profile", $tokenData->scope)) {
+            $payloadArr['given_name'] = $user->getName();
+            $payloadArr['family_name'] = $user->getSurname();
+            if ($user->getInstitution() !== null) {
+                $payloadArr['org'] = $user->getInstitution();
+            }
+        }
+
+        /*if ($tokenData->joinUser !== null) {
+            $payloadArr['alt_sub'] = strval($tokenData->joinUser);
+        }*/
+
+        return $payloadArr;
+    }
+
+    public function getAccessToken(AccessTokenData $tokenData) {
+
+        $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair();
+
+        $user = $this->locator->getUserDAO()->findUserById($tokenData->userId);
+
+        $payload = array(
+            'iss' => $this->locator->config->jwtIssuer,
+            'sub' => strval($user->id),
+            'iat' => intval($tokenData->creationTime),
+            'exp' => intval($tokenData->expirationTime),
+            'aud' => $this->getAudience($tokenData)
+        );
+
+        return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId);
+    }
+
+    private function getAudience(AccessTokenData $tokenData) {
+
+        $client = $this->locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($tokenData->clientId);
+
+        $audiences = [$tokenData->clientId];
+        error_log(json_encode($client->scopeAudienceMap));
+
+        foreach ($tokenData->scope as $scope) {
+            if (array_key_exists($scope, $client->scopeAudienceMap)) {
+                $audience = $client->scopeAudienceMap[$scope];
+                if (!in_array($audience, $audiences)) {
+                    array_push($audiences, $audience);
+                }
+            }
+        }
+
+        if (count($audiences) === 1) {
+            // according to RFC 7519 audience can be a single value or an array
+            return $audiences[0];
+        }
+        return $audiences;
+    }
+
+    /**
+     * @param int $lifespan in hours
+     * @param string $audience target service
+     */
+    public function generateNewToken(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),
+            'iat' => $iat,
+            'exp' => $exp,
+            'aud' => $audience
+        );
+
+        return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId);
+    }
+
+}
diff --git a/classes/UserHandler.php b/classes/UserHandler.php
index 318f5230ade99769f1a87ecb28d9a8466e739484..86facfecee0e1e29f45bd79bdb057ef0b9f8b6fe 100644
--- a/classes/UserHandler.php
+++ b/classes/UserHandler.php
@@ -125,7 +125,7 @@ class UserHandler {
         $accessToken->expirationTime = $accessToken->creationTime + 100;
         $accessToken->scope = ['openid'];
 
-        return $this->locator->getIdTokenBuilder()->getIdToken($accessToken);
+        return $this->locator->getTokenBuilder()->getIdToken($accessToken);
     }
 
 }
diff --git a/classes/datalayer/AccessTokenDAO.php b/classes/datalayer/AccessTokenDAO.php
index 34a25b0a040b1f7ae6c72c395d8c10b9a587b652..a7d6090c50aa72d1f008ba3aa650fabddea73184 100644
--- a/classes/datalayer/AccessTokenDAO.php
+++ b/classes/datalayer/AccessTokenDAO.php
@@ -9,11 +9,9 @@ interface AccessTokenDAO {
      * @param type $token login token
      * @param type $userId
      */
-    function createAccessToken(AccessToken $accessToken): AccessToken;
+    function createTokenData(AccessTokenData $tokenData): AccessTokenData;
 
-    function retrieveAccessTokenFromCode(string $code): ?AccessToken;
+    function retrieveTokenDataFromCode(string $codeHash): ?AccessTokenData;
 
-    function getAccessToken(string $token): ?AccessToken;
-
-    function deleteAccessToken(string $token): void;
+    function deleteTokenData(string $codeHash): void;
 }
diff --git a/classes/datalayer/RefreshTokenDAO.php b/classes/datalayer/RefreshTokenDAO.php
index e8196cbe7d583417945ef99b1a4c49808995382c..8124e96618bf56bb041006813d99fba55dd4bf5f 100644
--- a/classes/datalayer/RefreshTokenDAO.php
+++ b/classes/datalayer/RefreshTokenDAO.php
@@ -4,9 +4,9 @@ namespace RAP;
 
 interface RefreshTokenDAO {
 
-    function createRefreshToken(RefreshToken $refreshToken): RefreshToken;
+    function createRefreshTokenData(RefreshTokenData $refreshToken): RefreshTokenData;
 
-    function getRefreshToken(string $token): ?RefreshToken;
+    function getRefreshTokenData(string $tokenHash): ?RefreshTokenData;
 
-    function deleteRefreshToken(string $token): void;
+    function deleteRefreshTokenData(string $tokenHash): void;
 }
diff --git a/classes/datalayer/mysql/MySQLAccessTokenDAO.php b/classes/datalayer/mysql/MySQLAccessTokenDAO.php
index 446be56f0d31c8f8872d82868c85add01e333c17..00b5954cb3602ed47bda60615f55160c58b7c437 100644
--- a/classes/datalayer/mysql/MySQLAccessTokenDAO.php
+++ b/classes/datalayer/mysql/MySQLAccessTokenDAO.php
@@ -8,44 +8,43 @@ class MySQLAccessTokenDAO extends BaseMySQLDAO implements AccessTokenDAO {
         parent::__construct($locator);
     }
 
-    public function createAccessToken(AccessToken $accessToken): AccessToken {
+    public function createTokenData(AccessTokenData $tokenData): AccessTokenData {
 
         $dbh = $this->getDBHandler();
-        $stmt = $dbh->prepare("INSERT INTO access_token (token, code, user_id, redirect_uri, client_id, scope, creation_time, expiration_time)"
-                . " VALUES(:token, :code, :user_id, :redirect_uri, :client_id, :scope, :creation_time, :expiration_time)");
+        $stmt = $dbh->prepare("INSERT INTO access_token (code_hash, user_id, redirect_uri, client_id, scope, creation_time, expiration_time)"
+                . " VALUES(:code_hash, :user_id, :redirect_uri, :client_id, :scope, :creation_time, :expiration_time)");
 
         $scope = null;
-        if ($accessToken->scope !== null) {
-            $scope = join(' ', $accessToken->scope);
+        if ($tokenData->scope !== null) {
+            $scope = join(' ', $tokenData->scope);
         }
 
         $params = array(
-            ':token' => $accessToken->token,
-            ':code' => $accessToken->code,
-            ':user_id' => $accessToken->userId,
-            ':redirect_uri' => $accessToken->redirectUri,
-            ':client_id' => $accessToken->clientId,
+            ':code_hash' => $tokenData->codeHash,
+            ':user_id' => $tokenData->userId,
+            ':redirect_uri' => $tokenData->redirectUri,
+            ':client_id' => $tokenData->clientId,
             ':scope' => $scope,
-            ':creation_time' => $accessToken->creationTime,
-            ':expiration_time' => $accessToken->expirationTime
+            ':creation_time' => $tokenData->creationTime,
+            ':expiration_time' => $tokenData->expirationTime
         );
 
         if ($stmt->execute($params)) {
-            return $accessToken;
+            return $tokenData;
         } else {
             error_log($stmt->errorInfo()[2]);
             throw new \Exception("SQL error while storing user token");
         }
     }
 
-    public function retrieveAccessTokenFromCode(string $code): ?AccessToken {
+    public function retrieveTokenDataFromCode(string $codeHash): ?AccessTokenData {
 
         $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, scope "
-                . " FROM access_token WHERE code = :code AND UNIX_TIMESTAMP() < (creation_time + 60)");
-        $stmt->bindParam(':code', $code);
+        $stmt = $dbh->prepare("SELECT user_id, redirect_uri, client_id, creation_time, expiration_time, scope "
+                . " FROM access_token WHERE code_hash = :code_hash AND UNIX_TIMESTAMP() < (creation_time + 60)");
+        $stmt->bindParam(':code_hash', $codeHash);
 
         $stmt->execute();
 
@@ -54,32 +53,12 @@ class MySQLAccessTokenDAO extends BaseMySQLDAO implements AccessTokenDAO {
             return null;
         }
 
-        return $this->getAccessTokenFromRow($row);
+        return $this->getTokenDataFromRow($row);
     }
 
-    public function getAccessToken(string $token): ?AccessToken {
+    private function getTokenDataFromRow(array $row): AccessTokenData {
 
-        $dbh = $this->getDBHandler();
-
-        $stmt = $dbh->prepare("SELECT token, code, user_id, redirect_uri, client_id, creation_time, expiration_time, scope "
-                . " FROM access_token WHERE token = :token");
-        $stmt->bindParam(':token', $token);
-
-        $stmt->execute();
-
-        $row = $stmt->fetch();
-        if (!$row) {
-            return null;
-        }
-
-        return $this->getAccessTokenFromRow($row);
-    }
-
-    private function getAccessTokenFromRow(array $row): ?AccessToken {
-
-        $token = new AccessToken();
-        $token->token = $row['token'];
-        $token->code = $row['code'];
+        $token = new AccessTokenData();
         $token->userId = $row['user_id'];
         $token->redirectUri = $row['redirect_uri'];
         $token->clientId = $row['client_id'];
@@ -97,12 +76,13 @@ class MySQLAccessTokenDAO extends BaseMySQLDAO implements AccessTokenDAO {
         return $token;
     }
 
-    public function deleteAccessToken($token): void {
+    function deleteTokenData(string $codeHash): void {
 
         $dbh = $this->getDBHandler();
 
-        $stmt = $dbh->prepare("DELETE FROM access_token WHERE token = :token");
-        $stmt->bindParam(':token', $token);
+        $stmt = $dbh->prepare("DELETE FROM access_token WHERE code_hash = :code_hash");
+        $stmt->bindParam(':code_hash', $codeHash);
+
         $stmt->execute();
     }
 
diff --git a/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php b/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php
index ca23c39a28073951eb28b5d041a17caa83b62350..b63f94ba1d7b6f25bb40e2030b2e5aeba372dead 100644
--- a/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php
+++ b/classes/datalayer/mysql/MySQLOAuth2ClientDAO.php
@@ -8,9 +8,24 @@ class MySQLOAuth2ClientDAO extends BaseMySQLDAO implements OAuth2ClientDAO {
         parent::__construct($config);
     }
 
-    function getOAuth2Clients(): array {
+    public function getOAuth2Clients(): array {
+
         $dbh = $this->getDBHandler();
 
+        $clientsMap = $this->getClientsMap($dbh);
+        $this->loadAuthenticationMethods($dbh, $clientsMap);
+        $this->loadScopeAudienceMapping($dbh, $clientsMap);
+
+        $clients = [];
+        foreach ($clientsMap as $id => $client) {
+            array_push($clients, $client);
+        }
+
+        return $clients;
+    }
+
+    private function getClientsMap(PDO $dbh): array {
+
         // Load clients info
         $queryClient = "SELECT id, title, icon, client, secret, redirect_url, scope, home_page, show_in_home FROM oauth2_client";
         $stmtClients = $dbh->prepare($queryClient);
@@ -32,7 +47,11 @@ class MySQLOAuth2ClientDAO extends BaseMySQLDAO implements OAuth2ClientDAO {
             $clientsMap[$client->id] = $client;
         }
 
-        // Load authentication methods info
+        return $clientsMap;
+    }
+
+    private function loadAuthenticationMethods(PDO $dbh, array $clientsMap): void {
+
         $queryAuthNMethods = "SELECT client_id, auth_method FROM oauth2_client_auth_methods";
 
         $stmtAuthNMethods = $dbh->prepare($queryAuthNMethods);
@@ -42,13 +61,24 @@ class MySQLOAuth2ClientDAO extends BaseMySQLDAO implements OAuth2ClientDAO {
             $id = $row['client_id'];
             array_push($clientsMap[$id]->authMethods, $row['auth_method']);
         }
+    }
 
-        $clients = [];
-        foreach ($clientsMap as $id => $client) {
-            array_push($clients, $client);
-        }
+    private function loadScopeAudienceMapping(PDO $dbh, array $clientsMap): void {
 
-        return $clients;
+        $query = "SELECT client_id, scope, audience FROM oauth2_client_scope_audience_mapping";
+
+        $stmt = $dbh->prepare($query);
+
+        foreach ($stmt->fetchAll() as $row) {
+            $id = $row['client_id'];
+
+            if (array_key_exists($id, $clientsMap)) {
+                $client = $clientsMap[$id];
+                $client->scopeAudienceMap[$row['scope']] = $row['audience'];
+            }
+
+            array_push($clientsMap[$id]->authMethods, $row['auth_method']);
+        }
     }
 
     function createOAuth2Client(OAuth2Client $client): OAuth2Client {
@@ -203,6 +233,16 @@ class MySQLOAuth2ClientDAO extends BaseMySQLDAO implements OAuth2ClientDAO {
             array_push($client->authMethods, $row['auth_method']);
         }
 
+        // Load scope-audience mapping
+        $queryAudienceMapping = "SELECT scope, audience FROM oauth2_client_scope_audience_mapping WHERE client_id = :id";
+        $stmtAudienceMapping = $dbh->prepare($queryAudienceMapping);
+        $stmtAudienceMapping->bindParam(':id', $client->id);
+        $stmtAudienceMapping->execute();
+
+        foreach ($stmtAudienceMapping->fetchAll() as $row) {
+            $client->scopeAudienceMap[$row['scope']] = $row['audience'];
+        }
+
         return $client;
     }
 
diff --git a/classes/datalayer/mysql/MySQLRefreshTokenDAO.php b/classes/datalayer/mysql/MySQLRefreshTokenDAO.php
index f316aad55fcb14f6bdaf9f77292fdb592fff0a41..a99864e98ea88f4466ff27a6732c7b42f8109d7d 100644
--- a/classes/datalayer/mysql/MySQLRefreshTokenDAO.php
+++ b/classes/datalayer/mysql/MySQLRefreshTokenDAO.php
@@ -8,42 +8,42 @@ class MySQLRefreshTokenDAO extends BaseMySQLDAO implements RefreshTokenDAO {
         parent::__construct($locator);
     }
 
-    function createRefreshToken(RefreshToken $refreshToken): RefreshToken {
+    function createRefreshTokenData(RefreshTokenData $refreshTokenData): RefreshTokenData {
 
         $dbh = $this->getDBHandler();
-        $stmt = $dbh->prepare("INSERT INTO refresh_token (token, user_id, client_id, scope, creation_time, expiration_time)"
-                . " VALUES(:token, :user_id, :client_id, :scope, :creation_time, :expiration_time)");
+        $stmt = $dbh->prepare("INSERT INTO refresh_token (token_hash, user_id, client_id, scope, creation_time, expiration_time)"
+                . " VALUES(:token_hash, :user_id, :client_id, :scope, :creation_time, :expiration_time)");
 
         $scope = null;
-        if ($refreshToken->scope !== null) {
-            $scope = join(' ', $refreshToken->scope);
+        if ($refreshTokenData->scope !== null) {
+            $scope = join(' ', $refreshTokenData->scope);
         }
 
         $params = array(
-            ':token' => $refreshToken->token,
-            ':user_id' => $refreshToken->userId,
-            ':client_id' => $refreshToken->clientId,
+            ':token_hash' => $refreshTokenData->tokenHash,
+            ':user_id' => $refreshTokenData->userId,
+            ':client_id' => $refreshTokenData->clientId,
             ':scope' => $scope,
-            ':creation_time' => $refreshToken->creationTime,
-            ':expiration_time' => $refreshToken->expirationTime
+            ':creation_time' => $refreshTokenData->creationTime,
+            ':expiration_time' => $refreshTokenData->expirationTime
         );
 
         if ($stmt->execute($params)) {
-            return $refreshToken;
+            return $refreshTokenData;
         } else {
             error_log($stmt->errorInfo()[2]);
             throw new \Exception("SQL error while storing user token");
         }
     }
 
-    function getRefreshToken(string $tokenValue): ?RefreshToken {
+    function getRefreshTokenData(string $tokenHash): ?RefreshTokenData {
 
         $dbh = $this->getDBHandler();
 
-        $stmt = $dbh->prepare("SELECT token, user_id, client_id, creation_time, expiration_time, scope "
-                . " FROM refresh_token WHERE token = :token");
+        $stmt = $dbh->prepare("SELECT user_id, client_id, creation_time, expiration_time, scope "
+                . " FROM refresh_token WHERE token_hash = :token_hash");
 
-        $stmt->bindParam(':token', $tokenValue);
+        $stmt->bindParam(':token', $tokenHash);
 
         $stmt->execute();
 
@@ -52,8 +52,8 @@ class MySQLRefreshTokenDAO extends BaseMySQLDAO implements RefreshTokenDAO {
             return null;
         }
 
-        $token = new RefreshToken();
-        $token->token = $row['token'];
+        $token = new RefreshTokenData();
+        $token->tokenHash = $tokenHash;
         $token->userId = $row['user_id'];
         $token->clientId = $row['client_id'];
         $token->creationTime = $row['creation_time'];
@@ -70,12 +70,12 @@ class MySQLRefreshTokenDAO extends BaseMySQLDAO implements RefreshTokenDAO {
         return $token;
     }
 
-    function deleteRefreshToken(string $token): void {
+    function deleteRefreshTokenData(string $tokenHash): void {
 
         $dbh = $this->getDBHandler();
 
-        $stmt = $dbh->prepare("DELETE FROM refresh_token WHERE token = :token");
-        $stmt->bindParam(':token', $token);
+        $stmt = $dbh->prepare("DELETE FROM refresh_token WHERE token_hash = :token_hash");
+        $stmt->bindParam(':token_hash', $tokenHash);
         $stmt->execute();
     }
 
diff --git a/classes/login/LoginHandler.php b/classes/login/LoginHandler.php
index 198e140aa78b1cc55f8155097416ec6dd856a9a5..fc2531d186007782c802be6f7075120403c48551 100644
--- a/classes/login/LoginHandler.php
+++ b/classes/login/LoginHandler.php
@@ -73,7 +73,7 @@ class LoginHandler {
         $session = $this->locator->getSession();
         $this->locator->getAuditLogger()->info("LOGIN," . $this->identityType . "," . $user->id);
 
-        if ($session->getOAuth2Data() !== null) {
+        if ($session->getOAuth2RequestData() !== null) {
             $session->setUser($user);
             $redirectUrl = $this->locator->getOAuth2RequestHandler()->getRedirectResponseUrl();
             session_destroy();
diff --git a/classes/model/AccessToken.php b/classes/model/AccessTokenData.php
similarity index 60%
rename from classes/model/AccessToken.php
rename to classes/model/AccessTokenData.php
index 43527ac2960aa25c1d854f8d9350f48cd8ff7f3d..3826e22a14a040dceb6464a8b2969f58c891e9e8 100644
--- a/classes/model/AccessToken.php
+++ b/classes/model/AccessTokenData.php
@@ -2,24 +2,28 @@
 
 namespace RAP;
 
-class AccessToken {
+/**
+ * Data related to access tokens stored into the database. This object is
+ * retrieved from the database (using the code or the refresh token hashes) and
+ * it is used to generate OAuth2 tokens.
+ */
+class AccessTokenData {
 
     private const TOKEN_LIFESPAN = 3600;
 
-    public function __construct() {
-        $this->creationTime = time();
-        $this->expirationTime = $this->creationTime + AccessToken::TOKEN_LIFESPAN;
-    }
-
-    public $token;
-    public $code;
+    public $id;
+    public $codeHash;
     public $userId;
     public $creationTime;
     public $expirationTime;
     public $redirectUri;
     public $clientId;
     public $scope;
-    public $joinUser;
+
+    public function __construct() {
+        $this->creationTime = time();
+        $this->expirationTime = $this->creationTime + AccessTokenData::TOKEN_LIFESPAN;
+    }
 
     public function isExpired(): bool {
         return $this->expirationTime < time();
diff --git a/classes/model/OAuth2Client.php b/classes/model/OAuth2Client.php
index 254fefed6efde4f8d78f921a79ae5257a59323ae..acad698f384be71d3ae51df48f1d7389e514dd99 100644
--- a/classes/model/OAuth2Client.php
+++ b/classes/model/OAuth2Client.php
@@ -36,6 +36,7 @@ class OAuth2Client extends RAPClient {
     public $scope;
     public $homePage;
     public $showInHome;
+    public $scopeAudienceMap = [];
 
     public function getIconBasePath() {
         return 'client-icons/';
diff --git a/classes/model/OAuth2Data.php b/classes/model/OAuth2RequestData.php
similarity index 65%
rename from classes/model/OAuth2Data.php
rename to classes/model/OAuth2RequestData.php
index 9716301cddf32d11ca4664d931e88056df1ce1d4..bac7778be8a6f9e72bba1c5ed1f071535ed20c6f 100644
--- a/classes/model/OAuth2Data.php
+++ b/classes/model/OAuth2RequestData.php
@@ -2,7 +2,10 @@
 
 namespace RAP;
 
-class OAuth2Data {
+/**
+ * Data model for OAuth2 request.
+ */
+class OAuth2RequestData {
 
     public $clientId;
     public $redirectUrl;
diff --git a/classes/model/RefreshToken.php b/classes/model/RefreshTokenData.php
similarity index 84%
rename from classes/model/RefreshToken.php
rename to classes/model/RefreshTokenData.php
index a0525294da66f0f59b80aa480eac558f4142543c..22e18d18f30a6658f66ce07b2d18c36b86dfd685 100644
--- a/classes/model/RefreshToken.php
+++ b/classes/model/RefreshTokenData.php
@@ -2,20 +2,19 @@
 
 namespace RAP;
 
-class RefreshToken {
+class RefreshTokenData {
 
     private const TOKEN_LIFESPAN = 2 * 3600;
 
     public function __construct() {
         $this->creationTime = time();
-        $this->expirationTime = $this->creationTime + RefreshToken::TOKEN_LIFESPAN;
+        $this->expirationTime = $this->creationTime + RefreshTokenData::TOKEN_LIFESPAN;
     }
 
-    public $token;
+    public $tokenHash;
     public $userId;
     public $creationTime;
     public $expirationTime;
-    public $expired;
     public $clientId;
     public $scope;
 
diff --git a/classes/model/SessionData.php b/classes/model/SessionData.php
index 1473ce824695b2bc5e6ec34f6c7f28977bae4a1c..a560b6a03214691bcf9e194714372e5c0889f29c 100644
--- a/classes/model/SessionData.php
+++ b/classes/model/SessionData.php
@@ -34,7 +34,7 @@ class SessionData {
 
     private $user;
     private $x509DataToRegister;
-    private $oauth2Data;
+    private $oauth2RequestData;
     private $action;
 
     public function setUser(?User $user): void {
@@ -68,13 +68,13 @@ class SessionData {
         return $this->x509DataToRegister;
     }
 
-    public function setOAuth2Data(?OAuth2Data $oauth2Data): void {
-        $this->oauth2Data = $oauth2Data;
+    public function setOAuth2RequestData(?OAuth2RequestData $oauth2RequestData): void {
+        $this->oauth2RequestData = $oauth2RequestData;
         $this->save();
     }
 
-    public function getOAuth2Data(): ?OAuth2Data {
-        return $this->oauth2Data;
+    public function getOAuth2RequestData(): ?OAuth2RequestData {
+        return $this->oauth2RequestData;
     }
 
     public function setAction(?string $action): void {
diff --git a/exec/join.php b/exec/join.php
index 8a3689f2d5ec5d46e6238d7ef35c7514cdebff28..0f42b1bacb9cdfb64fa262951dde10b1c6509db5 100644
--- a/exec/join.php
+++ b/exec/join.php
@@ -12,7 +12,7 @@ include '../include/init.php';
 
 $dao = $locator->getUserDAO();
 $handler = $locator->getUserHandler();
-$tokenBuilder = $locator->getIdTokenBuilder();
+$tokenBuilder = $locator->getTokenBuilder();
 
 $user1 = $dao->findUserById((int) $argv[1]);
 if($user1 === null) {
diff --git a/include/front-controller.php b/include/front-controller.php
index 4df876eabd279b985dca7f5487f90e3936332578..a3010bc5fc3aff6a7c258c54cd1e90ee8c1aa286 100644
--- a/include/front-controller.php
+++ b/include/front-controller.php
@@ -30,8 +30,8 @@ Flight::route('/', function() {
     $locator->getSession()->setAction($action);
 
     switch ($action) {
-        case "oaut2client":
-            $clientId = $locator->getSession()->getOAuth2Data()->clientId;
+        case "oauth2client":
+            $clientId = $locator->getSession()->getOAuth2RequestData()->clientId;
             $client = $locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($clientId);
             $authPageModel = new \RAP\AuthPageModel($locator, $client);
             renderMainPage($authPageModel);
@@ -49,11 +49,11 @@ Flight::route('/', function() {
             $authPageModel = new \RAP\AuthPageModel($locator, $client);
             renderMainPage($authPageModel);
             break;
-        case "admin":
+        /*case "admin":
             $client = new \RAP\InternalClient('admin');
             $authPageModel = new \RAP\AuthPageModel($locator, $client);
             renderMainPage($authPageModel);
-            break;
+            break;*/
         default:
             session_destroy();
             $clients = $locator->getOAuth2ClientDAO()->getOAuth2Clients();
@@ -88,7 +88,7 @@ Flight::route('GET /auth/oauth2/authorize', function() {
     $requestHandler = new \RAP\OAuth2RequestHandler($locator);
     $requestHandler->handleAuthorizeRequest($params);
 
-    Flight::redirect('/?action=oaut2client');
+    Flight::redirect('/?action=oauth2client');
 });
 
 Flight::route('POST /auth/oauth2/token', function() {
@@ -391,7 +391,7 @@ Flight::route('POST /token-issuer', function () {
         throw new \RAP\BadRequestException("Missing form parameter");
     }
 
-    $tokenBuilder = $locator->getIdTokenBuilder();
+    $tokenBuilder = $locator->getTokenBuilder();
     $token = $tokenBuilder->generateNewToken($postData['lifespan'], $postData['audit']);
 
     header('Content-Type: text/plain');
diff --git a/sql/setup-database.sql b/sql/setup-database.sql
index 11e1b68b9248fb63e9bcd247306ce80795e7d83c..2d46eaa0ca2af1912ff5edee9eabc30bfee173d4 100644
--- a/sql/setup-database.sql
+++ b/sql/setup-database.sql
@@ -16,7 +16,15 @@ CREATE TABLE `oauth2_client_auth_methods` (
   `auth_method` varchar(50) NOT NULL,
   PRIMARY KEY (`client_id`, `auth_method`),
   FOREIGN KEY (`client_id`) REFERENCES `oauth2_client`(`id`)
-);
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE `oauth2_client_scope_audience_mapping` (
+  `client_id` int NOT NULL,
+  `scope` varchar(255) NOT NULL,
+  `audience` text NOT NULL,
+  PRIMARY KEY (`client_id`, `scope`),
+  FOREIGN KEY (`client_id`) REFERENCES `oauth2_client`(`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE TABLE `user` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
@@ -45,9 +53,8 @@ SET FOREIGN_KEY_CHECKS=1;
 
 CREATE TABLE `access_token` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
-  `token` text NOT NULL,
-  `user_id` text NOT NULL,
-  `code` text DEFAULT NULL,
+  `user_id` varchar(255) NOT NULL,
+  `code_hash` varchar(255) DEFAULT NULL,
   `creation_time` bigint(20) NOT NULL,
   `expiration_time` bigint(20) DEFAULT NULL,
   `redirect_uri` text DEFAULT NULL,
@@ -58,7 +65,7 @@ CREATE TABLE `access_token` (
 
 CREATE TABLE `refresh_token` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
-  `token` text NOT NULL,
+  `token_hash` varchar(255) NOT NULL,
   `user_id` text NOT NULL,
   `creation_time` BIGINT NOT NULL,
   `expiration_time` BIGINT NOT NULL,
@@ -74,13 +81,13 @@ CREATE TABLE `rsa_keypairs` (
   `alg` varchar(255),
   `creation_time` BIGINT NOT NULL,
   PRIMARY KEY (`id`)
-);
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE TABLE `rap_permissions` (
   `user_id` bigint NOT NULL,
   `permission` varchar(255) NOT NULL,
   PRIMARY KEY (`user_id`, `permission`)
-);
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE EVENT login_tokens_cleanup
     ON SCHEDULE
diff --git a/tests/IdTokenBuilderTest.php b/tests/IdTokenBuilderTest.php
index 67a2596bf4f98a63bf97d4ebff53f5d5efc6a151..2328f2c7bcc72080a617297fb20d01fea8d95c70 100644
--- a/tests/IdTokenBuilderTest.php
+++ b/tests/IdTokenBuilderTest.php
@@ -3,7 +3,7 @@
 use PHPUnit\Framework\TestCase;
 use \Firebase\JWT\JWT;
 
-final class IdTokenBuilderTest extends TestCase {
+final class TokenBuilderTest extends TestCase {
 
     public function testJWTCreation() {
 
@@ -38,7 +38,7 @@ final class IdTokenBuilderTest extends TestCase {
         $accessToken->scope = ["email", "profile"];
         $accessToken->userId = "user_id";
 
-        $tokenBuilder = new \RAP\IdTokenBuilder($locatorStub);
+        $tokenBuilder = new \RAP\TokenBuilder($locatorStub);
         $jwt = $tokenBuilder->getIdToken($accessToken);
 
         $this->assertNotNull($jwt);
diff --git a/tests/OAuth2RequestHandlerTest.php b/tests/OAuth2RequestHandlerTest.php
index 2efed06a18487180934599b4799433eaf06ab3cf..e24171a449a595b89b83a976c6197098ecb7d90e 100644
--- a/tests/OAuth2RequestHandlerTest.php
+++ b/tests/OAuth2RequestHandlerTest.php
@@ -63,7 +63,7 @@ final class OAuth2RequestHandlerTest extends TestCase {
         $locatorStub->method('getSession')->willReturn($sessionStub);
 
         $sessionStub->expects($this->once())
-                ->method('setOAuth2Data')->with($this->anything());
+                ->method('setOAuth2RequestData')->with($this->anything());
 
         $requestHandler = new \RAP\OAuth2RequestHandler($locatorStub);
         $requestHandler->handleAuthorizeRequest($params);
@@ -85,13 +85,13 @@ final class OAuth2RequestHandlerTest extends TestCase {
         $userDaoStub = $this->createMock(\RAP\UserDAO::class);
         $userDaoStub->method('findUserById')->willReturn($user);
 
-        $idTokenBuilderStub = $this->createMock(\RAP\IdTokenBuilder::class);
-        $idTokenBuilderStub->method('getIdToken')->willReturn('id-token');
+        $tokenBuilderStub = $this->createMock(\RAP\TokenBuilder::class);
+        $tokenBuilderStub->method('getIdToken')->willReturn('id-token');
 
         $locatorStub = $this->createMock(\RAP\Locator::class);
         $locatorStub->method('getAccessTokenDAO')->willReturn($tokenDaoStub);
         $locatorStub->method('getUserDAO')->willReturn($userDaoStub);
-        $locatorStub->method('getIdTokenBuilder')->willReturn($idTokenBuilderStub);
+        $locatorStub->method('getIdTokenBuilder')->willReturn($tokenBuilderStub);
 
         $requestHandler = new \RAP\OAuth2RequestHandler($locatorStub);