Added service layer to SearchController, added OAuth2 scope handling,
fixed bugs.

Change-Id: Id6cfb5c264472d106314dbd4a485681460e67288
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java b/full/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java
index e272a34..b90406b 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/BasicAuthentication.java
@@ -78,7 +78,7 @@
             c.setToken(StringUtils.stripTokenType(authToken));
             // fixme: you can make queries, but user sensitive data is
             // off limits?!
-            c.addContextParameter(Attributes.SCOPES,
+            c.addContextParameter(Attributes.SCOPE,
                     Scopes.Scope.search.toString());
             return c;
         }
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java b/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
index f490de0..ffee80a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
@@ -47,7 +47,7 @@
         c.setExpirationTime(expiry.toInstant().toEpochMilli());
         c.setToken(authToken);
         c.setTokenType(TokenType.BEARER);
-        c.addContextParameter(Attributes.SCOPES, scopes);
+        c.addContextParameter(Attributes.SCOPE, scopes);
         c.setAuthenticationTime(accessToken.getUserAuthenticationTime());
         return c;
     }
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/Initializator.java b/full/src/main/java/de/ids_mannheim/korap/config/Initializator.java
index 6c04431..e730a0b 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/Initializator.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/Initializator.java
@@ -1,7 +1,10 @@
 package de.ids_mannheim.korap.config;
 
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Set;
 
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Scope;
 import de.ids_mannheim.korap.oauth2.dao.AccessScopeDao;
 
 /**
@@ -12,13 +15,9 @@
  */
 public class Initializator {
 
-    private FullConfiguration config;
     private AccessScopeDao accessScopeDao;
 
-
-    public Initializator (FullConfiguration config,
-                          AccessScopeDao accessScopeDao) {
-        this.config = config;
+    public Initializator (AccessScopeDao accessScopeDao) {
         this.accessScopeDao = accessScopeDao;
     }
 
@@ -27,7 +26,11 @@
     }
 
     private void setAccessScope () {
-        accessScopeDao.storeAccessScopes(config.getDefaultAccessScopes());
-        accessScopeDao.storeAccessScopes(config.getClientCredentialsScopes());
+        OAuth2Scope[] enums = OAuth2Scope.values();
+        Set<String> scopes = new HashSet<>(enums.length);
+        for (OAuth2Scope s : enums) {
+            scopes.add(s.toString());
+        }
+        accessScopeDao.storeAccessScopes(scopes);
     }
 }
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 a55a08c..49f8183 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
@@ -67,7 +67,7 @@
         Builder csBuilder = new JWTClaimsSet.Builder();
         csBuilder.issuer(this.issuer.toString());
 
-        if ((scopes = (String) attr.get(Attributes.SCOPES)) != null) {
+        if ((scopes = (String) attr.get(Attributes.SCOPE)) != null) {
             Userdata data = new GenericUserData();
             data.readQuietly(attr, false);
             Scopes claims = Scopes.mapScopes(scopes, data);
diff --git a/full/src/main/java/de/ids_mannheim/korap/handlers/OAuthDb.java b/full/src/main/java/de/ids_mannheim/korap/handlers/OAuthDb.java
index a1ae65c..838b1c0 100644
--- a/full/src/main/java/de/ids_mannheim/korap/handlers/OAuthDb.java
+++ b/full/src/main/java/de/ids_mannheim/korap/handlers/OAuthDb.java
@@ -212,8 +212,8 @@
                             c.setToken(token);
                             c.setTokenType(TokenType.BEARER);
 //                            c.setTokenType(Attributes.OAUTH2_AUTHORIZATION);
-                            c.addContextParameter(Attributes.SCOPES,
-                                    rs.getString(Attributes.SCOPES));
+                            c.addContextParameter(Attributes.SCOPE,
+                                    rs.getString(Attributes.SCOPE));
                             return c;
                         }
                     });
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Scope.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Scope.java
new file mode 100644
index 0000000..7e6c973
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Scope.java
@@ -0,0 +1,11 @@
+package de.ids_mannheim.korap.oauth2.constant;
+
+public enum OAuth2Scope {
+
+    OPENID, SEARCH, SERIALIZE_QUERY, MATCH_INFO, CREATE_VC, LIST_VC, EDIT_VC, VC_INFO, CLIENT_INFO;
+
+    @Override
+    public String toString () {
+        return super.toString().toLowerCase();
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
index 4e89d5e..ec97923 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
@@ -21,6 +21,7 @@
 import de.ids_mannheim.korap.oauth2.dao.AccessTokenDao;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.Authorization;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
 import de.ids_mannheim.korap.oauth2.service.OAuth2TokenService;
 
 @Service
@@ -39,21 +40,13 @@
         String grantType = oAuthRequest.getGrantType();
 
         if (grantType.equals(GrantType.AUTHORIZATION_CODE.toString())) {
-            Authorization authorization =
-                    retrieveAuthorization(
-                            oAuthRequest.getCode(),
-                            oAuthRequest.getRedirectURI(),
-                            oAuthRequest.getClientId(),
-                            oAuthRequest.getClientSecret());
+            Authorization authorization = retrieveAuthorization(
+                    oAuthRequest.getCode(), oAuthRequest.getRedirectURI(),
+                    oAuthRequest.getClientId(), oAuthRequest.getClientSecret());
             return createsAccessTokenResponse(authorization);
         }
         else if (grantType.equals(GrantType.PASSWORD.toString())) {
-            ZonedDateTime authenticationTime = authenticateClientAndUser(
-                    oAuthRequest.getUsername(), oAuthRequest.getPassword(),
-                    oAuthRequest.getScopes(), oAuthRequest.getClientId(),
-                    oAuthRequest.getClientSecret());
-            return createsAccessTokenResponse(oAuthRequest.getScopes(),
-                    oAuthRequest.getUsername(), authenticationTime);
+            return requestAccessTokenWithPassword(oAuthRequest);
         }
         else if (grantType.equals(GrantType.CLIENT_CREDENTIALS.toString())) {
             ZonedDateTime authenticationTime =
@@ -75,6 +68,31 @@
 
     }
 
+    private OAuthResponse requestAccessTokenWithPassword (
+            AbstractOAuthTokenRequest oAuthRequest)
+            throws KustvaktException, OAuthSystemException {
+
+        OAuth2Client client = clientService.authenticateClient(
+                oAuthRequest.getClientId(), oAuthRequest.getClientSecret());
+        if (!client.isNative()) {
+            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Password grant is not allowed for third party clients",
+                    OAuth2Error.UNAUTHORIZED_CLIENT);
+        }
+
+        Set<String> scopes = oAuthRequest.getScopes();
+        if (scopes == null || scopes.isEmpty()) {
+            scopes = config.getDefaultAccessScopes();
+        }
+
+        ZonedDateTime authenticationTime = authenticateUser(
+                oAuthRequest.getUsername(), oAuthRequest.getPassword(), scopes);
+
+        return createsAccessTokenResponse(scopes, oAuthRequest.getUsername(),
+                authenticationTime);
+
+    }
+
     /**
      * Creates an OAuthResponse containing an access token and a
      * refresh token with type Bearer.
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 eca6fa2..a61cbe1 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
@@ -47,6 +47,7 @@
 import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.Authorization;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
 import de.ids_mannheim.korap.oauth2.service.OAuth2TokenService;
 import de.ids_mannheim.korap.utils.TimeUtils;
 
@@ -98,6 +99,30 @@
         return null;
     }
 
+    /**
+     * Third party apps must not be allowed to use password grant.
+     * MH: password grant is only allowed for trusted clients (korap
+     * frontend)
+     * 
+     * According to RFC 6749, client authentication is only required
+     * for confidential clients and whenever client credentials are
+     * provided. Moreover, client_id is optional for password grant,
+     * but without it, the authentication server cannot check the
+     * client type. To make sure that confidential clients
+     * authenticate, client_id is made required (similar to
+     * authorization code grant).
+     * 
+     * @param username
+     *            username, required
+     * @param password
+     *            password, required
+     * @param scope
+     *            scope, optional
+     * @param clientAuthentication
+     * @param clientId
+     * @return
+     * @throws KustvaktException
+     */
     private AccessTokenResponse requestAccessTokenWithPassword (String username,
             String password, Scope scope,
             ClientAuthentication clientAuthentication, ClientID clientId)
@@ -111,6 +136,7 @@
 
         ZonedDateTime authenticationTime;
         String clientIdStr = null;
+        OAuth2Client client;
         if (clientAuthentication == null) {
             if (clientId == null) {
                 throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
@@ -118,18 +144,29 @@
                         OAuth2Error.INVALID_REQUEST);
             }
             else {
-                authenticationTime = authenticateClientAndUser(username,
-                        password, scopes, clientId.getValue(), null);
                 clientIdStr = clientId.getValue();
+                client = clientService.authenticateClient(clientIdStr,
+                        null);
             }
         }
         else {
             String[] clientCredentials =
                     extractClientCredentials(clientAuthentication);
             clientIdStr = clientCredentials[0];
-            authenticationTime = authenticateClientAndUser(username, password,
-                    scopes, clientCredentials[0], clientCredentials[1]);
+            client = clientService.authenticateClient(clientCredentials[0],
+                    clientCredentials[1]);
         }
+        
+        if (!client.isNative()) {
+            throw new KustvaktException(
+                    StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Password grant is not allowed for third party clients",
+                    OAuth2Error.UNAUTHORIZED_CLIENT);
+        }
+        
+        authenticationTime =
+                authenticateUser(username, password, scopes);
+        
         return createsAccessTokenResponse(scope, clientIdStr, username,
                 authenticationTime, null);
     }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
