diff --git a/classes/JWKSHandler.php b/classes/JWKSHandler.php
index feffd906d1ac26d6c27e3ae528ffcf8461773efb..966873fe7cfc8cd27225f19d1218c0cf5c874b2b 100644
--- a/classes/JWKSHandler.php
+++ b/classes/JWKSHandler.php
@@ -77,4 +77,67 @@ class JWKSHandler {
         return $matches[1];
     }
 
+    public function loadAllJWKS(): array {
+
+        foreach ($this->locator->config->jwksUrls as $url) {
+            $this->loadJWKS($url);
+        }
+
+        $dao = $this->locator->getJWKSDAO();
+        return $dao->getAllPublicJWK();
+    }
+
+    private function loadJWKS($url) {
+
+        $dao = $this->locator->getJWKSDAO();
+
+        $conn = curl_init($url);
+        curl_setopt($conn, CURLOPT_FOLLOWLOCATION, 1);
+        curl_setopt($conn, CURLOPT_RETURNTRANSFER, true);
+
+        $result = curl_exec($conn);
+        $info = curl_getinfo($conn);
+
+        if ($info['http_code'] === 200) {
+            $jwks = json_decode($result, TRUE);
+
+            foreach ($jwks['keys'] as $key) {
+                $key['url'] = $url;
+                $jwk = $this->getPublicJWK($key);
+                $dao->updatePublicJWK($jwk);
+            }
+        } else {
+            $errorMessage = 'Error while retrieving JWKS: ' . curl_error($conn);
+            error_log($result);
+            curl_close($conn);
+            http_response_code(500);
+            die($errorMessage);
+        }
+
+        curl_close($conn);
+    }
+
+    private function getPublicJWK($data): PublicJWK {
+
+        // Convert Base64 uri-safe variant to default (needed for JWKS)
+        $n = strtr($data['n'], '-_', '+/');
+
+        $rsa = new RSA();
+
+        $key = "<RSAKeyPair>"
+                . "<Modulus>" . $n . "</Modulus>"
+                . "<Exponent>" . $data['e'] . "</Exponent>"
+                . "</RSAKeyPair>";
+
+        $rsa->loadKey($key, RSA::PUBLIC_FORMAT_XML);
+
+        $jwk = new PublicJWK();
+        $jwk->kid = $data['kid'];
+        $jwk->key = $rsa;
+        $jwk->url = $data['url'];
+        $jwk->updateTime = time();
+
+        return $jwk;
+    }
+
 }
