Updated JWT lib & added MySQL database tables.

Change-Id: I5860e0484ef03f473a863001f44128e8274a14d3
diff --git a/core/pom.xml b/core/pom.xml
index 2cc97db..91bb414 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -241,13 +241,6 @@
 			<version>0.4</version>
 		</dependency>
 		
-		<!-- EM: nimbus version is very old -->
-		<dependency>
-			<groupId>com.nimbusds</groupId>
-			<artifactId>nimbus-jose-jwt</artifactId>
-			<version>2.10.1</version>
-		</dependency>
-
 		<dependency>
 			<groupId>de.ids_mannheim.korap</groupId>
 			<artifactId>Krill</artifactId>
diff --git a/full/Changes b/full/Changes
index 0a58ea6..00d2cad 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,11 +1,13 @@
 version 0.60.3
-30/05/2018
+06/06/2018
 	- improved user authentication by using authentication filter for authorization code request (margaretha)
 	- limited client authentication to client id checking in authorization code request (margaretha)
 	- added user_id in the oauth2_access_token database table (margaretha)
 	- implemented OAuth2Authentication provider for token context management (margaretha)
 	- added parameter checking for authorization DAO (margaretha)
 	- added controller tests using OAuth2 access token (margaretha)
+	- added database tables for MySQL (margaretha)
+	- updated JWT library and related codes (margaretha)
 	
 version 0.60.2
 03/05/2018
diff --git a/full/pom.xml b/full/pom.xml
index 9ea7367..8937594 100644
--- a/full/pom.xml
+++ b/full/pom.xml
@@ -220,9 +220,9 @@
 			<scope>test</scope>
 		</dependency>
 		<dependency>
-		    <groupId>com.sun.jersey</groupId>
-		    <artifactId>jersey-json</artifactId>
-		    <version>${jersey.version}</version>
+			<groupId>com.sun.jersey</groupId>
+			<artifactId>jersey-json</artifactId>
+			<version>${jersey.version}</version>
 		</dependency>
 
 		<!-- Spring -->
@@ -269,11 +269,11 @@
 			<artifactId>activation</artifactId>
 			<version>1.1.1</version>
 		</dependency>
-		
+
 		<dependency>
-		    <groupId>javax.servlet</groupId>
-		    <artifactId>javax.servlet-api</artifactId>
-		    <version>4.0.0</version>
+			<groupId>javax.servlet</groupId>
+			<artifactId>javax.servlet-api</artifactId>
+			<version>4.0.0</version>
 		</dependency>
 
 		<!-- Flyway -->
@@ -282,13 +282,19 @@
 			<artifactId>flyway-core</artifactId>
 			<version>4.0</version>
 		</dependency>
-		
+
 		<!-- OAuth -->
 		<dependency>
 			<groupId>org.apache.oltu.oauth2</groupId>
 			<artifactId>org.apache.oltu.oauth2.authzserver</artifactId>
 			<version>1.0.0</version>
 		</dependency>
-	</dependencies>
 
+		<!-- JWT -->
+		<dependency>
+			<groupId>com.nimbusds</groupId>
+			<artifactId>nimbus-jose-jwt</artifactId>
+			<version>5.10</version>
+		</dependency>
+	</dependencies>
 </project>
\ No newline at end of file
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/APIAuthentication.java b/full/src/main/java/de/ids_mannheim/korap/authentication/APIAuthentication.java
index b90889b..be9d330 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/APIAuthentication.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/APIAuthentication.java
@@ -30,7 +30,7 @@
     //private Cache id_tokens = CacheManager.getInstance().getCache("id_tokens");
 
 