index f47fa3c..cd4eec1 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
@@ -2,6 +2,7 @@
 
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -9,11 +10,16 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.TokenType;
+import de.ids_mannheim.korap.dao.AdminDao;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Scope;
 import de.ids_mannheim.korap.oauth2.dao.AccessScopeDao;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.security.context.TokenContext;
 
 @Service
 public class OAuth2ScopeService {
@@ -21,6 +27,9 @@
     @Autowired
     private AccessScopeDao accessScopeDao;
 
+    @Autowired
+    private AdminDao adminDao;
+    
     /**
      * Converts a set of scope strings to a set of {@link AccessScope}
      * 
@@ -53,8 +62,9 @@
         Set<String> set = convertAccessScopesToStringSet(scopes);
         return String.join(" ", set);
     }
-    
-    public Set<String> convertAccessScopesToStringSet (Set<AccessScope> scopes) {
+
+    public Set<String> convertAccessScopesToStringSet (
+            Set<AccessScope> scopes) {
         Set<String> set = scopes.stream().map(scope -> scope.toString())
                 .collect(Collectors.toSet());
         return set;
@@ -76,4 +86,17 @@
                         .collect(Collectors.toSet());
         return filteredScopes;
     }
+
+    public void verifyScope (TokenContext context, OAuth2Scope requestScope)
+            throws KustvaktException {
+        if (!adminDao.isAdmin(context.getUsername())
+                && context.getTokenType().equals(TokenType.BEARER)) {
+            Map<String, Object> parameters = context.getParameters();
+            String scope = (String) parameters.get(Attributes.SCOPE);
+            if (!scope.contains(requestScope.toString())) {
+                throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                        "Scope " + requestScope + " is not authorized");
+            }
+        }
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
index d36a077..d6c11d9 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
@@ -6,6 +6,7 @@
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -16,7 +17,6 @@
 import de.ids_mannheim.korap.interfaces.AuthenticationManagerIface;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
 import de.ids_mannheim.korap.oauth2.entity.Authorization;
-import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
 
 /**
  * OAuth2TokenService manages business logic related to OAuth2
@@ -29,7 +29,7 @@
 public class OAuth2TokenService {
 
     @Autowired
-    private OAuth2ClientService clientService;
+    protected OAuth2ClientService clientService;
 
     @Autowired
     private OAuth2AuthorizationService authorizationService;
@@ -107,21 +107,6 @@
      * @throws KustvaktException
      * @throws OAuthSystemException
      */
-    protected ZonedDateTime authenticateClientAndUser (String username,
-            String password, Set<String> scopes, String clientId,
-            String clientSecret) throws KustvaktException {
-
-        OAuth2Client client =
-                clientService.authenticateClient(clientId, clientSecret);
-        if (!client.isNative()) {
-            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
-                    "Password grant is not allowed for third party clients",
-                    OAuth2Error.UNAUTHORIZED_CLIENT);
-        }
-
-        return authenticateUser(username, password, scopes);
-        // verify or limit scopes ?
-    }
 
     public ZonedDateTime authenticateUser (String username, String password,
             Set<String> scopes) throws KustvaktException {
@@ -136,7 +121,7 @@
 
         Map<String, Object> attributes = new HashMap<>();
         if (scopes != null && !scopes.isEmpty()) {
-            attributes.put(Attributes.SCOPES, scopes);
+            attributes.put(Attributes.SCOPE, scopes);
         }
         authenticationManager.authenticate(
                 config.getOAuth2passwordAuthentication(), username, password,
diff --git a/full/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java b/full/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java
index 851631a..b31c643 100644
--- a/full/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java
+++ b/full/src/main/java/de/ids_mannheim/korap/security/context/TokenContext.java
@@ -41,7 +41,7 @@
     private String token;
     private boolean secureRequired;
 
-    @Getter(AccessLevel.PRIVATE)
+//    @Getter(AccessLevel.PRIVATE)
     @Setter(AccessLevel.PRIVATE)
     private Map<String, Object> parameters;
     private String hostAddress;
@@ -72,8 +72,7 @@
     public Map<String, Object> params () {
         return new HashMap<>(parameters);
     }
-
-
+    
     public boolean match (TokenContext other) {
         if (other.getToken().equals(this.token))
             if (this.getHostAddress().equals(this.hostAddress))
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/SearchService.java b/full/src/main/java/de/ids_mannheim/korap/service/SearchService.java
new file mode 100644
index 0000000..1aea369
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/service/SearchService.java
@@ -0,0 +1,231 @@
+package de.ids_mannheim.korap.service;
+
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import javax.annotation.PostConstruct;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.UriBuilder;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.sun.jersey.core.util.MultivaluedMapImpl;
+
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.interfaces.AuthenticationManagerIface;
+import de.ids_mannheim.korap.query.serialize.MetaQueryBuilder;
+import de.ids_mannheim.korap.query.serialize.QuerySerializer;
+import de.ids_mannheim.korap.rewrite.FullRewriteHandler;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.user.User.CorpusAccess;
+import de.ids_mannheim.korap.web.ClientsHandler;
+import de.ids_mannheim.korap.web.SearchKrill;
+
+@Service
+public class SearchService {
+
+    private static Logger jlog = LogManager.getLogger(SearchService.class);
+
+    @Autowired
+    private FullConfiguration config;
+
+    @Autowired
+    private AuthenticationManagerIface authManager;
+
+    @Autowired
+    private FullRewriteHandler rewriteHandler;
+
+    @Autowired
+    private SearchKrill searchKrill;
+
+    private ClientsHandler graphDBhandler;
+
+    @PostConstruct
+    private void doPostConstruct () {
+        this.rewriteHandler.defaultRewriteConstraints();
+
+        UriBuilder builder = UriBuilder.fromUri("http://10.0.10.13").port(9997);
+        this.graphDBhandler = new ClientsHandler(builder.build());
+    }
+
+    public String serializeQuery (String q, String ql, String v, String cq,
+            Integer pageIndex, Integer startPage, Integer pageLength,
+            String context, Boolean cutoff) {
+        QuerySerializer ss = new QuerySerializer().setQuery(q, ql, v);
+        if (cq != null) ss.setCollection(cq);
+
+        MetaQueryBuilder meta = new MetaQueryBuilder();
+        if (pageIndex != null) meta.addEntry("startIndex", pageIndex);
+        if (pageIndex == null && startPage != null)
+            meta.addEntry("startPage", startPage);
+        if (pageLength != null) meta.addEntry("count", pageLength);
+        if (context != null) meta.setSpanContext(context);
+        meta.addEntry("cutOff", cutoff);
+
+        ss.setMeta(meta.raw());
+        return ss.toJSON();
+    }
+
+    private User createUser (String username, HttpHeaders headers)
+            throws KustvaktException {
+        User user = authManager.getUser(username);
+        authManager.setAccessAndLocation(user, headers);
+        jlog.debug("Debug: /getMatchInfo/: location=" + user.locationtoString()
+                + ", access=" + user.accesstoString());
+        return user;
+    }
+
+    public String search (String jsonld) {
+        // MH: todo: should be possible to add the meta part to
+        // the query serialization
+        // User user = controller.getUser(ctx.getUsername());
+        // jsonld = this.processor.processQuery(jsonld, user);
+        return searchKrill.search(jsonld);
+    }
+
+    public String search (String engine, String username, HttpHeaders headers,
+            String q, String ql, String v, String cq, Integer pageIndex,
+            Integer pageInteger, String ctx, Integer pageLength, Boolean cutoff)
+            throws KustvaktException {
+
+        KustvaktConfiguration.BACKENDS eng = this.config.chooseBackend(engine);
+        User user = createUser(username, headers);
+
+        QuerySerializer serializer = new QuerySerializer();
+        serializer.setQuery(q, ql, v);
+        if (cq != null) serializer.setCollection(cq);
+
+        MetaQueryBuilder meta = createMetaQuery(pageIndex, pageInteger, ctx,
+                pageLength, cutoff);
+        serializer.setMeta(meta.raw());
+
+        // There is an error in query processing
+        // - either query, corpus or meta
+        if (serializer.hasErrors()) {
+            throw new KustvaktException(serializer.toJSON());
+        }
+
+        String query = this.rewriteHandler.processQuery(serializer.toJSON(), user);
+        jlog.info("the serialized query " + query);
+
+        String result;
+        if (eng.equals(KustvaktConfiguration.BACKENDS.NEO4J)) {
+            result = searchNeo4J(query, pageLength, meta, false);
+        }
+        else {
+            result = searchKrill.search(query);
+        }
+        jlog.debug("Query result: " + result);
+        return result;
+
+    }
+
+    private MetaQueryBuilder createMetaQuery (Integer pageIndex,
+            Integer pageInteger, String ctx, Integer pageLength,
+            Boolean cutoff) {
+        MetaQueryBuilder meta = new MetaQueryBuilder();
+        meta.addEntry("startIndex", pageIndex);
+        meta.addEntry("startPage", pageInteger);
+        meta.setSpanContext(ctx);
+        meta.addEntry("count", pageLength);
+        // todo: what happened to cutoff?
+        meta.addEntry("cutOff", cutoff);
+        // meta.addMeta(pageIndex, pageInteger, pageLength, ctx,
+        // cutoff);
+        // fixme: should only apply to CQL queries per default!
+        // meta.addEntry("itemsPerResource", 1);
+        return meta;
+    }
+
+    private String searchNeo4J (String query, int pageLength,
+            MetaQueryBuilder meta, boolean raw) throws KustvaktException {
+
+        if (raw) {
+            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
+                    "raw not supported!");
+        }
+
+        MultivaluedMap<String, String> map = new MultivaluedMapImpl();
+        map.add("q", query);
+        map.add("count", String.valueOf(pageLength));
+        map.add("lctxs", String.valueOf(meta.getSpanContext().getLeftSize()));
+        map.add("rctxs", String.valueOf(meta.getSpanContext().getRightSize()));
+        return this.graphDBhandler.getResponse(map, "distKwic");
+
+    }
+
+    public String retrieveMatchInfo (String corpusId, String docId,
+            String textId, String matchId, Set<String> foundries,
+            String username, HttpHeaders headers, Set<String> layers,
+            boolean spans) throws KustvaktException {
+        String matchid =
+                searchKrill.getMatchId(corpusId, docId, textId, matchId);
+
+        User user = createUser(username, headers);
+        CorpusAccess corpusAccess = user.getCorpusAccess();
+        Pattern p;
+        switch (corpusAccess) {
+            case PUB:
+                p = config.getPublicLicensePattern();
+                break;
+            case ALL:
+                p = config.getAllLicensePattern();
+                break;
+            default: // FREE
+                p = config.getFreeLicensePattern();
+                break;
+        }
+
+        boolean match_only = foundries == null || foundries.isEmpty();
+        String results;
+        try {
+            if (!match_only) {
+
+                ArrayList<String> foundryList = new ArrayList<String>();
+                ArrayList<String> layerList = new ArrayList<String>();
+
+                // EM: now without user, just list all foundries and
+                // layers
+                if (foundries.contains("*")) {
+                    foundryList = config.getFoundries();
+                    layerList = config.getLayers();
+                }
+                else {
+                    foundryList.addAll(foundries);
+                    layerList.addAll(layers);
+                }
+
+                results = searchKrill.getMatch(matchid, foundryList, layerList,
+                        spans, false, true, p);
+            }
+            else {
+                results = searchKrill.getMatch(matchid, p);
+            }
+        }
+        catch (Exception e) {
+            jlog.error("Exception in the MatchInfo service encountered!", e);
+            throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
+                    e.getMessage());
+        }
+        jlog.debug("MatchInfo results: " + results);
+        return results;
+    }
+
+    public String retrieveDocMetadata (String corpusId, String docId,
+            String textId) {
+        String textSigle = searchKrill.getTextSigle(corpusId, docId, textId);
+        return searchKrill.getFields(textSigle);
+    }
+    
+    public String getCollocationBase (String query) throws KustvaktException {
+        return graphDBhandler.getResponse("distCollo", "q", query);
+    }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java b/full/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
index f7bef6a..bce194c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
@@ -12,7 +12,9 @@
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.interfaces.db.AuditingIface;
 
-/** KustvaktResponseHandler includes exceptions regarding authorization. 
+/**
+ * KustvaktResponseHandler includes exceptions regarding
+ * authorization.
  * 
  * @author margaretha
  *
@@ -27,7 +29,12 @@
     public WebApplicationException throwit (KustvaktException e) {
         Response r;
 
-        if (e.getStatusCode() == StatusCodes.USER_REAUTHENTICATION_REQUIRED
+        // KustvaktException just wraps another exception
+        if (e.getStatusCode() == null && e.hasNotification()) {
+            r = Response.status(Response.Status.BAD_REQUEST)
+                    .entity(e.getNotification()).build();
+        }
+        else if (e.getStatusCode() == StatusCodes.USER_REAUTHENTICATION_REQUIRED
                 || e.getStatusCode() == StatusCodes.AUTHORIZATION_FAILED
                 || e.getStatusCode() >= StatusCodes.AUTHENTICATION_FAILED) {
             String notification = buildNotification(e.getStatusCode(),
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
index 9c01fa5..0371c9e 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
@@ -245,7 +245,7 @@
 
         Map<String, Object> attr = new HashMap<>();
         if (scopes != null && !scopes.isEmpty())
-            attr.put(Attributes.SCOPES, scopes);
+            attr.put(Attributes.SCOPE, scopes);
         attr.put(Attributes.HOST, host);
         attr.put(Attributes.USER_AGENT, agent);
 
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 2d76a18..2c43acc 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
@@ -224,7 +224,7 @@
             StringBuilder scopes = new StringBuilder();
             for (String scope : oauthRequest.getScopes())
                 scopes.append(scope + " ");
-            attr.put(Attributes.SCOPES, scopes.toString());
+            attr.put(Attributes.SCOPE, scopes.toString());
 
             try {
                 user = controller.getUser(c.getUsername());
@@ -486,7 +486,7 @@
             Map<String, Object> attr = new HashMap<>();
             attr.put(Attributes.HOST, host);
             attr.put(Attributes.USER_AGENT, agent);
-            attr.put(Attributes.SCOPES,
+            attr.put(Attributes.SCOPE,
                     StringUtils.toString(oauthRequest.getScopes(), " "));
 
             // support code (for external clients only) and password grant type
@@ -534,7 +534,7 @@
                 }
                 // todo: errors for invalid scopes or different scopes then during authorization request?
                 //todo ??
-                attr.put(Attributes.SCOPES, codeInfo.getScopes());
+                attr.put(Attributes.SCOPE, codeInfo.getScopes());
 
             }
             else if (oauthRequest.getGrantType().equalsIgnoreCase(
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/ResourceController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/ResourceController.java
index a603489..0bc87a4 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/ResourceController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/ResourceController.java
@@ -6,7 +6,6 @@
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
@@ -14,9 +13,7 @@
 import com.sun.jersey.spi.container.ResourceFilters;
 
 import de.ids_mannheim.korap.dto.ResourceDto;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.service.ResourceService;
-import de.ids_mannheim.korap.utils.JsonUtils;
 import de.ids_mannheim.korap.web.filter.PiwikFilter;
 
 /**
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/SearchController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/SearchController.java
index d78eaea..d3481ec 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/SearchController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/SearchController.java
@@ -1,15 +1,10 @@
 package de.ids_mannheim.korap.web.controller;// package
                                              // de.ids_mannheim.korap.ext.web;
 
-import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Locale;
-import java.util.Map;
 import java.util.Set;
-import java.util.regex.Pattern;
 
-import javax.annotation.PostConstruct;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
@@ -19,10 +14,8 @@
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.SecurityContext;
-import javax.ws.rs.core.UriBuilder;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -30,93 +23,211 @@
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.RequestMapping;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import com.sun.jersey.core.util.MultivaluedMapImpl;
 import com.sun.jersey.spi.container.ResourceFilters;
 
-import de.ids_mannheim.korap.cache.ResourceCache;
-import de.ids_mannheim.korap.config.Attributes;
-import de.ids_mannheim.korap.config.FullConfiguration;
-import de.ids_mannheim.korap.config.KustvaktConfiguration;
-import de.ids_mannheim.korap.config.KustvaktConfiguration.BACKENDS;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.interfaces.AuthenticationManagerIface;
-import de.ids_mannheim.korap.query.serialize.MetaQueryBuilder;
-import de.ids_mannheim.korap.query.serialize.QuerySerializer;
-import de.ids_mannheim.korap.resources.Corpus;
-import de.ids_mannheim.korap.resources.KustvaktResource;
-import de.ids_mannheim.korap.resources.ResourceFactory;
-import de.ids_mannheim.korap.resources.VirtualCollection;
-import de.ids_mannheim.korap.rewrite.FullRewriteHandler;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Scope;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
 import de.ids_mannheim.korap.security.context.TokenContext;
-import de.ids_mannheim.korap.user.User;
-import de.ids_mannheim.korap.user.User.CorpusAccess;
-import de.ids_mannheim.korap.utils.JsonUtils;
-import de.ids_mannheim.korap.utils.KoralCollectionQueryBuilder;
-import de.ids_mannheim.korap.utils.StringUtils;
-import de.ids_mannheim.korap.web.ClientsHandler;
-import de.ids_mannheim.korap.web.CoreResponseHandler;
-import de.ids_mannheim.korap.web.SearchKrill;
+import de.ids_mannheim.korap.service.SearchService;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
 import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
 import de.ids_mannheim.korap.web.filter.DemoUserFilter;
 import de.ids_mannheim.korap.web.filter.PiwikFilter;
 
 /**
  * 
- * @author hanl, margaretha
+ * @author hanl, margaretha, diewald
  * @date 29/01/2014
- * @lastUpdate 01/2018
+ * @lastUpdate 09/07/2018
  * 
- * removed deprecated codes
  */
 @Controller
 @Path("/")
 @RequestMapping("/")
-@ResourceFilters({ AuthenticationFilter.class, DemoUserFilter.class, PiwikFilter.class })
+@ResourceFilters({ AuthenticationFilter.class, DemoUserFilter.class,
+        PiwikFilter.class })
 @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
 public class SearchController {
 
-    private static Logger jlog =
-            LogManager.getLogger(SearchController.class);
+    private static Logger jlog = LogManager.getLogger(SearchController.class);
 
     @Autowired
-    private CoreResponseHandler kustvaktResponseHandler;
-    @Autowired
-    private SearchKrill searchKrill;
-    private ResourceCache resourceHandler;
-    @Autowired
-    private AuthenticationManagerIface controller;
-    private ClientsHandler graphDBhandler;
-    @Autowired
-    private FullConfiguration config;
-    @Autowired
-    private FullRewriteHandler processor;
+    private KustvaktResponseHandler kustvaktResponseHandler;
 
+    @Autowired
+    private SearchService searchService;
+    @Autowired
+    private OAuth2ScopeService scopeService;
 
-    public SearchController () {
-        this.resourceHandler = new ResourceCache();
-        UriBuilder builder = UriBuilder.fromUri("http://10.0.10.13").port(9997);
-        this.graphDBhandler = new ClientsHandler(builder.build());
+    /**
+     * Builds a json query serialization from the given parameters.
+     * 
+     * @param locale
+     * @param securityContext
+     * @param q
+     *            query string
+     * @param ql
+     *            query language
+     * @param v
+     *            version
+     * @param context
+     * @param cutoff
+     *            true if the number of results should be limited
+     * @param pageLength
+     *            number of results per page
+     * @param pageIndex
+     * @param startPage
+     * @param cq
+     *            corpus query
+     * @return
+     */
+    // ref query parameter removed!
+    @GET
+    @Path("query")
+    public Response serializeQuery (@Context Locale locale,
+            @Context SecurityContext securityContext, @QueryParam("q") String q,
+            @QueryParam("ql") String ql, @QueryParam("v") String v,
+            @QueryParam("context") String context,
+            @QueryParam("cutoff") Boolean cutoff,
+            @QueryParam("count") Integer pageLength,
+            @QueryParam("offset") Integer pageIndex,
+            @QueryParam("page") Integer startPage,
+            @QueryParam("cq") String cq) {
+        TokenContext ctx = (TokenContext) securityContext.getUserPrincipal();
+        try {
+            scopeService.verifyScope(ctx, OAuth2Scope.SERIALIZE_QUERY);
+        }
+        catch (KustvaktException e) {
+            kustvaktResponseHandler.throwit(e);
+        }
+
+        String result = searchService.serializeQuery(q, ql, v, cq, pageIndex,
+                startPage, pageLength, context, cutoff);
+        jlog.debug("Query: " + result);
+        return Response.ok(result).build();
     }
 
-    @PostConstruct
-    private void doPostConstruct () {
-        this.processor.defaultRewriteConstraints();
+    @POST
+    @Path("search")
+    public Response searchPost (@Context SecurityContext context,
+            @Context Locale locale, @QueryParam("engine") String engine,
+            String jsonld) {
+        TokenContext ctx = (TokenContext) context.getUserPrincipal();
+        try {
+            scopeService.verifyScope(ctx, OAuth2Scope.SEARCH);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+        jlog.debug("Serialized search: " + jsonld);
+        String result = searchService.search(jsonld);
+        jlog.debug("The result set: " + result);
+        return Response.ok(result).build();
+    }
+
+
+    @GET
+    @Path("search")
+    public Response searchGet (@Context SecurityContext securityContext,
+            @Context HttpHeaders headers, @Context Locale locale,
+            @QueryParam("q") String q, @QueryParam("ql") String ql,
+            @QueryParam("v") String v, @QueryParam("context") String ctx,
+            @QueryParam("cutoff") Boolean cutoff,
+            @QueryParam("count") Integer pageLength,
+            @QueryParam("offset") Integer pageIndex,
+            @QueryParam("page") Integer pageInteger,
+            @QueryParam("cq") String cq, @QueryParam("engine") String engine) {
+
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+
+        String result;
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.SEARCH);
+            result = searchService.search(engine, context.getUsername(),
+                    headers, q, ql, v, cq, pageIndex, pageInteger, ctx,
+                    pageLength, cutoff);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+
+        return Response.ok(result).build();
+    }
+
+    @GET
+    @Path("/corpus/{corpusId}/{docId}/{textId}/{matchId}/matchInfo")
+    public Response getMatchInfo (@Context SecurityContext ctx,
+            @Context HttpHeaders headers, @Context Locale locale,
+            @PathParam("corpusId") String corpusId,
+            @PathParam("docId") String docId,
+            @PathParam("textId") String textId,
+            @PathParam("matchId") String matchId,
+            @QueryParam("foundry") Set<String> foundries,
+            @QueryParam("layer") Set<String> layers,
+            @QueryParam("spans") Boolean spans) throws KustvaktException {
+
+        TokenContext tokenContext = (TokenContext) ctx.getUserPrincipal();
+        scopeService.verifyScope(tokenContext, OAuth2Scope.MATCH_INFO);
+        spans = spans != null ? spans : false;
+        if (layers == null || layers.isEmpty()) layers = new HashSet<>();
+
+        String results = searchService.retrieveMatchInfo(corpusId, docId,
+                textId, matchId, foundries, tokenContext.getUsername(), headers,
+                layers, spans);
+        return Response.ok(results).build();
+    }
+
+
+    /*
+     * Returns the meta data fields of a certain document
+     */
+    // This is currently identical to LiteService#getMeta(),
+    // but may need auth code to work following policies
+    @GET
+    @Path("/corpus/{corpusId}/{docId}/{textId}")
+    public Response getMetadata (@PathParam("corpusId") String corpusId,
+            @PathParam("docId") String docId, @PathParam("textId") String textId
+    // @QueryParam("fields") Set<String> fields
+    ) throws KustvaktException {
+        String results =
+                searchService.retrieveDocMetadata(corpusId, docId, textId);
+        return Response.ok(results).build();
+    }
+
+
+    @POST
+    @Path("colloc")
+    public Response getCollocationBase (@QueryParam("q") String query) {
+        String result;
+        try {
+            result = searchService.getCollocationBase(query);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        return Response.ok(result).build();
     }
 
     // @GET
     // @Path("colloc")
-    // public Response getCollocationsAll(@Context SecurityContext ctx,
+    // public Response getCollocationsAll(@Context SecurityContext
+    // ctx,
     // @Context Locale locale, @QueryParam("props") String properties,
     // @QueryParam("sfskip") Integer sfs,
-    // @QueryParam("sflimit") Integer limit, @QueryParam("q") String query,
-    // @QueryParam("ql") String ql, @QueryParam("context") Integer context,
+    // @QueryParam("sflimit") Integer limit, @QueryParam("q") String
+    // query,
+    // @QueryParam("ql") String ql, @QueryParam("context") Integer
+    // context,
     // @QueryParam("foundry") String foundry,
     // @QueryParam("paths") Boolean wPaths) {
-    // TokenContext tokenContext = (TokenContext) ctx.getUserPrincipal();
+    // TokenContext tokenContext = (TokenContext)
+    // ctx.getUserPrincipal();
     // ColloQuery.ColloQueryBuilder builder;
-    // KoralCollectionQueryBuilder cquery = new KoralCollectionQueryBuilder();
+    // KoralCollectionQueryBuilder cquery = new
+    // KoralCollectionQueryBuilder();
     // String result;
     // try {
     // User user = controller.getUser(tokenContext.getUsername());
@@ -135,7 +246,8 @@
     // }catch (KustvaktException e) {
     // throw KustvaktResponseHandler.throwit(e);
     // }catch (JsonProcessingException e) {
-    // throw KustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT);
+    // throw
+    // KustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT);
     // }
     // return Response.ok(result).build();
     // }
@@ -143,7 +255,8 @@
 
     // /**
     // * @param locale
-    // * @param properties a json object string containing field, op and value
+    // * @param properties a json object string containing field, op
+    // and value
     // for the query
     // * @param query
     // * @param context
@@ -154,18 +267,23 @@
     // public Response getCollocations(@Context SecurityContext ctx,
     // @Context Locale locale, @QueryParam("props") String properties,
     // @QueryParam("sfskip") Integer sfs,
-    // @QueryParam("sflimit") Integer limit, @QueryParam("q") String query,
-    // @QueryParam("ql") String ql, @QueryParam("context") Integer context,
+    // @QueryParam("sflimit") Integer limit, @QueryParam("q") String
+    // query,
+    // @QueryParam("ql") String ql, @QueryParam("context") Integer
+    // context,
     // @QueryParam("foundry") String foundry,
-    // @QueryParam("paths") Boolean wPaths, @PathParam("id") String id,
+    // @QueryParam("paths") Boolean wPaths, @PathParam("id") String
+    // id,
     // @PathParam("type") String type) {
     // ColloQuery.ColloQueryBuilder builder;
     // type = StringUtils.normalize(type);
     // id = StringUtils.decodeHTML(id);
-    // TokenContext tokenContext = (TokenContext) ctx.getUserPrincipal();
+    // TokenContext tokenContext = (TokenContext)
+    // ctx.getUserPrincipal();
     // String result;
     // try {
-    // KoralCollectionQueryBuilder cquery = new KoralCollectionQueryBuilder();
+    // KoralCollectionQueryBuilder cquery = new
+    // KoralCollectionQueryBuilder();
     // try {
     // User user = controller.getUser(tokenContext.getUsername());
     //
@@ -199,483 +317,13 @@
     // .getResponse("distCollo", "q", builder.build().toJSON());
     //
     // }catch (JsonProcessingException e) {
-    // throw KustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT);
+    // throw
+    // KustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT);
     // }catch (KustvaktException e) {
     // throw KustvaktResponseHandler.throwit(e);
     // }
     //
     // return Response.ok(result).build();
     // }
-    @POST
-    @Path("colloc")
-    public Response getCollocationBase (@QueryParam("q") String query) {
-        String result;
-        try {
-            result = graphDBhandler.getResponse("distCollo", "q", query);
-        }
-        catch (KustvaktException e) {
-            throw kustvaktResponseHandler.throwit(e);
-        }
-        return Response.ok(result).build();
-    }
-
-
-    /** Builds a json query serialization from the given parameters.
-     * 
-     * @param locale
-     * @param securityContext
-     * @param q query string
-     * @param ql query language
-     * @param v version
-     * @param context
-     * @param cutoff true if the number of results should be limited
-     * @param pageLength number of results per page
-     * @param pageIndex
-     * @param startPage
-     * @param cq corpus query
-     * @return
-     */
-    // ref query parameter removed!
-    // EM: change the HTTP method to from TRACE to GET
-    // EM: change path from search to query
-    @GET
-    @Path("query")
-    public Response serializeQuery (@Context Locale locale,
-            @Context SecurityContext securityContext, @QueryParam("q") String q,
-            @QueryParam("ql") String ql, @QueryParam("v") String v,
-            @QueryParam("context") String context,
-            @QueryParam("cutoff") Boolean cutoff,
-            @QueryParam("count") Integer pageLength,
-            @QueryParam("offset") Integer pageIndex,
-            @QueryParam("page") Integer startPage,
-            @QueryParam("cq") String cq) {
-        TokenContext ctx = (TokenContext) securityContext.getUserPrincipal();
-        QuerySerializer ss = new QuerySerializer().setQuery(q, ql, v);
-        if (cq != null) ss.setCollection(cq);
-
-        MetaQueryBuilder meta = new MetaQueryBuilder();
-        if (pageIndex != null) meta.addEntry("startIndex", pageIndex);
-        if (pageIndex == null && startPage != null)
-            meta.addEntry("startPage", startPage);
-        if (pageLength != null) meta.addEntry("count", pageLength);
-        if (context != null) meta.setSpanContext(context);
-        meta.addEntry("cutOff", cutoff);
-
-        ss.setMeta(meta.raw());
-        String result = ss.toJSON();
-        jlog.debug("Query: " + result);
-        return Response.ok(result).build();
-    }
-
-
-    /**
-     * currently only supports either no reference at all in which
-     * case all corpora are retrieved or a corpus name like "WPD".
-     * No virtual collections supported!
-     * 
-     * @param locale
-     * @param q
-     * @param ql
-     * @param v
-     * @param pageLength
-     * @param pageIndex
-     * @return
-     */
-
-    // todo: does cq have any sensible worth here? --> would say no! --> is
-    // useful in non type/id scenarios
-
-    /* EM: potentially an unused service! */
-    // EM: build query using the given virtual collection id
-    // EM: change the HTTP method to from TRACE to GET
-    // EM: change path from search to query
-    // EM: there is no need to check resource licenses since the service just serialize a query serialization
-    @GET
-    @Path("{type}/{id}/query")
-    public Response serializeQueryWithResource (@Context Locale locale,
-            @Context SecurityContext securityContext, @QueryParam("q") String q,
-            @QueryParam("ql") String ql, @QueryParam("v") String v,
-            @QueryParam("context") String context,
-            @QueryParam("cutoff") Boolean cutoff,
-            @QueryParam("count") Integer pageLength,
-            @QueryParam("offset") Integer pageIndex,
-            @QueryParam("page") Integer startPage,
-            @PathParam("type") String type, @PathParam("id") String id) {
-        TokenContext ctx = (TokenContext) securityContext.getUserPrincipal();
-        type = StringUtils.normalize(type);
-        id = StringUtils.decodeHTML(id);
-
-        QuerySerializer ss = new QuerySerializer().setQuery(q, ql, v);
-
-        MetaQueryBuilder meta = new MetaQueryBuilder();
-        if (pageIndex != null) meta.addEntry("startIndex", pageIndex);
-        if (pageIndex == null && startPage != null)
-            meta.addEntry("startPage", startPage);
-        if (pageLength != null) meta.addEntry("count", pageLength);
-        if (context != null) meta.setSpanContext(context);
-        if (cutoff != null) meta.addEntry("cutOff", cutoff);
-
-        ss.setMeta(meta.raw());
-
-        KoralCollectionQueryBuilder cquery = new KoralCollectionQueryBuilder();
-        try {
-            cquery.setBaseQuery(ss.toJSON());
-        }
-        catch (KustvaktException e1) {
-            throw kustvaktResponseHandler.throwit(e1);
-        }
-
-        String query = "";
-        // EM: is this necessary at all?
-        KustvaktResource resource = isCollectionIdValid(ctx.getName(), id);
-        if (resource != null) {
-            try {
-                if (resource instanceof VirtualCollection) {
-                    JsonNode node = cquery.and().mergeWith(resource.getData());
-                    query = JsonUtils.toJSON(node);
-                }
-                else if (resource instanceof Corpus) {
-                    cquery.and().with(Attributes.CORPUS_SIGLE, "=",
-                            resource.getPersistentID());
-
-                    query = cquery.toJSON();
-                }
-            }
-            catch (KustvaktException e) {
-                throw kustvaktResponseHandler.throwit(e);
-            }
-        }
-
-        jlog.debug("Query: " + query);
-        return Response.ok(query).build();
-    }
-
-    // EM: prototype
-    private KustvaktResource isCollectionIdValid (String username,
-            String collectionId) {
-
-        //        try {
-        //            if (ctx.isDemo()) {
-        //                // EM: FIX ME: Is there public VCs? set default username 
-        // for nonlogin user, change demo? 
-        //                Set set = ResourceFinder.searchPublicFiltered(
-        //                        ResourceFactory.getResourceClass(type), id);
-        //                resource = (KustvaktResource) set.toArray()[0];
-        //            }
-        //            else {
-        //                // EM: FIX ME: search in user VC
-        //                User user = controller.getUser(ctx.getUsername());
-        //                if (StringUtils.isInteger(id))
-        //                    resource = this.resourceHandler
-        //                            .findbyIntId(Integer.valueOf(id), user);
-        //                else
-        //                    resource = this.resourceHandler.findbyStrId(id, user,
-        //                            ResourceFactory.getResourceClass(type));
-        //            }
-        //        }
-        //        // todo: instead of throwing exception, build notification and rewrites
-        //        // into result query
-        //        catch (KustvaktException e) {
-        //            jlog.error("Exception encountered: {}", e.string());
-        //            throw KustvaktResponseHandler.throwit(e);
-        //        }
-
-        return null;
-    }
-
-
-    @POST
-    @Path("search")
-    public Response queryRaw (@Context SecurityContext context,
-            @Context Locale locale, @QueryParam("engine") String engine,
-            String jsonld) {
-        TokenContext ctx = (TokenContext) context.getUserPrincipal();
-
-        // todo: should be possible to add the meta part to the query
-        // serialization
-        try {
-            User user = controller.getUser(ctx.getUsername());
-            // jsonld = this.processor.processQuery(jsonld, user);
-        }
-        catch (KustvaktException e) {
-            throw kustvaktResponseHandler.throwit(e);
-        }
-        jlog.info("Serialized search: "+ jsonld);
-
-        String result = searchKrill.search(jsonld);
-        // todo: logging
-        jlog.trace("The result set: "+ result);
-        return Response.ok(result).build();
-    }
-
-
-    @GET
-    @Path("search")
-    public Response search (@Context SecurityContext securityContext,
-            @Context HttpHeaders headers, @Context Locale locale,
-            @QueryParam("q") String q, @QueryParam("ql") String ql,
-            @QueryParam("v") String v, @QueryParam("context") String ctx,
-            @QueryParam("cutoff") Boolean cutoff,
-            @QueryParam("count") Integer pageLength,
-            @QueryParam("offset") Integer pageIndex,
-            @QueryParam("page") Integer pageInteger,
-            @QueryParam("cq") String cq, @QueryParam("engine") String engine) {
-
-        TokenContext context =
-                (TokenContext) securityContext.getUserPrincipal();
-        KustvaktConfiguration.BACKENDS eng = this.config.chooseBackend(engine);
-        User user;
-        try {
-            user = controller.getUser(context.getUsername());
-            controller.setAccessAndLocation(user, headers);
-            //            System.out.printf("Debug: /search/: location=%s, access='%s'.\n", user.locationtoString(), user.accesstoString());
-        }
-        catch (KustvaktException e) {
-            jlog.error("Failed retrieving user in the search service: "+
-                    e.string());
-            throw kustvaktResponseHandler.throwit(e);
-        }
-
-        QuerySerializer serializer = new QuerySerializer();
-        serializer.setQuery(q, ql, v);
-        if (cq != null) serializer.setCollection(cq);
-
-        MetaQueryBuilder meta = createMetaQuery(pageIndex, pageInteger, ctx,
-                pageLength, cutoff);
-        serializer.setMeta(meta.raw());
-
-		// There is an error in query processing
-		// - either query, corpus or meta
-		if (serializer.hasErrors()) {
-
-			// Do not pass further to backend
-			return Response.status(Response.Status.BAD_REQUEST).entity(serializer.toJSON()).build();		
-		};
-		
-        String query;
-        try {
-            query = this.processor.processQuery(serializer.toJSON(), user);
-            jlog.info("the serialized query "+ query);
-        }
-        catch (KustvaktException e) {
-            throw kustvaktResponseHandler.throwit(e);
-        }
-
-        String result = doSearch(eng, query, pageLength, meta);
-        jlog.debug("Query result: " + result);
-        return Response.ok(result).build();
-    }
-
-
-    private MetaQueryBuilder createMetaQuery (Integer pageIndex,
-            Integer pageInteger, String ctx, Integer pageLength,
-            Boolean cutoff) {
-        MetaQueryBuilder meta = new MetaQueryBuilder();
-        meta.addEntry("startIndex", pageIndex);
-        meta.addEntry("startPage", pageInteger);
-        meta.setSpanContext(ctx);
-        meta.addEntry("count", pageLength);
-        // todo: what happened to cutoff?
-        meta.addEntry("cutOff", cutoff);
-        // meta.addMeta(pageIndex, pageInteger, pageLength, ctx, cutoff);
-        // fixme: should only apply to CQL queries per default!
-        // meta.addEntry("itemsPerResource", 1);
-        return meta;
-    }
-
-
-    private String doSearch (BACKENDS eng, String query, Integer pageLength,
-            MetaQueryBuilder meta) {
-        String result;
-        if (eng.equals(KustvaktConfiguration.BACKENDS.NEO4J)) {
-            result = searchNeo4J(query, pageLength, meta, false);
-        }
-        else {
-            result = searchKrill.search(query);
-        }
-        jlog.trace("The result set: "+ result);
-        return result;
-
-    }
-
-
-    private String searchNeo4J (String query, int pageLength,
-            MetaQueryBuilder meta, boolean raw) {
-
-        if (raw) {
-            throw kustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT,
-                    "raw not supported!", null);
-        }
-
-        MultivaluedMap<String, String> map = new MultivaluedMapImpl();
-        map.add("q", query);
-        map.add("count", String.valueOf(pageLength));
-        map.add("lctxs", String.valueOf(meta.getSpanContext().getLeftSize()));
-        map.add("rctxs", String.valueOf(meta.getSpanContext().getRightSize()));
-        try {
-            return this.graphDBhandler.getResponse(map, "distKwic");
-        }
-        catch (KustvaktException e) {
-            jlog.error("Failed searching in Neo4J: "+ e.string());
-            throw kustvaktResponseHandler.throwit(e);
-        }
-
-    }
-
-
-    /**
-     * @param context
-     * @param locale
-     * @param json
-     * @return
-     */
-    // todo: rename
-    @POST
-    @Path("collection_raw")
-    public Response createRawCollection (@Context SecurityContext context,
-            @Context Locale locale, String json) {
-        TokenContext c = (TokenContext) context.getUserPrincipal();
-        VirtualCollection cache = ResourceFactory.getCachedCollection(json);
-        User user;
-        try {
-            user = controller.getUser(c.getUsername());
-        }
-        catch (KustvaktException e) {
-            jlog.error("Exception encountered: "+ e.string());
-            throw kustvaktResponseHandler.throwit(e);
-        }
-
-        VirtualCollection tmp = resourceHandler.getCache(cache.getId(),
-                VirtualCollection.class);
-        if (tmp == null) {
-            String query;
-            try {
-                query = this.processor.processQuery(cache.getData(), user);
-                String stats = searchKrill.getStatistics(query);
-                cache.setStats(JsonUtils.convertToClass(stats, Map.class));
-            }
-            catch (KustvaktException e) {
-                throw kustvaktResponseHandler.throwit(e);
-            }
-            resourceHandler.cache(cache);
-        }
-        else
-            cache = tmp;
-
-        Map vals = new HashMap();
-        vals.put("id", cache.getId());
-        vals.put("statistics", cache.getStats());
-        try {
-            return Response.ok(JsonUtils.toJSON(vals)).build();
-        }
-        catch (KustvaktException e) {
-            throw kustvaktResponseHandler.throwit(e);
-        }
-    }
-
-
-    @GET
-    @Path("/corpus/{corpusId}/{docId}/{textId}/{matchId}/matchInfo")
-    public Response getMatchInfo (@Context SecurityContext ctx,
-            @Context HttpHeaders headers, @Context Locale locale,
-            @PathParam("corpusId") String corpusId,
-            @PathParam("docId") String docId,
-            @PathParam("textId") String textId,
-            @PathParam("matchId") String matchId,
-            @QueryParam("foundry") Set<String> foundries,
-            @QueryParam("layer") Set<String> layers,
-            @QueryParam("spans") Boolean spans) throws KustvaktException {
-
-        TokenContext tokenContext = (TokenContext) ctx.getUserPrincipal();
-        spans = spans != null ? spans : false;
-
-        String matchid =
-                searchKrill.getMatchId(corpusId, docId, textId, matchId);
-        if (layers == null || layers.isEmpty()) layers = new HashSet<>();
-
-        boolean match_only = foundries == null || foundries.isEmpty();
-
-        User user;
-        try {
-            user = controller.getUser(tokenContext.getUsername());
-            controller.setAccessAndLocation(user, headers);
-            System.out.printf(
-                    "Debug: /getMatchInfo/: location=%s, access='%s'.\n",
-                    user.locationtoString(), user.accesstoString());
-        }
-        catch (KustvaktException e) {
-            jlog.error("Failed getting user in the matchInfo service: "+
-                    e.string());
-            throw kustvaktResponseHandler.throwit(e);
-        }
-
-        CorpusAccess corpusAccess = user.getCorpusAccess();
-        Pattern p;
-        switch (corpusAccess) {
-            case PUB:
-                p = config.getPublicLicensePattern();
-                break;
-            case ALL:
-                p = config.getAllLicensePattern();
-                break;
-            default: // FREE
-                p = config.getFreeLicensePattern();
-                break;
-        }
-
-        String results;
-        try {
-            if (!match_only) {
-
-                ArrayList<String> foundryList = new ArrayList<String>();
-                ArrayList<String> layerList = new ArrayList<String>();
-
-                // EM: now without user, just list all foundries and layers
-                if (foundries.contains("*")) {
-                    foundryList = config.getFoundries();
-                    layerList = config.getLayers();
-                }
-                else {
-                    foundryList.addAll(foundries);
-                    layerList.addAll(layers);
-                }
-
-                results = searchKrill.getMatch(matchid, foundryList, layerList,
-                        spans, false, true, p);
-            }
-            else {
-                results = searchKrill.getMatch(matchid, p);
-            }
-        }
-        catch (Exception e) {
-            jlog.error("Exception in the MatchInfo service encountered!", e);
-            throw kustvaktResponseHandler.throwit(StatusCodes.ILLEGAL_ARGUMENT,
-                    e.getMessage(), "");
-        }
-        jlog.debug("MatchInfo results: " + results);
-        return Response.ok(results).build();
-    }
-
-
-	/*
-     * Returns the meta data fields of a certain document
-     */
-	// This is currently identical to LiteService#getMeta(),
-	// but may need auth code to work following policies
-    @GET
-    @Path("/corpus/{corpusId}/{docId}/{textId}")
-    public Response getMeta (
-		@PathParam("corpusId") String corpusId,
-		@PathParam("docId") String docId,
-		@PathParam("textId") String textId
-		// @QueryParam("fields") Set<String> fields
-		) throws KustvaktException {
-
-		String textSigle = searchKrill.getTextSigle(corpusId, docId, textId);
-		
-		String results = searchKrill.getFields(textSigle);
-
-        return Response.ok(results).build();
-    }
 
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
index dd34a1f..a0850ce 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
@@ -38,7 +38,8 @@
  * adding members to a group and subscribing (confirming an
  * invitation) to a group.
  * 
- * These APIs are only available to logged-in users.
+ * These APIs are only available to logged-in users and not available
+ * via third-party apps.
  * 
  * @author margaretha
  *
@@ -54,9 +55,12 @@
     @Autowired
     private UserGroupService service;
 
-    /** Returns all user-groups in which a user is an active or a pending member.
-     *  Not suitable for system-admin, instead use {@link UserGroupController#
-     *  getUserGroupBySystemAdmin(SecurityContext, String, UserGroupStatus)} 
+    /**
+     * Returns all user-groups in which a user is an active or a
+     * pending member.
+     * Not suitable for system-admin, instead use
+     * {@link UserGroupController#
+     * getUserGroupBySystemAdmin(SecurityContext, String, UserGroupStatus)}
      * 
      * @param securityContext
      * @return a list of user-groups
@@ -78,13 +82,18 @@
     }
 
 
-    /** Lists user-groups for system-admin purposes. If username parameter 
-     *  is not specified, list user-groups of all users. If status is not
-     *  specified, list user-groups of all statuses.
+    /**
+     * Lists user-groups for system-admin purposes. If username
+     * parameter
+     * is not specified, list user-groups of all users. If status is
+     * not
+     * specified, list user-groups of all statuses.
      * 
      * @param securityContext
-     * @param username username
-     * @param status {@link UserGroupStatus}
+     * @param username
+     *            username
+     * @param status
+     *            {@link UserGroupStatus}
      * @return a list of user-groups
      */
     @GET
@@ -105,10 +114,12 @@
         }
     }
 
-    /** Retrieves a specific user-group for system admins.
+    /**
+     * Retrieves a specific user-group for system admins.
      * 
      * @param securityContext
-     * @param groupId group id
+     * @param groupId
+     *            group id
      * @return a user-group
      */
     @GET
@@ -128,23 +139,25 @@
 
     }
 
-    /** Creates a user group where the user in token context is the 
-     * group owner, and assigns the listed group members with status 
-     * GroupMemberStatus.PENDING. 
+    /**
+     * Creates a user group where the user in token context is the
+     * group owner, and assigns the listed group members with status
+     * GroupMemberStatus.PENDING.
      * 
      * Invitations must be sent to these proposed members. If a member
-     * accepts the invitation, update his GroupMemberStatus to 
-     * GroupMemberStatus.ACTIVE by using 
+     * accepts the invitation, update his GroupMemberStatus to
+     * GroupMemberStatus.ACTIVE by using
      * {@link UserGroupController#subscribeToGroup(SecurityContext, String)}.
      * 
-     * If he rejects the invitation, update his GroupMemberStatus 
-     * to GroupMemberStatus.DELETED using 
+     * If he rejects the invitation, update his GroupMemberStatus
+     * to GroupMemberStatus.DELETED using
      * {@link UserGroupController#unsubscribeFromGroup(SecurityContext, String)}.
      * 
-     *  
+     * 
      * 
      * @param securityContext
-     * @param group UserGroupJson
+     * @param group
+     *            UserGroupJson
      * @return if successful, HTTP response status OK
      */
     @POST
@@ -163,8 +176,10 @@
         }
     }
 
