Added JWK-set web-controller listing kustvakt public keys.

Change-Id: If8244161d7979008c65e3de5b9154cc5dd427a17
diff --git a/core/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java b/core/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
index 1d1c1ec..c3e45c2 100644
--- a/core/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
+++ b/core/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
@@ -32,11 +32,12 @@
 
     public static final Map<String, Object> KUSTVAKT_USER = new HashMap<>();
 
-    private static final Logger jlog = LoggerFactory
-            .getLogger(KustvaktConfiguration.class);
+    private static final Logger jlog =
+            LoggerFactory.getLogger(KustvaktConfiguration.class);
     private String indexDir;
     private int port;
-    // todo: make exclusive so that the containg languages can really only be used then
+    // todo: make exclusive so that the containg languages can really
+    // only be used then
     private List<String> queryLanguages;
 
     private String serverHost;
@@ -58,7 +59,7 @@
     private int validationStringLength;
     @Deprecated
     private int validationEmaillength;
-    
+
     private byte[] sharedSecret;
     @Deprecated
     private String adminToken;
@@ -75,102 +76,105 @@
     private String default_const;
     private ArrayList<String> foundries;
     private ArrayList<String> layers;
-    
+
     private String baseURL;
-    
-    
+
+
     // deprec?!
     private final BACKENDS DEFAULT_ENGINE = BACKENDS.LUCENE;
 