-    public APIAuthentication (KustvaktConfiguration config) {
+    public APIAuthentication (KustvaktConfiguration config) throws JOSEException {
         this.signedToken = new JWTSigner(config.getSharedSecret(),
                 config.getIssuer(), config.getTokenTTL());
     }
@@ -67,7 +67,7 @@
         c.setUsername(user.getUsername());
         SignedJWT jwt = signedToken.createJWT(user, attr);
         try {
-            c.setExpirationTime(jwt.getJWTClaimsSet().getExpirationTimeClaim());
+            c.setExpirationTime(jwt.getJWTClaimsSet().getExpirationTime().getTime());
         }
         catch (ParseException e) {
             throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT);
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java b/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
index 0cdd3d3..8258dcf 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/LdapAuth3.java
@@ -26,6 +26,7 @@
  
 package de.ids_mannheim.korap.authentication;
 
+import com.nimbusds.jose.JOSEException;
 import com.unboundid.ldap.sdk.*;
 
 import de.ids_mannheim.korap.config.KustvaktConfiguration;
@@ -72,7 +73,7 @@
 	public static final int LDAP_AUTH_RLOCKED	= 3;
 	public static final int LDAP_AUTH_RNOTREG	= 4;
 
-    public LdapAuth3 (KustvaktConfiguration config) {
+    public LdapAuth3 (KustvaktConfiguration config) throws JOSEException {
         super(config);
     }	
     
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/OpenIDconnectAuthentication.java b/full/src/main/java/de/ids_mannheim/korap/authentication/OpenIDconnectAuthentication.java
index 1cc6150..635b2ca 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/OpenIDconnectAuthentication.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/OpenIDconnectAuthentication.java
@@ -1,5 +1,6 @@
 package de.ids_mannheim.korap.authentication;
 
+import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jwt.SignedJWT;
 import de.ids_mannheim.korap.config.JWTSigner;
 import de.ids_mannheim.korap.config.KustvaktConfiguration;
@@ -40,26 +41,35 @@
 
 
     @Override
-    public TokenContext getTokenContext(String authToken)
+    public TokenContext getTokenContext (String authToken)
             throws KustvaktException {
         return this.database.getContext(authToken);
     }
 
 
     @Override
-    public TokenContext createTokenContext(User user, Map<String, Object> attr)
+    public TokenContext createTokenContext (User user, Map<String, Object> attr)
             throws KustvaktException {
         String cl_secret = (String) attr.get(Attributes.CLIENT_SECRET);
         if (cl_secret == null)
             throw new KustvaktException(StatusCodes.REQUEST_INVALID);
         attr.remove(cl_secret);
-        JWTSigner signer = new JWTSigner(cl_secret.getBytes(),
-                config.getIssuer(), config.getTokenTTL());
+        JWTSigner signer;
+        try {
+            signer = new JWTSigner(cl_secret.getBytes(), config.getIssuer(),
+                    config.getTokenTTL());
+        }
+        catch (JOSEException e1) {
+            // e1.printStackTrace();
+            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
+                    "Failed creating JWT.", e1);
+        }
         TokenContext c = new TokenContext();
         c.setUsername(user.getUsername());
         SignedJWT jwt = signer.createJWT(user, attr);
         try {
-            c.setExpirationTime(jwt.getJWTClaimsSet().getExpirationTimeClaim());
+            c.setExpirationTime(
+                    jwt.getJWTClaimsSet().getExpirationTime().getTime());
         }
         catch (ParseException e) {
             throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT);
@@ -79,13 +89,14 @@
 
 
     @Override
-    public TokenContext refresh (TokenContext context) throws KustvaktException {
+    public TokenContext refresh (TokenContext context)
+            throws KustvaktException {
         throw new UnsupportedOperationException("method not supported");
     }
 
 
     @Override
-    public TokenType getTokenType() {
+    public TokenType getTokenType () {
         return TokenType.ID_TOKEN;
     }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java b/full/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
index 426a873..659ce4a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
@@ -4,6 +4,7 @@
 import com.nimbusds.jose.crypto.MACSigner;
 import com.nimbusds.jose.crypto.MACVerifier;
 import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.JWTClaimsSet.Builder;
 import com.nimbusds.jwt.SignedJWT;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
@@ -17,6 +18,8 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -31,7 +34,8 @@
     private final int defaultttl;
 
 
-    public JWTSigner (final byte[] secret, URL issuer, final int defaulttl) {
+    public JWTSigner (final byte[] secret, URL issuer, final int defaulttl)
+            throws JOSEException {
         this.issuer = issuer;
         this.signer = new MACSigner(secret);
         this.verifier = new MACVerifier(secret);
@@ -40,7 +44,7 @@
 
 
     public JWTSigner (final byte[] secret, String issuer)
-            throws MalformedURLException {
+            throws MalformedURLException, JOSEException {
         this(secret, new URL(issuer), 72 * 60 * 60);
     }
 
@@ -50,32 +54,35 @@
     }
 
 
-    public SignedJWT signContent (User user, Map<String, Object> attr, int ttl) {
+    public SignedJWT signContent (User user, Map<String, Object> attr,
+            int ttl) {
         String scopes;
 
-        JWTClaimsSet cs = new JWTClaimsSet();
-        cs.setIssuerClaim(this.issuer.toString());
+        Builder csBuilder = new JWTClaimsSet.Builder();
+        csBuilder.issuer(this.issuer.toString());
 
         if ((scopes = (String) attr.get(Attributes.SCOPES)) != null) {
             Userdata data = new GenericUserData();
             data.readQuietly(attr, false);
             Scopes claims = Scopes.mapScopes(scopes, data);
-            cs.setCustomClaims(claims.toMap());
+            Map<String, Object> map = claims.toMap();
+            for (String key : map.keySet()) {
+                csBuilder.claim(key, map.get(key));
+            }
         }
 
-        cs.setSubjectClaim(user.getUsername());
-        if (attr.get(Attributes.CLIENT_ID) != null)
-            cs.setAudienceClaim(new String[] { (String) attr
-                    .get(Attributes.CLIENT_ID) });
-        cs.setExpirationTimeClaim(TimeUtils.getNow().plusSeconds(ttl)
-                .getMillis());
+        csBuilder.subject(user.getUsername());
+        if (attr.get(Attributes.CLIENT_ID) != null) {
+            csBuilder.audience((String) attr.get(Attributes.CLIENT_ID));
+        }
+        csBuilder.expirationTime(TimeUtils.getNow().plusSeconds(ttl).toDate());
         SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),
-                cs);
+                csBuilder.build());
         try {
             signedJWT.sign(signer);
         }
         catch (JOSEException e) {
-            return null;
+            e.printStackTrace();
         }
         return signedJWT;
     }
@@ -88,19 +95,16 @@
      */
     public SignedJWT signContent (String username, String userclient,
             String json, int ttl) {
-        JWTClaimsSet cs = new JWTClaimsSet();
-        cs.setSubjectClaim(username);
-        if (!json.isEmpty())
-            cs.setCustomClaim("data", json);
-        cs.setExpirationTimeClaim(TimeUtils.getNow().plusSeconds(ttl)
-                .getMillis());
-        cs.setIssuerClaim(this.issuer.toString());
+        Builder cs = new JWTClaimsSet.Builder();
+        cs.subject(username);
+        if (!json.isEmpty()) cs.claim("data", json);
+        cs.expirationTime(TimeUtils.getNow().plusSeconds(ttl).toDate());
+        cs.issuer(this.issuer.toString());
 
-        if (!userclient.isEmpty())
-            cs.setCustomClaim("userip", userclient);
+        if (!userclient.isEmpty()) cs.claim("userip", userclient);
 
-        SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),
-                cs);
+        SignedJWT signedJWT =
+                new SignedJWT(new JWSHeader(JWSAlgorithm.HS256), cs.build());
         try {
             signedJWT.sign(signer);
         }
@@ -133,15 +137,16 @@
         try {
             client = SignedJWT.parse(token);
             if (!client.verify(verifier))
-                throw new KustvaktException(StatusCodes.REQUEST_INVALID);
+                throw new KustvaktException(StatusCodes.INVALID_ACCESS_TOKEN,
+                        "Json Web Signature (JWS) object verification failed.");
 
-            if (!new DateTime(client.getJWTClaimsSet().getExpirationTimeClaim())
+            if (!new DateTime(client.getJWTClaimsSet().getExpirationTime())
                     .isAfterNow())
                 throw new KustvaktException(StatusCodes.EXPIRED,
                         "Authentication token is expired", token);
         }
         catch (ParseException | JOSEException e) {
-            //todo: message or entity, how to treat??!
+            // todo: message or entity, how to treat??!
             throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
                     "Token could not be verified", token);
         }
@@ -158,7 +163,7 @@
             if (!jwt.verify(verifier))
                 throw new KustvaktException(StatusCodes.REQUEST_INVALID,
                         "token invalid", signedContent);
-            return (String) jwt.getJWTClaimsSet().getCustomClaim("data");
+            return jwt.getJWTClaimsSet().getStringClaim("data");
         }
         catch (ParseException | JOSEException e) {
             return null;
@@ -166,19 +171,20 @@
     }
 
 
-    public TokenContext getTokenContext (String idtoken) throws ParseException,
-            JOSEException, KustvaktException {
+    public TokenContext getTokenContext (String idtoken)
+            throws ParseException, JOSEException, KustvaktException {
         SignedJWT signedJWT = verifyToken(idtoken);
 
         TokenContext c = new TokenContext();
-        c.setUsername(signedJWT.getJWTClaimsSet().getSubjectClaim());
-        if (signedJWT.getJWTClaimsSet().getAudienceClaim() != null)
-            c.addContextParameter(Attributes.CLIENT_ID, signedJWT
-                    .getJWTClaimsSet().getAudienceClaim()[0]);
-        c.setExpirationTime(signedJWT.getJWTClaimsSet()
-                .getExpirationTimeClaim());
+        c.setUsername(signedJWT.getJWTClaimsSet().getSubject());
+        List<String> audienceList = signedJWT.getJWTClaimsSet().getAudience();
+        if (audienceList != null && !audienceList.isEmpty())
+            c.addContextParameter(Attributes.CLIENT_ID,
+                    signedJWT.getJWTClaimsSet().getAudience().get(0));
+        c.setExpirationTime(
+                signedJWT.getJWTClaimsSet().getExpirationTime().getTime());
         c.setToken(idtoken);
-        c.addParams(signedJWT.getJWTClaimsSet().getCustomClaims());
+        c.addParams(signedJWT.getJWTClaimsSet().getClaims());
         return c;
     }
 
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
index 7bd4d72..ae53294 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
@@ -68,8 +68,8 @@
      */
     @POST
     @Path("authorize")
-    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     @ResourceFilters({ AuthenticationFilter.class, BlockingFilter.class })
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
     public Response requestAuthorizationCode (
             @Context HttpServletRequest request,
@@ -182,4 +182,14 @@
             throw responseHandler.throwit(e);
         }
     }
+    
+//    @POST
+//    @Path("revoke")
+//    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+//    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+//    public Response revokeAccessToken (@Context HttpServletRequest request,
+//            @FormParam("grant_type") String grantType,
+//            MultivaluedMap<String, String> form) {
+//        return null;
+//    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthController.java
index a24ffc9..9ef6de4 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthController.java
@@ -71,6 +71,7 @@
 //todo: only allow oauth2 access_token requests GET methods?
 //todo: allow refresh tokens
 //@Path("/oauth2")
+@Deprecated
 public class OAuthController {
 
     @Autowired
diff --git a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
index 81b0d92..fae3fac 100644
--- a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
@@ -2,16 +2,64 @@
 
 -- oauth2 db tables
 CREATE TABLE IF NOT EXISTS oauth2_client (
-	id VARCHAR(100) UNIQUE PRIMARY KEY,
+	id VARCHAR(100) PRIMARY KEY NOT NULL,
 	name VARCHAR(200) NOT NULL,
 	secret VARCHAR(200),
 	type VARCHAR(200) NOT NULL,
 	native BOOLEAN DEFAULT FALSE,
 	url TEXT NOT NULL,
-	url_hashcode UNIQUE INTEGER NOT NULL,
+	url_hashcode INTEGER NOT NULL,
 	redirect_uri TEXT NOT NULL,
 	registered_by VARCHAR(100) NOT NULL,
-	description VARCHAR(250) NOT NULL
+	description VARCHAR(250) NOT NULL,
+	UNIQUE INDEX unique_url(url_hashcode)
+);
+
+CREATE TABLE IF NOT EXISTS oauth2_authorization (
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	code VARCHAR(255) NOT NULL,
+	client_id VARCHAR(100) NOT NULL,
+	user_id VARCHAR(100) NOT NULL,
+	redirect_uri TEXT DEFAULT NULL,
+	created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+	is_revoked BOOLEAN DEFAULT 0,
+	total_attempts INTEGER DEFAULT 0,
+	FOREIGN KEY (client_id)
+	   REFERENCES oauth2_client(id),
+	UNIQUE INDEX authorization_index(code, client_id)
+);
+
+CREATE TABLE IF NOT EXISTS oauth2_access_scope (
+	id VARCHAR(255) PRIMARY KEY NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS oauth2_authorization_scope (
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	authorization_id INTEGER NOT NULL,
+	scope_id VARCHAR(100) NOT NULL,
+	FOREIGN KEY (authorization_id)
+	   REFERENCES oauth2_authorization(id),
+	FOREIGN KEY (scope_id)
+	   REFERENCES oauth2_access_scope(id),
+	UNIQUE INDEX authorization_scope_index(authorization_id, scope_id)
+);
+
+CREATE TABLE IF NOT EXISTS oauth2_access_token (
+	id INTEGER PRIMARY KEY AUTO_INCREMENT,
+	token VARCHAR(255) NOT NULL,
+	authorization_id INTEGER DEFAULT NULL,
+	user_id VARCHAR(100) DEFAULT NULL,
+	created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+	is_revoked BOOLEAN DEFAULT 0,
+	total_attempts INTEGER DEFAULT 0,
+	FOREIGN KEY (authorization_id)
+	   REFERENCES oauth2_authorization(id)
+);
+
+CREATE TABLE oauth2_access_token_scope (
+	token_id INTEGER NOT NULL, 
+	scope_id VARCHAR(100) NOT NULL, 
+	CONSTRAINT primary_key PRIMARY KEY (token_id, scope_id)
 );
 
 --
diff --git a/full/src/test/java/de/ids_mannheim/korap/authentication/APIAuthenticationTest.java b/full/src/test/java/de/ids_mannheim/korap/authentication/APIAuthenticationTest.java
index 0fca0b8..82bab07 100644
--- a/full/src/test/java/de/ids_mannheim/korap/authentication/APIAuthenticationTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/authentication/APIAuthenticationTest.java
@@ -9,6 +9,8 @@
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
+import com.nimbusds.jose.JOSEException;
+
 import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.config.KustvaktConfiguration;
 import de.ids_mannheim.korap.config.SpringJerseyTest;
@@ -24,8 +26,8 @@
     private KustvaktConfiguration config;
 
     @Test
-    public void testCreateGetTokenContext ()
-            throws KustvaktException, IOException, InterruptedException {
+    public void testCreateGetTokenContext () throws KustvaktException,
+            IOException, InterruptedException, JOSEException {
         User user = new KorAPUser();
         user.setUsername("testUser");
 
@@ -38,6 +40,7 @@
 
         // get token context
         String authToken = context.getToken();
+//        System.out.println(authToken);
         context = auth.getTokenContext(authToken);
 
         TokenType tokenType = context.getTokenType();
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserControllerTest.java
index 4ea6807..68f1b55 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserControllerTest.java
@@ -21,6 +21,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.nimbusds.jose.JOSEException;
 import com.nimbusds.jwt.SignedJWT;
 import com.sun.jersey.api.client.ClientResponse;
 import com.sun.jersey.core.util.MultivaluedMapImpl;
@@ -185,7 +186,7 @@
 	// EM: cannot do test with LDAP
 	@Test
 	@Ignore
-	public void loginJWTExpired() throws InterruptedException, KustvaktException, ParseException {
+	public void loginJWTExpired() throws InterruptedException, KustvaktException, ParseException, JOSEException {
 
 		assertTrue(BeansFactory.getKustvaktContext().getConfiguration().getTokenTTL() < 10);
 
@@ -205,7 +206,7 @@
 		SignedJWT jwt = sign.verifyToken(token);
 
 		while (true) {
-			if (TimeUtils.isExpired(jwt.getJWTClaimsSet().getExpirationTimeClaim()))
+			if (TimeUtils.isExpired(jwt.getJWTClaimsSet().getExpirationTime().getTime()))
 				break;
 		}
 
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
index 42f36ae..9ff8db4 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
@@ -368,7 +368,7 @@
         node = testCheckHiddenGroup(groupId);
         assertEquals(StatusCodes.GROUP_NOT_FOUND,
                 node.at("/errors/0/0").asInt());
-        assertEquals("Group with id 5 is not found",
+        assertEquals("Group with id "+groupId+" is not found",
                 node.at("/errors/0/1").asText());
     }
 
@@ -387,13 +387,13 @@
     }
 
     @Test
-    public void testCreateVCWithExpiredToken ()
+    public void testCreateVCWithInvalidToken ()
             throws IOException, KustvaktException {
         String json = "{\"name\": \"new vc\",\"type\": \"PRIVATE\","
                 + "\"corpusQuery\": \"corpusSigle=GOE\"}";
 
         InputStream is = getClass().getClassLoader()
-                .getResourceAsStream("test-user.token");
+                .getResourceAsStream("test-invalid-signature.token");
 
         String authToken;
         try (BufferedReader reader =
@@ -413,6 +413,40 @@
         assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
 
         JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.INVALID_ACCESS_TOKEN, node.at("/errors/0/0").asInt());
+        assertEquals("Json Web Signature (JWS) object verification failed.",
+                node.at("/errors/0/1").asText());
+
+        checkWWWAuthenticateHeader(response);
+    }
+    
+    @Test
+    public void testCreateVCWithExpiredToken ()
+            throws IOException, KustvaktException {
+        String json = "{\"name\": \"new vc\",\"type\": \"PRIVATE\","
+                + "\"corpusQuery\": \"corpusSigle=GOE\"}";
+
+        InputStream is = getClass().getClassLoader()
+                .getResourceAsStream("test-expired.token");
+
+        String authToken;
+        try (BufferedReader reader =
+                new BufferedReader(new InputStreamReader(is));) {
+            authToken = reader.readLine();
+        }
+
+        ClientResponse response = resource().path("vc").path("create")
+                .header(Attributes.AUTHORIZATION,
+                        AuthenticationScheme.API.displayName() + " "
+                                + authToken)
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .entity(json).post(ClientResponse.class);
+        
+        String entity = response.getEntity(String.class);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+
+        JsonNode node = JsonUtils.readTree(entity);
         assertEquals(StatusCodes.EXPIRED, node.at("/errors/0/0").asInt());
         assertEquals("Authentication token is expired",
                 node.at("/errors/0/1").asText());
diff --git a/full/src/test/resources/test-expired.token b/full/src/test/resources/test-expired.token
new file mode 100644
index 0000000..5d49da3
--- /dev/null
+++ b/full/src/test/resources/test-expired.token
@@ -0,0 +1 @@
+eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0VXNlciIsImlzcyI6Imh0dHA6XC9cL2tvcmFwLmlkcy1tYW5uaGVpbS5kZSIsImV4cCI6MTUyODMwMzE5OX0.rmEFpdm8-_iyHGb2tEaJbKBoceiZwnyodixWhyLrU9w
\ No newline at end of file
diff --git a/full/src/test/resources/test-user.token b/full/src/test/resources/test-invalid-signature.token
similarity index 100%
rename from full/src/test/resources/test-user.token
rename to full/src/test/resources/test-invalid-signature.token