-    /** Deletes a user-group specified by the group id. Only group owner 
-     *  and system admins can delete groups. 
+    /**
+     * Deletes a user-group specified by the group id. Only group
+     * owner
+     * and system admins can delete groups.
      * 
      * @param securityContext
      * @param groupId
@@ -185,11 +200,14 @@
         }
     }
 
-    /** Deletes a user-group member. Group owner cannot be deleted.
+    /**
+     * Deletes a user-group member. Group owner cannot be deleted.
      * 
      * @param securityContext
-     * @param memberId a username of a group member
-     * @param groupId a group id
+     * @param memberId
+     *            a username of a group member
+     * @param groupId
+     *            a group id
      * @return if successful, HTTP response status OK
      */
     @DELETE
@@ -209,12 +227,16 @@
         }
     }
 
-    /** Invites group members to join a user-group specified in the JSON object.
-     * Only user-group admins and system admins are allowed. 
+    /**
+     * Invites group members to join a user-group specified in the
+     * JSON object.
+     * Only user-group admins and system admins are allowed.
      * 
      * @param securityContext
-     * @param group UserGroupJson containing groupId and usernames to be invited
-     * as members 
+     * @param group
+     *            UserGroupJson containing groupId and usernames to be
+     *            invited
+     *            as members
      * @return if successful, HTTP response status OK
      */
     @POST