-	public KustvaktConfiguration (Properties properties) throws IOException, URISyntaxException {
+    public KustvaktConfiguration (Properties properties)
+            throws Exception {
         load(properties);
     }
-	
+
     /**
      * loading of the properties and mapping to parameter variables
      * 
      * @param properties
      * @return
-     * @throws IOException 
-     * @throws URISyntaxException 
-     * @throws KustvaktException 
+     * @throws Exception
      */
-    protected void load (Properties properties)
-            throws IOException, URISyntaxException {
+    protected void load (Properties properties) throws Exception {
         baseURL = properties.getProperty("kustvakt.base.url", "/api/*");
         maxhits = new Integer(properties.getProperty("maxhits", "50000"));
         returnhits = new Integer(properties.getProperty("returnhits", "50000"));
         indexDir = properties.getProperty("krill.indexDir", "");
         port = new Integer(properties.getProperty("server.port", "8095"));
         // server options
-        serverHost = String.valueOf(properties.getProperty("server.host",
-                "localhost"));
+        serverHost = String
+                .valueOf(properties.getProperty("server.host", "localhost"));
         String queries = properties.getProperty("korap.ql", "");
         String[] qls = queries.split(",");
         queryLanguages = new ArrayList<>();
         for (String querylang : qls)
             queryLanguages.add(querylang.trim().toUpperCase());
 
-        default_const = properties
-                .getProperty("default.layer.constituent", "mate");
-        default_dep = properties.getProperty("default.layer.dependency", "mate");
+        default_const =
+                properties.getProperty("default.layer.constituent", "mate");
+        default_dep =
+                properties.getProperty("default.layer.dependency", "mate");
         default_lemma = properties.getProperty("default.layer.lemma", "tt");
-        default_pos = properties.getProperty("default.layer.partOfSpeech", "tt");
-        default_token = properties.getProperty("default.layer.orthography",
-                "opennlp");
+        default_pos =
+                properties.getProperty("default.layer.partOfSpeech", "tt");
+        default_token =
+                properties.getProperty("default.layer.orthography", "opennlp");
 
         // security configuration
-        inactiveTime = TimeUtils.convertTimeToSeconds(properties.getProperty(
-                "security.idleTimeoutDuration", "10M"));
-        allowMultiLogIn = Boolean.valueOf(properties
-                .getProperty("security.multipleLogIn"));
+        inactiveTime = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.idleTimeoutDuration", "10M"));
+        allowMultiLogIn = Boolean
+                .valueOf(properties.getProperty("security.multipleLogIn"));
 
-        loginAttemptNum = Long.parseLong(properties.getProperty(
-                "security.loginAttemptNum", "3"));
-        loginAttemptTTL = TimeUtils.convertTimeToSeconds(properties
-                .getProperty("security.authAttemptTTL", "30M"));
+        loginAttemptNum = Long.parseLong(
+                properties.getProperty("security.loginAttemptNum", "3"));
+        loginAttemptTTL = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.authAttemptTTL", "30M"));
 
-        loadFactor = Integer.valueOf(properties.getProperty(
-                "security.encryption.loadFactor", "15"));
-        validationStringLength = Integer.valueOf(properties.getProperty(
-                "security.validation.stringLength", "150"));
-        validationEmaillength = Integer.valueOf(properties.getProperty(
-                "security.validation.emailLength", "40"));
-        
-        sharedSecret = properties.getProperty("security.sharedSecret", "")
-                .getBytes();
+        loadFactor = Integer.valueOf(
+                properties.getProperty("security.encryption.loadFactor", "15"));
+        validationStringLength = Integer.valueOf(properties
+                .getProperty("security.validation.stringLength", "150"));
+        validationEmaillength = Integer.valueOf(properties
+                .getProperty("security.validation.emailLength", "40"));
+
+        sharedSecret =
+                properties.getProperty("security.sharedSecret", "").getBytes();
         adminToken = properties.getProperty("security.adminToken");
 
-        longTokenTTL = TimeUtils.convertTimeToSeconds(properties.getProperty(
-                "security.longTokenTTL", "100D"));
-        tokenTTL = TimeUtils.convertTimeToSeconds(properties.getProperty(
-                "security.tokenTTL", "72H"));
-        shortTokenTTL = TimeUtils.convertTimeToSeconds(properties.getProperty(
-                "security.shortTokenTTL", "3H"));
+        longTokenTTL = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.longTokenTTL", "100D"));
+        tokenTTL = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.tokenTTL", "72H"));
+        shortTokenTTL = TimeUtils.convertTimeToSeconds(
+                properties.getProperty("security.shortTokenTTL", "3H"));
 
-//        passcodeSaltField = properties.getProperty("security.passcode.salt",
-//                "accountCreation");
-        
+        // passcodeSaltField =
+        // properties.getProperty("security.passcode.salt",
+        // "accountCreation");
+
     }
 
     /**
      * set properties
      * 
      * @param props
-     * @throws IOException 
+     * @throws IOException
      */
-//    public void setProperties (Properties props) throws IOException {
-//        this.load(props);
-//    }
+    // public void setProperties (Properties props) throws IOException
+    // {
+    // this.load(props);
+    // }
 
 
     /**
      * properties can be overloaded after spring init
      * 
      * @param stream
-     * @throws URISyntaxException 
+     * @throws Exception 
      */
-    public void setPropertiesAsStream (InputStream stream) throws URISyntaxException {
+    public void setPropertiesAsStream (InputStream stream)
+            throws Exception {
         try {
 
             Properties p = new Properties();
@@ -227,7 +231,8 @@
     @Deprecated
     private static void loadClassLogger () {
         Properties log4j = new Properties();
-        jlog.info("using class path logging properties file to configure logging system");
+        jlog.info(
+                "using class path logging properties file to configure logging system");
 
         try {
             log4j.load(KustvaktConfiguration.class.getClassLoader()
@@ -238,7 +243,8 @@
         }
 
         PropertyConfigurator.configure(log4j);
-        jlog.warn("No logger properties detected. Using default logger properties");
+        jlog.warn(
+                "No logger properties detected. Using default logger properties");
     }
 
     public enum BACKENDS {
diff --git a/full/Changes b/full/Changes
index b46a483..a8c1c2f 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,5 +1,5 @@
 version 0.60.4
-25/06/2018
+26/06/2018
     - implemented OAuth2 authorization code request with OpenID Authentication (margaretha)
     - enabled OAuth2 authorization without OpenID authentication using Nimbus library (margaretha)
     - implemented response handler for OpenID authentication errors in authorization requests (margaretha)
@@ -8,6 +8,7 @@
     - added state to OAuth2 authorization error response (margaretha)
     - implemented OpenID token service for authorization code flow (margaretha)
     - implemented signed OpenID token with default algorithm RSA256 (margaretha)
+    - added JSON Web Key (JWK) set web-controller listing kustvakt public keys (margaretha)
     
 version 0.60.3
 06/06/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
index 6c215e0..038e402 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
@@ -1,19 +1,15 @@
 package de.ids_mannheim.korap.config;
 
-import java.io.BufferedReader;
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
+import java.nio.charset.Charset;
 import java.security.interfaces.RSAPrivateKey;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
-import java.security.spec.PKCS8EncodedKeySpec;
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -22,15 +18,18 @@
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
-import org.apache.commons.codec.binary.Base64;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.util.IOUtils;
 
 import de.ids_mannheim.korap.constant.AuthenticationMethod;
 import de.ids_mannheim.korap.interfaces.EncryptionIface;
 
 /**
  * Configuration for Kustvakt full version including properties
- * concerning
- * authentication and licenses.
+ * concerning authentication and licenses.
  * 
  * @author margaretha
  *
@@ -76,42 +75,15 @@
     private URL issuer;
     private URI issuerURI;
     private RSAPrivateKey rsaPrivateKey;
+    private JWKSet publicKeySet;
+    private String rsaKeyId;
 
-    public FullConfiguration (Properties properties)
-            throws IOException, URISyntaxException, InvalidKeySpecException,
-            NoSuchAlgorithmException {
+    public FullConfiguration (Properties properties) throws Exception {
         super(properties);
-        setRSAPrivateKey();
-    }
-
-    public void setRSAPrivateKey () throws IOException, InvalidKeySpecException,
-            NoSuchAlgorithmException {
-        InputStream is = getClass().getClassLoader()
-                .getResourceAsStream("kustvakt-private.key");
-
-        if (is == null){
-            this.rsaPrivateKey = null;
-            return;
-        }
-            
-        String privateKey = null;
-        try (BufferedReader reader =
-                new BufferedReader(new InputStreamReader(is));) {
-            privateKey = reader.readLine();
-        }
-        byte[] decodedKey = Base64.decodeBase64(privateKey);
-        KeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
-        this.rsaPrivateKey = (RSAPrivateKey) KeyFactory.getInstance("RSA")
-                .generatePrivate(keySpec);
-    }
-
-    public RSAPrivateKey getRSAPrivateKey () {
-        return this.rsaPrivateKey;
     }
 
     @Override
-    public void load (Properties properties)
-            throws IOException, URISyntaxException {
+    public void load (Properties properties) throws Exception {
 
         super.load(properties);
         // EM: regex used for storing vc
@@ -128,6 +100,43 @@
 
         setOAuth2Configuration(properties);
         setOpenIdConfiguration(properties);
+        setRSAKeys(properties);
+    }
+
+    private void setRSAKeys (Properties properties)
+            throws IOException, ParseException, JOSEException {
+        setRsaKeyId(properties.getProperty("rsa.key.id", ""));
+
+        String rsaPublic = properties.getProperty("rsa.public", "");
+        String rsaPrivate = properties.getProperty("rsa.private", "");
+
+        File rsaPublicFile = new File(rsaPublic);
+        JWKSet jwkSet = null;
+        if (rsaPublicFile.exists()) {
+            jwkSet = JWKSet.load(rsaPublicFile);
+        }
+        else {
+            InputStream is =
+                    getClass().getClassLoader().getResourceAsStream(rsaPublic);
+            jwkSet = JWKSet.load(is);
+        }
+        setPublicKeySet(jwkSet);
+
+        File rsaPrivateFile = new File(rsaPrivate);
+        String keyString = null;
+        if (rsaPrivateFile.exists()) {
+            keyString = IOUtils.readFileToString(rsaPrivateFile,
+                    Charset.forName("UTF-8"));
+        }
+        else {
+            InputStream is =
+                    getClass().getClassLoader().getResourceAsStream(rsaPrivate);
+            keyString = IOUtils.readInputStreamToString(is,
+                    Charset.forName("UTF-8"));
+        }
+        RSAKey rsaKey = (RSAKey) JWK.parse(keyString);
+        RSAPrivateKey privateKey = (RSAPrivateKey) rsaKey.toPrivateKey();
+        setRsaPrivateKey(privateKey);
     }
 
     private void setOpenIdConfiguration (Properties properties)
@@ -451,4 +460,28 @@
         this.issuerURI = issuerURI;
     }
 
+    public JWKSet getPublicKeySet () {
+        return publicKeySet;
+    }
+
+    public void setPublicKeySet (JWKSet publicKeySet) {
+        this.publicKeySet = publicKeySet;
+    }
+
+    public RSAPrivateKey getRsaPrivateKey () {
+        return rsaPrivateKey;
+    }
+
+    public void setRsaPrivateKey (RSAPrivateKey rsaPrivateKey) {
+        this.rsaPrivateKey = rsaPrivateKey;
+    }
+
+    public String getRsaKeyId () {
+        return rsaKeyId;
+    }
+
+    public void setRsaKeyId (String rsaKeyId) {
+        this.rsaKeyId = rsaKeyId;
+    }
+
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/JWKService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/JWKService.java
new file mode 100644
index 0000000..c157662
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/JWKService.java
@@ -0,0 +1,79 @@
+package de.ids_mannheim.korap.oauth2.openid.service;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.UUID;
+
+import org.json.JSONObject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+
+import de.ids_mannheim.korap.config.FullConfiguration;
+
+@Service
+public class JWKService {
+
+    @Autowired
+    private FullConfiguration config;
+
+    public static void main (String[] args)
+            throws NoSuchAlgorithmException, IOException {
+        generateJWK();
+    }
+
+    public static void generateJWK ()
+            throws NoSuchAlgorithmException, IOException {
+        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
+        gen.initialize(2048);
+        KeyPair keyPair = gen.generateKeyPair();
+
+        // Convert to JWK format
+        JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
+                .privateKey((RSAPrivateKey) keyPair.getPrivate())
+                .keyID(UUID.randomUUID().toString()).build();
+
+        // write private key
+        JSONObject json = new JSONObject(jwk.toJSONString());
+        OutputStreamWriter writer = new OutputStreamWriter(
+                new FileOutputStream("kustvakt_rsa.key"));
+        writer.write(json.toString(2));
+        writer.flush();
+        writer.close();
+
+        JWK publicJWK = jwk.toPublicJWK();
+        JWKSet jwkSet = new JWKSet(publicJWK);
+        json = new JSONObject(jwkSet.toString());
+        // write public key
+        writer = new OutputStreamWriter(
+                new FileOutputStream("kustvakt_rsa_public.key"));
+        writer.write(json.toString(2));
+        writer.flush();
+        writer.close();
+    }
+
+    /**
+     * Generates indented JSON string representation of kustvakt
+     * public keys
+     * 
+     * @return json string of kustvakt public keys
+     * 
+     * @see RFC 8017 regarding RSA specifications
+     * @see RFC 7517 regarding JWK (Json Web Key) and JWK Set
+     * 
+     */
+    public String generatePublicKeySetJson () {
+        JWKSet jwkSet = config.getPublicKeySet();
+        JSONObject json = new JSONObject(jwkSet.toString());
+        return json.toString(2);
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
index 59f7e67..cedf59b 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
@@ -118,7 +118,7 @@
             SignedJWT idToken = signIdToken(claims,
                     // default
                     new JWSHeader(JWSAlgorithm.RS256),
-                    config.getRSAPrivateKey());
+                    config.getRsaPrivateKey());
             OIDCTokens tokens =
                     new OIDCTokens(idToken, accessToken, refreshToken);
             return new OIDCTokenResponse(tokens);
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
index d0e69d7..29b9856 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
@@ -7,6 +7,7 @@
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
@@ -31,6 +32,7 @@
 
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.openid.OpenIdHttpRequestWrapper;
+import de.ids_mannheim.korap.oauth2.openid.service.JWKService;
 import de.ids_mannheim.korap.oauth2.openid.service.OpenIdAuthorizationService;
 import de.ids_mannheim.korap.oauth2.openid.service.OpenIdTokenService;
 import de.ids_mannheim.korap.security.context.TokenContext;
@@ -48,6 +50,8 @@
     @Autowired
     private OpenIdTokenService tokenService;
     @Autowired
+    private JWKService jwkService;
+    @Autowired
     private OpenIdResponseHandler openIdResponseHandler;
 
     /**
@@ -92,7 +96,7 @@
      * Class Reference values. </li>
      * </ul>
      * 
-     * @see OpenID Connect Core 1.0 specification
+     * @see "OpenID Connect Core 1.0 specification"
      * 
      * @param request
      * @param context
@@ -184,4 +188,20 @@
         return null;
 
     }
+
+    /**
+     * Retrieves Kustvakt public keys of JWK (Json Web Key) set
+     * format.
+     * 
+     * @return json string representation of the public keys
+     * 
+     * @see "RFC 8017 regarding RSA specifications"
+     * @see "RFC 7517 regarding JWK (Json Web Key) and JWK Set"
+     */
+    @GET
+    @Path("key/public")
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public String retrievePublicKeys () {
+        return jwkService.generatePublicKeySetJson();
+    }
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/config/ConfigTest.java b/full/src/test/java/de/ids_mannheim/korap/config/ConfigTest.java
index ed263c0..380d5fb 100644
--- a/full/src/test/java/de/ids_mannheim/korap/config/ConfigTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/config/ConfigTest.java
@@ -60,7 +60,7 @@
 
     @Test(expected = KustvaktException.class)
     @Ignore
-    public void testBeanOverrideInjection () throws KustvaktException, URISyntaxException {
+    public void testBeanOverrideInjection () throws Exception {
         helper().getContext()
                 .getConfiguration()
                 .setPropertiesAsStream(
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
index 6bd081a..5459c6d 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
@@ -4,27 +4,17 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.net.URI;
-import java.security.KeyFactory;
 import java.security.NoSuchAlgorithmException;
-import java.security.interfaces.RSAPublicKey;
 import java.security.spec.InvalidKeySpecException;
-import java.security.spec.KeySpec;
-import java.security.spec.X509EncodedKeySpec;
 import java.text.ParseException;
 import java.util.Date;
 
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 
-import org.apache.commons.codec.binary.Base64;
 import org.apache.http.entity.ContentType;
 import org.apache.oltu.oauth2.common.message.types.TokenType;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.util.MultiValueMap;
@@ -35,6 +25,8 @@
 import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jose.JWSVerifier;
 import com.nimbusds.jose.crypto.RSASSAVerifier;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jwt.SignedJWT;
 import com.sun.jersey.api.client.ClientHandlerException;
 import com.sun.jersey.api.client.ClientResponse;
@@ -60,19 +52,6 @@
             "https://korap.ids-mannheim.de/confidential/redirect";
     private String username = "dory";
 
-    private static String publicKey;
-
-    @BeforeClass
-    public static void init () throws IOException {
-        InputStream is = OAuth2OpenIdControllerTest.class.getClassLoader()
-                .getResourceAsStream("kustvakt-public.key");
-
-        try (BufferedReader reader =
-                new BufferedReader(new InputStreamReader(is));) {
-            publicKey = reader.readLine();
-        }
-    }
-
     private ClientResponse sendAuthorizationRequest (
             MultivaluedMap<String, String> form) throws KustvaktException {
         return resource().path("oauth2").path("openid").path("authorize")
@@ -254,7 +233,8 @@
      * <li>code id_token token</li>
      * </ul>
      * 
-     * @throws KustvaktException
+     * @throws KustvaktExceptiony);
+     *             assertTrue(signedJWT.verify(verifier));
      */
 
     @Test
@@ -317,10 +297,8 @@
     private void verifyingIdToken (String id_token, String username,
             String client_id) throws ParseException, InvalidKeySpecException,
             NoSuchAlgorithmException, JOSEException {
-        byte[] decodedPuk = Base64.decodeBase64(publicKey);
-        KeySpec keySpec = new X509EncodedKeySpec(decodedPuk);
-        RSAPublicKey publicKey = (RSAPublicKey) KeyFactory.getInstance("RSA")
-                .generatePublic(keySpec);
+        JWKSet keySet = config.getPublicKeySet();
+        RSAKey publicKey = (RSAKey) keySet.getKeyByKeyId(config.getRsaKeyId());
 
         SignedJWT signedJWT = SignedJWT.parse(id_token);
         JWSVerifier verifier = new RSASSAVerifier(publicKey);
@@ -334,4 +312,18 @@
         assertTrue(new Date()
                 .before(signedJWT.getJWTClaimsSet().getExpirationTime()));
     }
+
+    @Test
+    public void testPublicKeyAPI () throws KustvaktException {
+        ClientResponse response = resource().path("oauth2").path("openid")
+                .path("key").path("public").get(ClientResponse.class);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(1,node.at("/keys").size());
+        node = node.at("/keys/0");
+        assertEquals("RSA", node.at("/kty").asText());
+        assertEquals(config.getRsaKeyId(), node.at("/kid").asText());
+        assertNotNull(node.at("/e").asText());
+        assertNotNull(node.at("/n").asText());
+    }
 }
diff --git a/full/src/test/resources/kustvakt-private.key b/full/src/test/resources/kustvakt-private.key
deleted file mode 100644
index d419b89..0000000
--- a/full/src/test/resources/kustvakt-private.key
+++ /dev/null
@@ -1 +0,0 @@
-MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAIlVfcPe+PXGph6BX1zU9HQ1kSt0lz2LIGAB+krHcj5oaWeS/4xicvmmGRE5MeJQEMIcijl3OXjdZR7lK1dxn1UUHuZa3ijMnMgDcQz9BuGg+49R5KdSkkMwlVW5Bdt08TmU9teFdQpg+7bsVGKpSuW6yE6wkgo+Wwufw23ULNkjAgMBAAECgYBVq8o3zTm7gH+SmhwWOhaBBAWaeTH7x3WbzsAHtCG1gsb2QMJAHg4hZJdQokBXMKEzpkAoFxL4Lgxt2IJQG2ZL778uiQiy+xHI8VTXBNXmdo+F3hlNzEmJySSSCxYefSSv+DN/yBrOx0heGXR3vbefXey4a6q8RhthCuRfpHmqmQJBALyFdf4Oj4rozi/KI8yiD71+NNR7hHMtepn3YyY0zBXxk2YEwpcPkzBhdDiL6fYJjjoGFnqKLNqlgO8gHx+ET70CQQC6faQiLjUp50wbEAZqLY7Q353k2qTdAX8W9L2lF/79GEA+EJumQ2iWOu9qYqQuSMSKwheY6mdOVWj8yOMiu2pfAkEAll0cr3aNpw3o5tUjmKPqSgnPuWqLShKMJyHaQy75WMdF+ajyS+pwS7ZvLGrsQQF+H2mbpEFxZTN8kz3blRfDQQJBAKADPdm2HBegRkTSMy7XeDrwI+JBWEPpDMr9o9sMA9XWAQk/5s15+Tstxk9Z49VyynDkqKqkNY+Y6UQ8eedLN7ECQQCDiAsbwOe79EpsHdQBOZeNvpWu1x1TxieN0nCAa/zQz8qupHkL/u8VI8csz+s3qOcgxpJqsn58G8eb9Jmk9fGY
\ No newline at end of file
diff --git a/full/src/test/resources/kustvakt-public.key b/full/src/test/resources/kustvakt-public.key
deleted file mode 100644
index 087d6f7..0000000
--- a/full/src/test/resources/kustvakt-public.key
+++ /dev/null
@@ -1 +0,0 @@
-MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJVX3D3vj1xqYegV9c1PR0NZErdJc9iyBgAfpKx3I+aGlnkv+MYnL5phkROTHiUBDCHIo5dzl43WUe5StXcZ9VFB7mWt4ozJzIA3EM/QbhoPuPUeSnUpJDMJVVuQXbdPE5lPbXhXUKYPu27FRiqUrlushOsJIKPlsLn8Nt1CzZIwIDAQAB
\ No newline at end of file
diff --git a/full/src/test/resources/kustvakt-test.conf b/full/src/test/resources/kustvakt-test.conf
index ebfb216..48ce51c 100644
--- a/full/src/test/resources/kustvakt-test.conf
+++ b/full/src/test/resources/kustvakt-test.conf
@@ -53,10 +53,15 @@
 oauth2.default.scopes = openid read_username read_email 
 oauth2.client.credentials.scopes = read_client_info
 
-# JWT
+## JWT
 security.jwt.issuer=korap.ids-mannheim.de
 
-## token expiration
+## JWK
+rsa.private = kustvakt_rsa.key
+rsa.public = kustvakt_rsa_public.key
+rsa.key.id = 74caa3a9-217c-49e6-94e9-2368fdd02c35
+
+## token expiration time
 security.longTokenTTL = 1D
 security.tokenTTL = 9S
 security.shortTokenTTL = 5S
@@ -73,7 +78,7 @@
 security.validation.stringLength = 150
 security.validation.emailLength = 50
 security.encryption.algo=BCRYPT
-security.sharedSecret=testSecret
+security.sharedSecret=testSecretCodeMustContainsMinimum256Bits$87aL2t0sklnf66roGDerNsw2s9
 
 ## applicable: rewrite, foundry, filter, deny
 security.rewrite.strategies=filter, foundry, rewrite
\ No newline at end of file
diff --git a/full/src/test/resources/kustvakt_rsa.key b/full/src/test/resources/kustvakt_rsa.key
new file mode 100644
index 0000000..1db25e9
--- /dev/null
+++ b/full/src/test/resources/kustvakt_rsa.key
@@ -0,0 +1,12 @@
+{
+  "p": "y7t3f2VRo5TN3IsCjshSWWwe4H1-Xd7iBbtPS_fmBeaVDbLr-05LsGRJxXzKheMJ5DwBzhvWAlCig5uSJG3Gk4i0LgLY5YO33shb9qqqEnF54ZkJbiqxSs5l_dggzZgYB5z0riVl2VA3yfNm1qJIE2eipBouUjBEXMOEtJlOrFc",
+  "kty": "RSA",
+  "q": "v4HHIpOddl_78fVQgvZCsINygpLuniJ3sVShLhX7LnCU0Eb4TMK_Fyz9_JPb3YFvEoPpQw3kfnAhkOBTATTpXzg_dNtR6eQfvDJfHl9R6FuSoVTJoNAO_rqEpKzQOGXl4ohBxVjhXcbEo6GEVp4pZAeXMM8D02IWfvGbJd0Yw0k",
+  "d": "OJFnms4n3ajWKvK26aOh_r8JGgQwbQNIXpx8UqFnc_EB4nzxcLns8-FGKa9Vg3VMAs8cFC4iM9evx1084yqsCeSKgwiV5ZVQkwnp35Gd5BslZxuH8kCdR1mL5y0V0RMwgW-W1ry_YtdhBSIze8XCJXB7udNk7bviiJylEm8OouyxAq-5uUy_qMWYk-mtDSpmPW9SfFf91c6P7-ataDFcd_zxFotd1UwXDVDaPfUnxpOA6Jh1WsvIFhX4IzETuUG8n5C-j6FrK_YlU7U-zFzzF8qWTthQVj5l7A0zOGmq6OC9mv_xtnSc6z9I-HklWFXa8eDsc2JasYqJY8CmTDSy8Q",
+  "e": "AQAB",
+  "kid": "74caa3a9-217c-49e6-94e9-2368fdd02c35",
+  "qi": "Iv8_jAuCTdU7xZ1GXK0Zaql3Azu1-qXiZseod9urLFFZK6OvxrhH0BexG_P1tRikUfEUQiyqNVCU544Z0Y0AdDbgb5aEYNa3Bkb5WAHHXsLDtzXSsxgvR4Pzg3PhT3HTrLkgTlWy9g0u7bwfhb-KTRszcw4SyFXz9o62xJLPJZo",
+  "dp": "pA8_qHhHqMoAiNPsaFyKa_Y0WyTTqPX93w26SnvDYQcRCqoFfCbNrqrj-UOHtw9gfMmRzo795HlYlVCm--zmlxHjvpWOYiyS2bVQ0S8Xq6hztKbPQEbi5FGXMjZkHAuZdi__nWkCPmBpvJfkPX0LO40eHLX0jTzPIEBWUjSOdRs",
+  "dq": "GtnydumlqWRZ6hoQWNx4i1FS6_X4GRoSGD4af2C7oE5Ov0lEJVck_fXkAtcke9FbJohyW2GGSSglvK-HU-L8WcqEMzlRKe8_d97EMXkB_gdg7tf5kV-6yoKSeJh2dYHsErAyMJ5-suxcw-iwqohwm0LpMwHDso7NQq1TqKJwh2k",
+  "n": "mGgmGYIN06ibCh98nsXp0a77xRQNnB9rKpRGKm41tVi0zLQWqmEdDh2CmrMiOOxTJFSlAuAVkwK-KVQZ5Men5dJvRyTwZPtBWSJZk32Znj3VshFloSQlQU-g3oh3c2htP03EDtBLmecZMI-OUV1hRCvrRUrS-qF24CJ-rheFsCmpSievEJDQqTTfXcbAG2DdRQJHWb3y1iyNojB_mV1H2Gztg9DGEZarloqXoTFeDcxs7SpZJqAWCWTJQk8n6Ye79SfGMNrzaaqN9aHx__6FU-GFdZexlWE0CemQcfx_hTEkCTa2EsGgI_GETQIjeCZRB29x91E3AlWVvEgA591pzw"
+}
\ No newline at end of file
diff --git a/full/src/test/resources/kustvakt_rsa_public.key b/full/src/test/resources/kustvakt_rsa_public.key
new file mode 100644
index 0000000..28c2fad
--- /dev/null
+++ b/full/src/test/resources/kustvakt_rsa_public.key
@@ -0,0 +1,6 @@
+{"keys": [{
+  "kty": "RSA",
+  "e": "AQAB",
+  "kid": "74caa3a9-217c-49e6-94e9-2368fdd02c35",
+  "n": "mGgmGYIN06ibCh98nsXp0a77xRQNnB9rKpRGKm41tVi0zLQWqmEdDh2CmrMiOOxTJFSlAuAVkwK-KVQZ5Men5dJvRyTwZPtBWSJZk32Znj3VshFloSQlQU-g3oh3c2htP03EDtBLmecZMI-OUV1hRCvrRUrS-qF24CJ-rheFsCmpSievEJDQqTTfXcbAG2DdRQJHWb3y1iyNojB_mV1H2Gztg9DGEZarloqXoTFeDcxs7SpZJqAWCWTJQk8n6Ye79SfGMNrzaaqN9aHx__6FU-GFdZexlWE0CemQcfx_hTEkCTa2EsGgI_GETQIjeCZRB29x91E3AlWVvEgA591pzw"
+}]}
\ No newline at end of file