diff --git a/classes/TokenBuilder.php b/classes/TokenBuilder.php
index f848112593eee27e38199afc482e056985f64508..d42103f813787875793ad5bf4e8eac05a2602766 100644
--- a/classes/TokenBuilder.php
+++ b/classes/TokenBuilder.php
@@ -101,17 +101,15 @@ class TokenBuilder {
      * @param int $lifespan in hours
      * @param string $audience target service
      */
-    public function generateNewToken(int $lifespan, string $audience) {
+    public function generateNewToken(string $subject, 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),
+            'sub' => strval($subject),
             'iat' => $iat,
             'exp' => $exp,
             'aud' => $audience
diff --git a/classes/TokenExchanger.php b/classes/TokenExchanger.php
new file mode 100644
index 0000000000000000000000000000000000000000..a397478b8146cf3db6bc4b644618968084779fd1
--- /dev/null
+++ b/classes/TokenExchanger.php
@@ -0,0 +1,86 @@
+<?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;
+
+use \Firebase\JWT\JWT;
+
+class TokenExchanger {
+
+    private $locator;
+
+    public function __construct(Locator $locator) {
+        $this->locator = $locator;
+    }
+
+    public function exchangeToken(string $token) {
+
+        $key = $this->getKeyForToken($token);
+        $decoded = JWT::decode($token, $key->key, ['RS256']);
+
+        $subject = $decoded->sub;
+        $lifespan = ($decoded->exp - time());
+
+        $data = [];
+
+        $data['access_token'] = $this->locator->getTokenBuilder()->generateNewToken($subject, $lifespan / 3600, "gms");
+        $data['issued_token_type'] = "urn:ietf:params:oauth:token-type:access_token";
+        $data['token_type'] = 'Bearer';
+        $data['expires_in'] = $lifespan;
+
+        return $data;
+    }
+
+    private function getKeyForToken(string $token): PublicJWK {
+
+        $keys = $this->locator->getJWKSDAO()->getAllPublicJWK();
+
+        $parts = explode('.', $token);
+        $head = JWT::jsonDecode(JWT::urlsafeB64Decode($parts[0]));
+
+        $kid = $head->kid;
+
+        $key = $this->getKeyByKid($keys, $kid);
+        if ($key === null) {
+            $keys = $this->locator->getJWKSHandler()->loadAllJWKS();
+        }
+        $key = $this->getKeyByKid($keys, $kid);
+
+        if ($key !== null) {
+            return $key;
+        }
+
+        throw new \Exception("Invalid kid");
+    }
+
+    private function getKeyByKid(array $keys, string $kid): ?PublicJWK {
+        foreach ($keys as $key) {
+            if ($key->kid === $kid) {
+                return $key;
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/classes/datalayer/JWKSDAO.php b/classes/datalayer/JWKSDAO.php
index 9cd83f49900a9cfccd8cbadc77caa4116da2e0f2..a5b52a612b834e745e1396ab8ca3c1ac81a3387f 100644
--- a/classes/datalayer/JWKSDAO.php
+++ b/classes/datalayer/JWKSDAO.php
@@ -11,4 +11,8 @@ interface JWKSDAO {
     public function insertRSAKeyPair(RSAKeyPair $keyPair): RSAKeyPair;
 
     public function getNewestKeyPair(): ?RSAKeyPair;
+
+    public function getAllPublicJWK(): array;
+
+    public function updatePublicJWK(PublicJWK $jwk);
 }
diff --git a/classes/datalayer/mysql/MySQLJWKSDAO.php b/classes/datalayer/mysql/MySQLJWKSDAO.php
index 8e7a83ad188340b257950396055f12ceee49ec17..5611db62ab69e85ec700c51bf174be86f98376e8 100644
--- a/classes/datalayer/mysql/MySQLJWKSDAO.php
+++ b/classes/datalayer/mysql/MySQLJWKSDAO.php
@@ -85,4 +85,48 @@ class MySQLJWKSDAO extends BaseMySQLDAO implements JWKSDAO {
         return $keyPair;
     }
 
+    public function getAllPublicJWK(): array {
+
+        $dbh = $this->getDBHandler();
+
+        $query = "SELECT `kid`, `key`, `url`, `update_time` FROM public_jwk";
+
+        $stmt = $dbh->prepare($query);
+        $stmt->execute();
+
+        $keys = [];
+
+        foreach ($stmt->fetchAll() as $row) {
+            array_push($keys, $this->getPublicJWKFromResultRow($row));
+        }
+
+        return $keys;
+    }
+
+    private function getPublicJWKFromResultRow($row): PublicJWK {
+
+        $jwk = new PublicJWK ();
+        $jwk->key = $row['key'];
+        $jwk->kid = $row['kid'];
+        $jwk->url = $row['url'];
+        $jwk->updateTime = $row['update_time'];
+        return $jwk;
+    }
+
+    public function updatePublicJWK(PublicJWK $jwk) {
+
+        $dbh = $this->getDBHandler();
+
+        $query = "INSERT INTO public_jwk(kid, `key`, `url`, update_time) VALUES (:kid, :key, :url, :update_time)"
+                . " ON DUPLICATE KEY UPDATE `key`=:key, `url`=:url, update_time=:update_time";
+
+        $stmt = $dbh->prepare($query);
+        $stmt->bindParam(':kid', $jwk->kid);
+        $stmt->bindParam(':key', $jwk->key);
+        $stmt->bindParam(':url', $jwk->url);
+        $stmt->bindParam(':update_time', $jwk->updateTime);
+
+        $stmt->execute();
+    }
+
 }
diff --git a/classes/model/PublicJWK.php b/classes/model/PublicJWK.php
new file mode 100644
index 0000000000000000000000000000000000000000..c883bf5edd7e79d36b4b6da59b214b730ba0147f
--- /dev/null
+++ b/classes/model/PublicJWK.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace RAP;
+
+class PublicJWK {
+
+    public $kid;
+    public $key;
+    public $url;
+    public $updateTime;
+
+}
diff --git a/config-example.json b/config-example.json
index 14f3e739a3ecc97f3e888a0fe1b1b2444d9c8a23..adc90f42e72e9efc7a17b8463401905abccf4446 100644
--- a/config-example.json
+++ b/config-example.json
@@ -56,5 +56,6 @@
                 "scope": "read:fileserver read:rap"
             }],
         "lifespans": [1, 6, 12, 24]
-    }
+    },
+    "jwksUrls": ["http://service/jwks"]
 }
diff --git a/include/front-controller.php b/include/front-controller.php
index 019da569d30f1d9816dacf29f49226663f6a204f..41c49f41f20f8fd1cd5a95e86527e37b360e55ad 100644
--- a/include/front-controller.php
+++ b/include/front-controller.php
@@ -370,7 +370,8 @@ Flight::route('POST /token-issuer', function () {
     }
 
     $tokenBuilder = $locator->getTokenBuilder();
-    $token = $tokenBuilder->generateNewToken($postData['lifespan'], $postData['audit']);
+    $userId = $this->locator->getSession()->getUser()->id;
+    $token = $tokenBuilder->generateNewToken($userId, $postData['lifespan'], $postData['audit']);
 
     header('Content-Type: text/plain');
     header("Content-disposition: attachment; filename=\"token.txt\"");
diff --git a/include/rest-web-service.php b/include/rest-web-service.php
index ac213cf92bb7716bd1bcd1f11508744da3f69d5b..c4e78543b83082f7a3758a46622cec2d89c3dd68 100644
--- a/include/rest-web-service.php
+++ b/include/rest-web-service.php
@@ -89,3 +89,14 @@ Flight::route('POST ' . $WS_PREFIX . '/user', function() {
 
     Flight::json($user);
 });
+
+Flight::route('POST ' . $WS_PREFIX . '/exchange', function() {
+
+    global $locator;
+
+    $subjectToken = Flight::request()->data['subject_token'];
+
+    $exchanger = new \RAP\TokenExchanger($locator);
+
+    Flight::json($exchanger->exchangeToken($subjectToken));
+});
diff --git a/sql/setup-database.sql b/sql/setup-database.sql
index 2d46eaa0ca2af1912ff5edee9eabc30bfee173d4..5b3842b0e1b0db1004bbb5b76e743c1eb605e5f8 100644
--- a/sql/setup-database.sql
+++ b/sql/setup-database.sql
@@ -83,6 +83,14 @@ CREATE TABLE `rsa_keypairs` (
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
+CREATE TABLE `public_jwk` (
+  `kid` varchar(255) NOT NULL,
+  `key` text,
+  `url` text,
+  `update_time` BIGINT NOT NULL,
+  PRIMARY KEY (`kid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
 CREATE TABLE `rap_permissions` (
   `user_id` bigint NOT NULL,
   `permission` varchar(255) NOT NULL,