@@ -233,13 +255,18 @@
         }
     }
 
-    /** Adds roles of an active member of a user-group. Only user-group admins
+    /**
+     * Adds roles of an active member of a user-group. Only user-group
+     * admins
      * and system admins are allowed.
      * 
      * @param securityContext
-     * @param groupId a group id
-     * @param memberUsername the username of a group member
-     * @param roleIds list of role ids
+     * @param groupId
+     *            a group id
+     * @param memberUsername
+     *            the username of a group member
+     * @param roleIds
+     *            list of role ids
      * @return if successful, HTTP response status OK
      */
     @POST
@@ -261,13 +288,18 @@
         }
     }
 
-    /** Deletes roles of a member of a user-group. Only user-group admins
+    /**
+     * Deletes roles of a member of a user-group. Only user-group
+     * admins
      * and system admins are allowed.
      * 
      * @param securityContext
-     * @param groupId a group id
-     * @param memberUsername the username of a group member
-     * @param roleIds list of role ids
+     * @param groupId
+     *            a group id
+     * @param memberUsername
+     *            the username of a group member
+     * @param roleIds
+     *            list of role ids
      * @return if successful, HTTP response status OK
      */
     @POST
@@ -289,11 +321,14 @@
         }
     }
 
-    /** Handles requests to accept membership invitation. Only invited users 
-     * can subscribe to the corresponding user-group. 
+    /**
+     * Handles requests to accept membership invitation. Only invited
+     * users
+     * can subscribe to the corresponding user-group.
      * 
      * @param securityContext
-     * @param groupId a group id
+     * @param groupId
+     *            a group id
      * @return if successful, HTTP response status OK
      */
     @POST
@@ -312,8 +347,10 @@
         }
     }
 
-    /** Handles requests to reject membership invitation. A member can only 
-     * unsubscribe him/herself from a group. 
+    /**
+     * Handles requests to reject membership invitation. A member can
+     * only
+     * unsubscribe him/herself from a group.
      * 
      * Implemented identical to delete group member.
      * 
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
index f8198e5..05edea1 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
@@ -26,6 +26,8 @@
 import de.ids_mannheim.korap.dto.VirtualCorpusAccessDto;
 import de.ids_mannheim.korap.dto.VirtualCorpusDto;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Scope;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
 import de.ids_mannheim.korap.security.context.TokenContext;
 import de.ids_mannheim.korap.service.VirtualCorpusService;
 import de.ids_mannheim.korap.web.KustvaktResponseHandler;
@@ -34,14 +36,15 @@
 import de.ids_mannheim.korap.web.filter.PiwikFilter;
 import de.ids_mannheim.korap.web.input.VirtualCorpusJson;
 
-/** VirtualCorpusController defines web APIs related to virtual corpus (VC)
- * such as creating, deleting and listing user virtual corpora.
+/**
+ * VirtualCorpusController defines web APIs related to virtual corpus
+ * (VC) such as creating, deleting and listing user virtual corpora.
  * 
- * This class also includes APIs related to virtual corpus access (VCA) 
- * such as sharing and publishing VC. When a VC is published, it is shared 
- * with all users, but not always listed like system VC. It is listed for 
- * a user, once when he/she have searched for the VC. A VC can be published 
- * by creating or editing the VC. 
+ * This class also includes APIs related to virtual corpus access
+ * (VCA) such as sharing and publishing VC. When a VC is published,
+ * it is shared with all users, but not always listed like system
+ * VC. It is listed for a user, once when he/she have searched for the
+ * VC. A VC can be published by creating or editing the VC.
  * 
  * All the APIs in this class are available to logged-in users.
  * 
@@ -58,13 +61,17 @@
     private KustvaktResponseHandler kustvaktResponseHandler;
     @Autowired
     private VirtualCorpusService service;
+    @Autowired
+    private OAuth2ScopeService scopeService;
 
-    /** Creates a user virtual corpus, also for system admins
+    /**
+     * Creates a user virtual corpus, also for system admins
      * 
      * @see VirtualCorpusJson
      * 
      * @param securityContext
-     * @param vc a JSON object describing the virtual corpus
+     * @param vc
+     *            a JSON object describing the virtual corpus
      * @return HTTP Response OK if successful
      */
     @POST
@@ -77,6 +84,7 @@
             TokenContext context =
                     (TokenContext) securityContext.getUserPrincipal();
 
+            scopeService.verifyScope(context, OAuth2Scope.CREATE_VC);
             service.storeVC(vc, context.getUsername());
         }
         catch (KustvaktException e) {
@@ -85,14 +93,16 @@
         return Response.ok().build();
     }
 
-    /** Edits a virtual corpus attributes including name, type and corpus 
-     *  query. Only the virtual corpus owner and system admins can edit 
-     *  a virtual corpus.
+    /**
+     * Edits a virtual corpus attributes including name, type and
+     * corpus query. Only the virtual corpus owner and system admins
+     * can edit a virtual corpus.
      * 
      * @see VirtualCorpusJson
      * 
      * @param securityContext
-     * @param vc a JSON object describing the virtual corpus
+     * @param vc
+     *            a JSON object describing the virtual corpus
      * @return HTTP Response OK if successful
      * @throws KustvaktException
      */
@@ -105,6 +115,7 @@
                 (TokenContext) securityContext.getUserPrincipal();
 
         try {
+            scopeService.verifyScope(context, OAuth2Scope.EDIT_VC);
             service.editVC(vc, context.getUsername());
         }
         catch (KustvaktException e) {
@@ -113,20 +124,24 @@
         return Response.ok().build();
     }
 
-    /** Searches for a specific VC given the VC id. 
+    /**
+     * Searches for a specific VC given the VC id.
      * 
      * @param securityContext
-     * @param vcId a virtual corpus id
+     * @param vcId
+     *            a virtual corpus id
      * @return a list of virtual corpora
      */
     @GET
     @Path("{vcId}")
     @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
-    public VirtualCorpusDto retrieveVC (@Context SecurityContext securityContext,
+    public VirtualCorpusDto retrieveVC (
+            @Context SecurityContext securityContext,
             @PathParam("vcId") int vcId) {
         TokenContext context =
                 (TokenContext) securityContext.getUserPrincipal();
         try {
+            scopeService.verifyScope(context, OAuth2Scope.VC_INFO);
             return service.searchVCById(context.getUsername(), vcId);
         }
         catch (KustvaktException e) {
@@ -134,15 +149,17 @@
         }
     }
 
-    /** Lists not only private virtual corpora but all virtual corpora 
-     *  available to a user.
-     *  
-     *  Users, except system admins, cannot list virtual corpora of 
-     *  other users. Thus, createdBy parameter is only relevant for 
-     *  requests from system admins.
+    /**
+     * Lists not only private virtual corpora but all virtual corpora
+     * available to a user.
+     * 
+     * Users, except system admins, cannot list virtual corpora of
+     * other users. Thus, createdBy parameter is only relevant for
+     * requests from system admins.
      * 
      * @param securityContext
-     * @param createdBy username of virtual corpus creator (optional)
+     * @param createdBy
+     *            username of virtual corpus creator (optional)
      * @return a list of virtual corpora
      */
     @GET
@@ -154,6 +171,7 @@
         TokenContext context =
                 (TokenContext) securityContext.getUserPrincipal();
         try {
+            scopeService.verifyScope(context, OAuth2Scope.VC_INFO);
             return service.listVCByUser(context.getUsername(), createdBy);
         }
         catch (KustvaktException e) {
@@ -161,11 +179,12 @@
         }
     }
 
-    /** Lists all virtual corpora created by a user
+    /**
+     * Lists all virtual corpora created by a user
      * 
      * @param securityContext
-     * @return a list of virtual corpora created by the user 
-     * in the security context.
+     * @return a list of virtual corpora created by the user
+     *         in the security context.
      */
     @GET
     @Path("list/user")
@@ -175,6 +194,7 @@
         TokenContext context =
                 (TokenContext) securityContext.getUserPrincipal();
         try {
+            scopeService.verifyScope(context, OAuth2Scope.VC_INFO);
             return service.listOwnerVC(context.getUsername());
         }
         catch (KustvaktException e) {
@@ -182,16 +202,19 @@
         }
     }
 
-    /** Lists virtual corpora by creator and type. This is a controller for 
-     *  system admin requiring valid system admin authentication. 
-     *  
-     *  If type is not specified, retrieves virtual corpora of all types. 
-     *  If createdBy is not specified, retrieves virtual corpora of all 
-     *  users.
-     *  
+    /**
+     * Lists virtual corpora by creator and type. This is a controller
+     * for system admin requiring valid system admin authentication.
+     * 
+     * If type is not specified, retrieves virtual corpora of all
+     * types. If createdBy is not specified, retrieves virtual corpora
+     * of all users.
+     * 
      * @param securityContext
-     * @param createdBy username of virtual corpus creator
-     * @param type {@link VirtualCorpusType}
+     * @param createdBy
+     *            username of virtual corpus creator
+     * @param type
+     *            {@link VirtualCorpusType}
      * @return a list of virtual corpora
      */
     @GET
@@ -211,12 +234,14 @@
         }
     }
 
-    /** Only the VC owner and system admins can delete VC. VCA admins 
-     *  can delete VC-accesses e.g. of project VC, but not the VC 
-     *  themselves. 
+    /**
+     * Only the VC owner and system admins can delete VC. VCA admins
+     * can delete VC-accesses e.g. of project VC, but not the VC
+     * themselves.
      * 
      * @param securityContext
-     * @param vcId the id of the virtual corpus
+     * @param vcId
+     *            the id of the virtual corpus
      * @return HTTP status 200, if successful
      */
     @DELETE
@@ -234,13 +259,19 @@
         return Response.ok().build();
     }
 
-    /** VC can only be shared with a group, not individuals. 
-     *  Only VCA admins are allowed to share VC and 
-     *  the VC must have been created by themselves.
+    /**
+     * VC can only be shared with a group, not individuals.
+     * Only VCA admins are allowed to share VC and the VC must have
+     * been created by themselves.
+     * 
+     * <br /><br />
+     * Not allowed via third-party apps.
      * 
      * @param securityContext
-     * @param vcId a virtual corpus id
-     * @param groupId a user group id
+     * @param vcId
+     *            a virtual corpus id
+     * @param groupId
+     *            a user group id
      * @return HTTP status 200, if successful
      */
     @POST
@@ -259,7 +290,12 @@
         return Response.ok().build();
     }
 
-    /** Only VCA Admins and system admins are allowed to delete a VC-access.
+    /**
+     * Only VCA Admins and system admins are allowed to delete a
+     * VC-access.
+     * 
+     * <br /><br />
+     * Not allowed via third-party apps.
      * 
      * @param securityContext
      * @param accessId
@@ -281,14 +317,19 @@
     }
 
 
-    /** Lists active VC accesses to the specified VC.
-     *  Only available to VCA and system admins.
-     *  For system admins, lists all VCA of the VC.
+    /**
+     * Lists active VC accesses to the specified VC.
+     * Only available to VCA and system admins.
+     * For system admins, lists all VCA of the VC.
+     * 
+     * <br /><br />
+     * Not allowed via third-party apps.
      * 
      * @see VirtualCorpusAccessStatus
      * 
      * @param securityContext
-     * @param vcId virtual corpus id
+     * @param vcId
+     *            virtual corpus id
      * @return a list of access to the specified virtual corpus
      */
     @GET
@@ -307,12 +348,17 @@
         }
     }
 
-    /** Lists active VC-accesses available for a user-group. 
-     *  Only available to VCA and system admins. 
-     *  For system admins, list all VCA for the group.
+    /**
+     * Lists active VC-accesses available for a user-group.
+     * Only available to VCA and system admins.
+     * For system admins, list all VCA for the group.
+     * 
+     * <br /><br />
+     * Not allowed via third-party apps.
      * 
      * @param securityContext
-     * @param groupId a group id
+     * @param groupId
+     *            a group id
      * @return a list of VC-access
      */
     @GET
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/filter/AuthenticationFilter.java b/full/src/main/java/de/ids_mannheim/korap/web/filter/AuthenticationFilter.java
index bdcc0cf..4059933 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/filter/AuthenticationFilter.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/filter/AuthenticationFilter.java
@@ -72,11 +72,14 @@
 
                     // OAuth2 authentication scheme
                     case BEARER:
-//                        if (request.getPath().equals("oauth2/authorize")) {
-//                            throw new KustvaktException(
-//                                    StatusCodes.AUTHENTICATION_FAILED,
-//                                    "Bearer is not supported for user authentication at oauth2/authorize");
-//                        }
+                        if (request.getPath().startsWith("vc/access")
+                                || request.getPath().startsWith("vc/delete")
+                                || request.getPath().startsWith("group")
+                                || request.getPath().startsWith("user")) {
+                            throw new KustvaktException(
+                                    StatusCodes.AUTHENTICATION_FAILED,
+                                    "Token type Bearer is not allowed.");
+                        }
 
                         context = authenticationManager.getTokenContext(
                                 TokenType.BEARER, authData.getToken(), host,
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/filter/DemoUserFilter.java b/full/src/main/java/de/ids_mannheim/korap/web/filter/DemoUserFilter.java
index 78138bb..e5e838a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/filter/DemoUserFilter.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/filter/DemoUserFilter.java
@@ -6,6 +6,7 @@
 import com.sun.jersey.spi.container.ResourceFilter;
 import de.ids_mannheim.korap.config.BeansFactory;
 import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.constant.TokenType;
 import de.ids_mannheim.korap.security.context.KustvaktContext;
 import de.ids_mannheim.korap.security.context.TokenContext;
 import de.ids_mannheim.korap.user.User;
@@ -69,6 +70,7 @@
         c.setExpirationTime(TimeUtils.plusSeconds(
                 config
                         .getShortTokenTTL()).getMillis());
+        c.setTokenType(TokenType.BASIC);
         return c;
     }
 
diff --git a/full/src/main/resources/default-config.xml b/full/src/main/resources/default-config.xml
index bb5ac1e..456ecff 100644
--- a/full/src/main/resources/default-config.xml
+++ b/full/src/main/resources/default-config.xml
@@ -167,7 +167,6 @@
 
 	<bean id="initializator" class="de.ids_mannheim.korap.config.Initializator"
 		init-method="init">
-		<constructor-arg name="config" ref="kustvakt_config" />
 		<constructor-arg name="accessScopeDao" ref="accessScopeDao" />
 	</bean>
 
diff --git a/full/src/main/resources/kustvakt.conf b/full/src/main/resources/kustvakt.conf
index da7ec20..654a1ab 100644
--- a/full/src/main/resources/kustvakt.conf
+++ b/full/src/main/resources/kustvakt.conf
@@ -50,7 +50,7 @@
 oauth2.native.client.host = korap.ids-mannheim.de
 oauth2.max.attempts = 3
 # -- scopes separated by space
-oauth2.default.scopes = username email 
+oauth2.default.scopes = search match_info 
 oauth2.client.credentials.scopes = client_info
 
 # JWT
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
index 8126843..911f8a9 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
@@ -6,6 +6,7 @@
 import java.io.IOException;
 
 import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response.Status;
 
 import org.apache.http.entity.ContentType;
 import org.junit.Test;
@@ -13,13 +14,13 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.net.HttpHeaders;
 import com.sun.jersey.api.client.ClientResponse;
-import com.sun.jersey.api.client.ClientResponse.Status;
 import com.sun.jersey.core.util.MultivaluedMapImpl;
 
 import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.config.SpringJerseyTest;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Scope;
 import de.ids_mannheim.korap.utils.JsonUtils;
 
 public class OAuth2AccessTokenTest extends SpringJerseyTest {
@@ -42,17 +43,50 @@
         JsonNode node = JsonUtils.readTree(entity);
         return node.at("/access_token").asText();
     }
+    
+    @Test
+    public void testListVCScope() throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", "password");
+        form.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        form.add("client_secret", "secret");
+        form.add("username", "dory");
+        form.add("password", "password");
+        form.add("scope", OAuth2Scope.VC_INFO.toString());
+
+        ClientResponse response = resource().path("oauth2").path("token")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        String token = node.at("/access_token").asText();
+        
+        response = resource().path("vc").path("list")
+                .header(Attributes.AUTHORIZATION, "Bearer " + token)
+                .get(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        entity = response.getEntity(String.class);
+        node = JsonUtils.readTree(entity);
+        assertEquals(4, node.size());
+    }
 
     @Test
-    public void testListVC () throws KustvaktException {
+    public void testListVCScopeNotAuthorized () throws KustvaktException {
         ClientResponse response = resource().path("vc").path("list")
                 .header(Attributes.AUTHORIZATION, "Bearer " + requestToken())
                 .get(ClientResponse.class);
 
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        assertEquals(ClientResponse.Status.UNAUTHORIZED.getStatusCode(),
+                response.getStatus());
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(4, node.size());
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Scope vc_info is not authorized",
+                node.at("/errors/0/1").asText());
     }
 
     @Test
@@ -64,10 +98,11 @@
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .get(ClientResponse.class);
 
+        String ent = response.getEntity(String.class);
+        
         assertEquals(ClientResponse.Status.OK.getStatusCode(),
                 response.getStatus());
-
-        String ent = response.getEntity(String.class);
+        
         JsonNode node = JsonUtils.readTree(ent);
         assertNotNull(node);
         assertEquals(25, node.at("/matches").size());
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
index 93666ea..cc821fe 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
@@ -172,7 +172,7 @@
         MultivaluedMap<String, String> authForm = new MultivaluedMapImpl();
         authForm.add("response_type", "code");
         authForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
-        authForm.add("scope", "username");
+        authForm.add("scope", "search");
 
         ClientResponse response =
                 requestAuthorizationConfidentialClient(authForm);
@@ -182,7 +182,7 @@
         String code = params.get("code").get(0);
         String scopes = params.get("scope").get(0);
 
-        assertEquals(scopes, "username");
+        assertEquals(scopes, "search");
 
         MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
         tokenForm.add("grant_type", "authorization_code");
@@ -242,7 +242,7 @@
         MultivaluedMap<String, String> authForm = new MultivaluedMapImpl();
         authForm.add("response_type", "code");
         authForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
-        authForm.add("scope", "username");
+        authForm.add("scope", "search");
         authForm.add("redirect_uri", uri);
 
         ClientResponse response =
@@ -342,7 +342,6 @@
                         ContentType.APPLICATION_FORM_URLENCODED)
                 .entity(form).post(ClientResponse.class);
         String entity = response.getEntity(String.class);
-        System.out.println(entity);
         JsonNode node = JsonUtils.readTree(entity);
         assertNotNull(node.at("/access_token").asText());
         assertNotNull(node.at("/refresh_token").asText());
diff --git a/full/src/test/resources/kustvakt-test.conf b/full/src/test/resources/kustvakt-test.conf
index 8416b6a..e09f9c4 100644
--- a/full/src/test/resources/kustvakt-test.conf
+++ b/full/src/test/resources/kustvakt-test.conf
@@ -50,7 +50,7 @@
 oauth2.native.client.host = korap.ids-mannheim.de
 oauth2.max.attempts = 2
 # -- scopes separated by space
-oauth2.default.scopes = openid username email
+oauth2.default.scopes = openid search match_info
 oauth2.client.credentials.scopes = client_info
 
 ## OpenId
diff --git a/full/src/test/resources/log4j2-test.properties b/full/src/test/resources/log4j2-test.properties
index 58a30cf..dc7dc8f 100644
--- a/full/src/test/resources/log4j2-test.properties
+++ b/full/src/test/resources/log4j2-test.properties
@@ -19,6 +19,7 @@
 logger.console.level = info
 logger.console.appenderRefs = stdout
 logger.console.appenderRef.file.ref = STDOUT
+logger.console.additivity=false
 
 #loggers=file
 #logger.file.name=com.sun.jersey.test.framework.spi.container
diff --git a/full/src/test/resources/test-config.xml b/full/src/test/resources/test-config.xml
index cad93aa..28cd408 100644
--- a/full/src/test/resources/test-config.xml
+++ b/full/src/test/resources/test-config.xml
@@ -167,7 +167,6 @@
 
 	<bean id="initializator" class="de.ids_mannheim.korap.config.Initializator"
 		init-method="init">
-		<constructor-arg name="config" ref="kustvakt_config" />
 		<constructor-arg name="accessScopeDao" ref="accessScopeDao" />
 	</bean>