Handled scopes & added request token with authorization code tests.

Change-Id: I775141b8b94bf2d1c86ad873807fcb1b12f3914f
diff --git a/full/Changes b/full/Changes
index 9182b80..4589e7b 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,5 +1,5 @@
 version 0.60.2
-02/05/2018
+03/05/2018
 	- implemented OAuth2 client registration (margaretha)
 	- implemented OAuth2 client authentication (margaretha)
 	- changed virtual corpus search to retrieval (margaretha)
@@ -19,6 +19,8 @@
 	- fixed loading spring config multiple times in the test suite (margaretha)
 	- added SQLite created_date trigger for access token (margaretha)  
 	- added a join table for access token scopes (margaretha)
+	- added access scopes handling (margaretha)
+	- added tests about request token with authorization code (margaretha)
 	
 version 0.60.1
 28/03/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
index 5fb08a8..d237494 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
@@ -54,7 +54,8 @@
 
     private AuthenticationMethod OAuth2passwordAuthentication;
     private String nativeClientHost;
-    private Set<String> accessScopes;
+    private Set<String> defaultAccessScopes;
+    private Set<String> clientCredentialsScopes;
     private int maxAuthenticationAttempts;
 
     public FullConfiguration (Properties properties) throws IOException {
@@ -94,7 +95,12 @@
                 "read_username read_email");
         Set<String> scopeSet =
                 Arrays.stream(scopes.split(" ")).collect(Collectors.toSet());
-        setAccessScopes(scopeSet);
+        setDefaultAccessScopes(scopeSet);
+
+        String clientScopes = properties.getProperty(
+                "oauth2.client.credentials.scopes", "read_client_info");
+        setClientCredentialsScopes(Arrays.stream(clientScopes.split(" "))
+                .collect(Collectors.toSet()));
     }
 
     private void setMailConfiguration (Properties properties) {
@@ -352,12 +358,21 @@
         this.maxAuthenticationAttempts = maxAuthenticationAttempts;
     }
 
-    public Set<String> getAccessScopes () {
-        return accessScopes;
+    public Set<String> getDefaultAccessScopes () {
+        return defaultAccessScopes;
     }
 
-    public void setAccessScopes (Set<String> accessScopes) {
-        this.accessScopes = accessScopes;
+    public void setDefaultAccessScopes (Set<String> accessScopes) {
+        this.defaultAccessScopes = accessScopes;
+    }
+
+    public Set<String> getClientCredentialsScopes () {
+        return clientCredentialsScopes;
+    }
+
+    public void setClientCredentialsScopes (
+            Set<String> clientCredentialsScopes) {
+        this.clientCredentialsScopes = clientCredentialsScopes;
     }
 
 }
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 e77f006..6c04431 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
@@ -4,7 +4,8 @@
 
 import de.ids_mannheim.korap.oauth2.dao.AccessScopeDao;
 
-/** Initializes values in the database from kustvakt configuration.
+/**
+ * Initializes values in the database from kustvakt configuration.
  * 
  * @author margaretha
  *
@@ -13,19 +14,20 @@
 
     private FullConfiguration config;
     private AccessScopeDao accessScopeDao;
-    
 
-    public Initializator (FullConfiguration config, AccessScopeDao accessScopeDao) {
+
+    public Initializator (FullConfiguration config,
+                          AccessScopeDao accessScopeDao) {
         this.config = config;
         this.accessScopeDao = accessScopeDao;
     }
-    
+
     public void init () {
         setAccessScope();
     }
-    
-    private void setAccessScope(){
-        Set<String> accessScopes = config.getAccessScopes();
-        accessScopeDao.storeAccessScopes(accessScopes);
+
+    private void setAccessScope () {
+        accessScopeDao.storeAccessScopes(config.getDefaultAccessScopes());
+        accessScopeDao.storeAccessScopes(config.getClientCredentialsScopes());
     }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
index 1834c66..f1507b8 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
@@ -1,13 +1,18 @@
 package de.ids_mannheim.korap.oauth2.dao;
 
+import java.util.Set;
+
 import javax.persistence.EntityManager;
 import javax.persistence.PersistenceContext;
 
 import org.springframework.stereotype.Repository;
 import org.springframework.transaction.annotation.Transactional;
 
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.AccessToken;
 import de.ids_mannheim.korap.oauth2.entity.Authorization;
+import de.ids_mannheim.korap.utils.ParameterChecker;
 
 @Repository
 @Transactional
@@ -16,11 +21,24 @@
     @PersistenceContext
     private EntityManager entityManager;
 
-    public void storeAccessToken (Authorization authorization, String token) {
+    public void storeAccessToken (Authorization authorization, String token)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(authorization, "Authorization");
+        ParameterChecker.checkStringValue(token, "accessToken");
+
         AccessToken accessToken = new AccessToken();
         accessToken.setAuthorization(authorization);
         accessToken.setToken(token);
         accessToken.setScopes(authorization.getScopes());
         entityManager.persist(accessToken);
     }
+
+    public void storeAccessToken (String token, Set<AccessScope> scopes)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(scopes, "scopes");
+        AccessToken accessToken = new AccessToken();
+        accessToken.setToken(token);
+        accessToken.setScopes(scopes);
+        entityManager.persist(accessToken);
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
index f6f505f..d60fe63 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
@@ -14,6 +14,8 @@
 import org.springframework.transaction.annotation.Transactional;
 
 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.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.Authorization;
 import de.ids_mannheim.korap.oauth2.entity.Authorization_;
@@ -39,24 +41,29 @@
         // what if unique fails
     }
 
-    public Authorization retrieveAuthorizationCode (String code,
-            String clientId) throws KustvaktException {
+    public Authorization retrieveAuthorizationCode (String code)
+            throws KustvaktException {
         ParameterChecker.checkStringValue(code, "code");
-        ParameterChecker.checkStringValue(clientId, "client_id");
 
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
         CriteriaQuery<Authorization> query =
                 builder.createQuery(Authorization.class);
         Root<Authorization> root = query.from(Authorization.class);
 
-        Predicate restrictions = builder.and(
-                builder.equal(root.get(Authorization_.code), code),
-                builder.equal(root.get(Authorization_.clientId), clientId));
+        Predicate restrictions =
+                builder.equal(root.get(Authorization_.code), code);
 
         query.select(root);
         query.where(restrictions);
         Query q = entityManager.createQuery(query);
-        return (Authorization) q.getSingleResult();
+        try {
+            return (Authorization) q.getSingleResult();
+        }
+        catch (Exception e) {
+            throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
+                    "Invalid authorization: " + e.getMessage(),
+                    OAuth2Error.INVALID_REQUEST);
+        }
     }
 
     public Authorization updateAuthorization (Authorization authorization) {
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java
index cca86ac..46d2b5c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java
@@ -34,7 +34,7 @@
 
     @Override
     public String toString () {
-        return "id: " + id;
+        return id;
     }
 
     @Override
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
index 5c1d02b..1a33155 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
@@ -1,8 +1,6 @@
 package de.ids_mannheim.korap.oauth2.service;
 
 import java.time.ZonedDateTime;
-import java.util.HashSet;
-import java.util.List;
 import java.util.Set;
 
 import javax.servlet.http.HttpServletRequest;
@@ -24,7 +22,6 @@
 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.dao.AccessScopeDao;
 import de.ids_mannheim.korap.oauth2.dao.AuthorizationDao;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.Authorization;
@@ -41,12 +38,12 @@
     @Autowired
     private OAuth2TokenService auth2Service;
     @Autowired
+    private OAuth2ScopeService scopeService;
+    @Autowired
     private OAuthIssuer oauthIssuer;
 
     @Autowired
     private AuthorizationDao authorizationDao;
-    @Autowired
-    private AccessScopeDao accessScopeDao;
 
     @Autowired
     private FullConfiguration config;
@@ -55,11 +52,7 @@
             OAuthAuthzRequest authzRequest, String authorization)
             throws KustvaktException, OAuthSystemException {
 
-        String responseType = authzRequest.getResponseType();
-        if (responseType == null || responseType.isEmpty()) {
-            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
-                    "response_type is missing.", OAuth2Error.INVALID_REQUEST);
-        }
+        checkResponseType(authzRequest.getResponseType());
 
         OAuth2Client client = clientService.authenticateClient(
                 authzRequest.getClientId(), authzRequest.getClientSecret());
@@ -74,42 +67,41 @@
                 authzRequest.getScopes());
 
         String code = oauthIssuer.authorizationCode();
-        Set<AccessScope> scopes =
-                convertToAccessScope(authzRequest.getScopes());
+        Set<String> scopeSet = authzRequest.getScopes();
+        if (scopeSet == null || scopeSet.isEmpty()) {
+            scopeSet = config.getDefaultAccessScopes();
+        }
+        String scopeStr = String.join(" ", scopeSet);
+        Set<AccessScope> scopes = scopeService.convertToAccessScope(scopeSet);
 
         authorizationDao.storeAuthorizationCode(authzRequest.getClientId(),
                 username, code, scopes, authzRequest.getRedirectURI());
 
         return OAuthASResponse
                 .authorizationResponse(request, Status.FOUND.getStatusCode())
-                .setCode(code).location(redirectUri).buildQueryMessage();
+                .setCode(code).setScope(scopeStr).location(redirectUri)
+                .buildQueryMessage();
     }
 
-    private Set<AccessScope> convertToAccessScope (Set<String> scopes)
+    private void checkResponseType (String responseType)
             throws KustvaktException {
-
-        if (scopes.isEmpty()) {
-            // return default scopes
-            return null;
+        if (responseType == null || responseType.isEmpty()) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    "response_type is missing.", OAuth2Error.INVALID_REQUEST);
         }
-
-        List<AccessScope> definedScopes = accessScopeDao.retrieveAccessScopes();
-        Set<AccessScope> requestedScopes =
-                new HashSet<AccessScope>(scopes.size());
-        int index;
-        for (String scope : scopes) {
-            index = definedScopes.indexOf(new AccessScope(scope));
-            if (index == -1) {
-                throw new KustvaktException(StatusCodes.INVALID_SCOPE,
-                        scope + " is invalid.", OAuth2Error.INVALID_SCOPE);
-            }
-            else {
-                requestedScopes.add(definedScopes.get(index));
-            }
+        else if (responseType.equals("token")) {
+            throw new KustvaktException(StatusCodes.NOT_SUPPORTED,
+                    "response_type token is not supported.",
+                    OAuth2Error.INVALID_REQUEST);
         }
-        return requestedScopes;
+        else if (!responseType.equals("code")) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    "unknown response_type", OAuth2Error.INVALID_REQUEST);
+        }
     }
 
+
+
     private boolean hasRedirectUri (String redirectURI) {
         if (redirectURI != null && !redirectURI.isEmpty()) {
             return true;
@@ -152,7 +144,8 @@
         }
         else {
             // check if there is a redirect URI in the DB
-            // This should not happened as it is required in client registration!
+            // This should not happened as it is required in client
+            // registration!
             if (registeredUri != null && !registeredUri.isEmpty()) {
                 redirectUri = registeredUri;
             }
@@ -167,52 +160,55 @@
     }
 
 
-    public Authorization verifyAuthorization (String code, String clientId,
-            String redirectURI) throws KustvaktException {
-        Authorization authorization =
-                authorizationDao.retrieveAuthorizationCode(code, clientId);
+    public Authorization retrieveAuthorization (String code)
+            throws KustvaktException {
+        return authorizationDao.retrieveAuthorizationCode(code);
+    }
 
-        // EM: can Kustvakt be specific about the invalid request param?
-        if (authorization.isRevoked()) {
-            addTotalAttempts(authorization);
+    public Authorization verifyAuthorization (Authorization authorization,
+            String clientId, String redirectURI) throws KustvaktException {
+
+        // EM: can Kustvakt be specific about the invalid grant error
+        // description?
+        if (!authorization.getClientId().equals(clientId)) {
             throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
-                    "Invalid authorization", OAuth2Error.INVALID_REQUEST);
+                    "Invalid authorization", OAuth2Error.INVALID_GRANT);
+        }
+        if (authorization.isRevoked()) {
+            throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
+                    "Invalid authorization", OAuth2Error.INVALID_GRANT);
         }
 
         if (isExpired(authorization.getCreatedDate())) {
-            addTotalAttempts(authorization);
             throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
-                    "Authorization expired", OAuth2Error.INVALID_REQUEST);
+                    "Authorization expired", OAuth2Error.INVALID_GRANT);
         }
 
         String authorizedUri = authorization.getRedirectURI();
         if (authorizedUri != null && !authorizedUri.isEmpty()
                 && !authorizedUri.equals(redirectURI)) {
-            addTotalAttempts(authorization);
             throw new KustvaktException(StatusCodes.INVALID_REDIRECT_URI,
-                    "Invalid redirect URI", OAuth2Error.INVALID_REQUEST);
+                    "Invalid redirect URI", OAuth2Error.INVALID_GRANT);
         }
 
         authorization.setRevoked(true);
         authorization = authorizationDao.updateAuthorization(authorization);
-        
+
         return authorization;
     }
 
     public void addTotalAttempts (Authorization authorization) {
         int totalAttempts = authorization.getTotalAttempts() + 1;
-        if (totalAttempts > config.getMaxAuthenticationAttempts()) {
+        if (totalAttempts == config.getMaxAuthenticationAttempts()) {
             authorization.setRevoked(true);
         }
-        else {
-            authorization.setTotalAttempts(totalAttempts);
-        }
+        authorization.setTotalAttempts(totalAttempts);
         authorizationDao.updateAuthorization(authorization);
     }
 
     private boolean isExpired (ZonedDateTime createdDate) {
         jlog.debug("createdDate: " + createdDate);
-        ZonedDateTime expiration = createdDate.plusMinutes(10);
+        ZonedDateTime expiration = createdDate.plusSeconds(60);
         ZonedDateTime now = ZonedDateTime.now();
         jlog.debug("expiration: " + expiration + ", now: " + now);
 
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
new file mode 100644
index 0000000..15b51e3
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
@@ -0,0 +1,74 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+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.dao.AccessScopeDao;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+
+@Service
+public class OAuth2ScopeService {
+
+    @Autowired
+    private AccessScopeDao accessScopeDao;
+
+    /**
+     * Converts a set of scope strings to a set of {@link AccessScope}
+     * 
+     * @param scopes
+     * @return
+     * @throws KustvaktException
+     */
+    public Set<AccessScope> convertToAccessScope (Set<String> scopes)
+            throws KustvaktException {
+
+        List<AccessScope> definedScopes = accessScopeDao.retrieveAccessScopes();
+        Set<AccessScope> requestedScopes =
+                new HashSet<AccessScope>(scopes.size());
+        int index;
+        for (String scope : scopes) {
+            index = definedScopes.indexOf(new AccessScope(scope));
+            if (index == -1) {
+                throw new KustvaktException(StatusCodes.INVALID_SCOPE,
+                        scope + " is an invalid scope",
+                        OAuth2Error.INVALID_SCOPE);
+            }
+            else {
+                requestedScopes.add(definedScopes.get(index));
+            }
+        }
+        return requestedScopes;
+    }
+
+    public String convertAccessScopesToString (Set<AccessScope> scopes) {
+        Set<String> set = scopes.stream().map(scope -> scope.toString())
+                .collect(Collectors.toSet());
+        return String.join(" ", set);
+    }
+
+    /**
+     * Simple reduction of requested scopes, i.e. excluding any scopes
+     * that are not default scopes for a specific authorization grant.
+     * 
+     * @param scopes
+     * @param defaultScopes
+     * @return accepted scopes
+     */
+    public Set<String> filterScopes (Set<String> scopes,
+            Set<String> defaultScopes) {
+        Stream<String> stream = scopes.stream();
+        Set<String> filteredScopes =
+                stream.filter(scope -> defaultScopes.contains(scope))
+                        .collect(Collectors.toSet());
+        return filteredScopes;
+    }
+}
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 a2d1ba5..506fde8 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
@@ -23,6 +23,7 @@
 import de.ids_mannheim.korap.interfaces.AuthenticationManagerIface;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
 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;
 
@@ -34,6 +35,8 @@
     @Autowired
     private OAuth2AuthorizationService authorizationService;
     @Autowired
+    private OAuth2ScopeService scopeService;
+    @Autowired
     private AccessTokenDao tokenDao;
 
     @Autowired
@@ -86,7 +89,8 @@
      * @param clientSecret
      *            clilent_secret, required if client_secret was issued
      *            for the client in client registration.
-     * @return
+     * @return an OAuthResponse containing an access token if
+     *         successful
      * @throws OAuthSystemException
      * @throws KustvaktException
      */
@@ -95,9 +99,17 @@
             String clientSecret)
             throws KustvaktException, OAuthSystemException {
 
-        clientService.authenticateClient(clientId, clientSecret);
-        Authorization authorization = authorizationService
-                .verifyAuthorization(authorizationCode, clientId, redirectURI);
+        Authorization authorization =
+                authorizationService.retrieveAuthorization(authorizationCode);
+        try {
+            clientService.authenticateClient(clientId, clientSecret);
+            authorization = authorizationService
+                    .verifyAuthorization(authorization, clientId, redirectURI);
+        }
+        catch (KustvaktException e) {
+            authorizationService.addTotalAttempts(authorization);
+            throw e;
+        }
         return createsAccessTokenResponse(authorization);
     }
 
@@ -126,7 +138,8 @@
      * @param clientSecret
      *            clilent_secret, required if client_secret was issued
      *            for the client in client registration.
-     * @return
+     * @return an OAuthResponse containing an access token if
+     *         successful
      * @throws KustvaktException
      * @throws OAuthSystemException
      */
@@ -144,7 +157,8 @@
         }
 
         authenticateUser(username, password, scopes);
-        return createsAccessTokenResponse();
+        // verify or limit scopes ?
+        return createsAccessTokenResponse(scopes);
     }
 
     public void authenticateUser (String username, String password,
@@ -169,13 +183,15 @@
 
     /**
      * Clients must authenticate.
+     * Client credentials grant is limited to native clients.
      * 
      * @param clientId
      *            client_id parameter, required
      * @param clientSecret
      *            client_secret parameter, required
      * @param scopes
-     * @return
+     * @return an OAuthResponse containing an access token if
+     *         successful
      * @throws KustvaktException
      * @throws OAuthSystemException
      */
@@ -190,10 +206,21 @@
                     OAuth2Error.INVALID_REQUEST);
         }
 
+        // OAuth2Client client =
         clientService.authenticateClient(clientId, clientSecret);
-        return createsAccessTokenResponse();
-    }
 
+        // if (client.isNative()) {
+        // throw new KustvaktException(
+        // StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+        // "Client credentials grant is not allowed for third party
+        // clients",
+        // OAuth2Error.UNAUTHORIZED_CLIENT);
+        // }
+
+        scopes = scopeService.filterScopes(scopes,
+                config.getClientCredentialsScopes());
+        return createsAccessTokenResponse(scopes);
+    }
 
     /**
      * Creates an OAuthResponse containing an access token and a
@@ -201,27 +228,45 @@
      * 
      * @return an OAuthResponse containing an access token
      * @throws OAuthSystemException
+     * @throws KustvaktException
      */
 
-    private OAuthResponse createsAccessTokenResponse ()
-            throws OAuthSystemException {
-        return createsAccessTokenResponse(null);
+    private OAuthResponse createsAccessTokenResponse (Set<String> scopes)
+            throws OAuthSystemException, KustvaktException {
+
+        String accessToken = oauthIssuer.accessToken();
+        // String refreshToken = oauthIssuer.refreshToken();
+
+        Set<AccessScope> accessScopes =
+                scopeService.convertToAccessScope(scopes);
+        tokenDao.storeAccessToken(accessToken, accessScopes);
+
+        return OAuthASResponse.tokenResponse(Status.OK.getStatusCode())
+                .setAccessToken(accessToken)
+                .setTokenType(TokenType.BEARER.toString())
+                .setExpiresIn(String.valueOf(config.getTokenTTL()))
+                // .setRefreshToken(refreshToken)
+                .setScope(String.join(" ", scopes)).buildJSONMessage();
     }
 
     private OAuthResponse createsAccessTokenResponse (
-            Authorization authorization) throws OAuthSystemException {
+            Authorization authorization)
+            throws OAuthSystemException, KustvaktException {
         String accessToken = oauthIssuer.accessToken();
-        String refreshToken = oauthIssuer.refreshToken();
+        // String refreshToken = oauthIssuer.refreshToken();
 
         tokenDao.storeAccessToken(authorization, accessToken);
 
+        String scopes = scopeService
+                .convertAccessScopesToString(authorization.getScopes());
+
         OAuthResponse r =
                 OAuthASResponse.tokenResponse(Status.OK.getStatusCode())
                         .setAccessToken(accessToken)
                         .setTokenType(TokenType.BEARER.toString())
                         .setExpiresIn(String.valueOf(config.getTokenTTL()))
-                        .setRefreshToken(refreshToken).buildJSONMessage();
-        // scope
+                        // .setRefreshToken(refreshToken)
+                        .setScope(scopes).buildJSONMessage();
         return r;
     }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
index 05c0bd0..d272f73 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
@@ -112,13 +112,18 @@
     }
 
 
+    /** Deregisters confidential clients. Clients must authenticate. 
+     * 
+     * @param securityContext
+     * @param request
+     * @param form
+     * @return
+     */
     @DELETE
     @Path("deregister/confidential")
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     public Response deregisterConfidentialClient (
             @Context SecurityContext securityContext,
-            //            @HeaderParam("Authorization") String authorization,
-            //            @FormParam("client_id") String clientId
             @Context HttpServletRequest request,
             MultivaluedMap<String, String> form) {
         try {
diff --git a/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql b/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
index f9cd171..b42ae28 100644
--- a/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
+++ b/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
@@ -10,7 +10,7 @@
   "This is a test native confidential client.");
   
 -- plain secret value is "secret"
-INSERT INTO oauth2_client(id,name,secret,type,native, url,url_hashcode,
+INSERT INTO oauth2_client(id,name,secret,type,native,url,url_hashcode,
   redirect_uri,registered_by, description) 
 VALUES ("9aHsGW6QflV13ixNpez","test non native confidential client",
   "$2a$08$vi1FbuN3p6GcI1tSxMAoeuIYL8Yw3j6A8wJthaN8ZboVnrQaTwLPq",
@@ -18,10 +18,10 @@
   "https://third.party.com/confidential/redirect", "system",
   "This is a test nonnative confidential client.");
   
-INSERT INTO oauth2_client(id,name,secret,type,url,url_hashcode,
+INSERT INTO oauth2_client(id,name,secret,type,native,url,url_hashcode,
   redirect_uri, registered_by, description) 
 VALUES ("8bIDtZnH6NvRkW2Fq","third party client",null,
-  "PUBLIC","http://third.party.client.com", -2137275617,
+  "PUBLIC", 0,"http://third.party.client.com", -2137275617,
   "https://third.party.client.com/redirect","system",
   "This is a test nonnative public client.");
   
diff --git a/full/src/main/resources/kustvakt.conf b/full/src/main/resources/kustvakt.conf
index 99e3b94..75a3483 100644
--- a/full/src/main/resources/kustvakt.conf
+++ b/full/src/main/resources/kustvakt.conf
@@ -47,6 +47,11 @@
 ### (see de.ids_mannheim.korap.constant.AuthenticationMethod for possible 
 ### oauth.password.authentication values)
 oauth.password.authentication = TEST
+oauth2.native.client.host = korap.ids-mannheim.de
+oauth2.max.attempts = 3
+# -- scopes separated by space
+oauth2.default.scopes = read_username read_email 
+oauth2.client.credentials.scopes = read_client_info
 
 # JWT
 security.jwt.issuer=korap.ids-mannheim.de
diff --git a/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java b/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
index 619b795..246e14e 100644
--- a/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
@@ -11,6 +11,7 @@
 import org.springframework.context.support.GenericApplicationContext;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.web.context.ContextLoaderListener;
 import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext;
 
 import com.sun.jersey.spi.spring.container.servlet.SpringServlet;
@@ -72,8 +73,9 @@
         return new WebAppDescriptor.Builder(classPackages)
                 .servletClass(SpringServlet.class)
                 .contextListenerClass(StaticContextLoaderListener.class)
-                //                .contextParam("contextConfigLocation",
-                //                        "classpath:test-config.xml")
+//                .contextListenerClass(ContextLoaderListener.class)
+//                                .contextParam("contextConfigLocation",
+//                                        "classpath:test-config.xml")
                 .build();
     }
 
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 ca4d0ee..216d7a1 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
@@ -18,12 +18,14 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.net.HttpHeaders;
 import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.uri.UriComponent;
 import com.sun.jersey.core.util.MultivaluedMapImpl;
 
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 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.oauth2.constant.OAuth2Error;
 import de.ids_mannheim.korap.utils.JsonUtils;
 
 /**
@@ -34,7 +36,7 @@
 
     @Autowired
     private HttpAuthorizationHandler handler;
-    
+
     private ClientResponse requestAuthorizationConfidentialClient (
             MultivaluedMap<String, String> form) throws KustvaktException {
 
@@ -127,6 +129,25 @@
                 node.at("/error_description").asText());
     }
 
+    @Test
+    public void testAuthorizeInvalidScope () throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("response_type", "code");
+        form.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        form.add("username", "dory");
+        form.add("password", "password");
+        form.add("scope", "read_address");
+
+        ClientResponse response = requestAuthorizationConfidentialClient(form);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_SCOPE, node.at("/error").asText());
+        assertEquals("read_address is an invalid scope",
+                node.at("/error_description").asText());
+    }
+
     private ClientResponse requestToken (MultivaluedMap<String, String> form)
             throws KustvaktException {
         return resource().path("oauth2").path("token")
@@ -139,24 +160,30 @@
     @Test
     public void testRequestTokenAuthorizationConfidential ()
             throws KustvaktException {
-        
+
         MultivaluedMap<String, String> authForm = new MultivaluedMapImpl();
         authForm.add("response_type", "code");
         authForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
         authForm.add("username", "dory");
         authForm.add("password", "password");
         authForm.add("scope", "read_username");
-        
-        ClientResponse response = requestAuthorizationConfidentialClient(authForm);
+
+        ClientResponse response =
+                requestAuthorizationConfidentialClient(authForm);
         URI redirectUri = response.getLocation();
-        String code = redirectUri.getQuery().split("=")[1];
-        
+        MultivaluedMap<String, String> params =
+                UriComponent.decodeQuery(redirectUri, true);
+        String code = params.get("code").get(0);
+        String scopes = params.get("scope").get(0);
+
+        assertEquals(scopes, "read_username");
+
         MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
         tokenForm.add("grant_type", "authorization_code");
         tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
         tokenForm.add("client_secret", "secret");
         tokenForm.add("code", code);
-        
+
         response = requestToken(tokenForm);
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
@@ -165,8 +192,115 @@
         assertEquals(TokenType.BEARER.toString(),
                 node.at("/token_type").asText());
         assertNotNull(node.at("/expires_in").asText());
+
+        testRequestTokenWithUsedAuthorization(tokenForm);
     }
-    
+
+    private void testRequestTokenWithUsedAuthorization (
+            MultivaluedMap<String, String> form) throws KustvaktException {
+        ClientResponse response = requestToken(form);
+        String entity = response.getEntity(String.class);
+
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuthError.TokenResponse.INVALID_GRANT,
+                node.at("/error").asText());
+        assertEquals("Invalid authorization",
+                node.at("/error_description").asText());
+    }
+
+    @Test
+    public void testRequestTokenInvalidAuthorizationCode ()
+            throws KustvaktException {
+        MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+        tokenForm.add("grant_type", "authorization_code");
+        tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        tokenForm.add("client_secret", "secret");
+        tokenForm.add("code", "blahblah");
+
+        ClientResponse response = requestToken(tokenForm);
+        String entity = response.getEntity(String.class);
+
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuthError.TokenResponse.INVALID_REQUEST,
+                node.at("/error").asText());
+    }
+
+    @Test
+    public void testRequestTokenAuthorizationReplyAttack ()
+            throws KustvaktException {
+        String uri = "https://korap.ids-mannheim.de/confidential/redirect";
+        MultivaluedMap<String, String> authForm = new MultivaluedMapImpl();
+        authForm.add("response_type", "code");
+        authForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        authForm.add("username", "dory");
+        authForm.add("password", "password");
+        authForm.add("scope", "read_username");
+        authForm.add("redirect_uri", uri);
+
+        ClientResponse response =
+                requestAuthorizationConfidentialClient(authForm);
+        URI redirectUri = response.getLocation();
+        MultivaluedMap<String, String> params =
+                UriComponent.decodeQuery(redirectUri, true);
+        String code = params.get("code").get(0);
+
+        testRequestTokenAuthorizationInvalidClient(code);
+        testRequestTokenAuthorizationInvalidRedirectUri(code);
+        testRequestTokenAuthorizationRevoked(code, uri);
+    }
+
+    private void testRequestTokenAuthorizationInvalidClient (String code)
+            throws KustvaktException {
+        MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+        tokenForm.add("grant_type", "authorization_code");
+        tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        tokenForm.add("client_secret", "blah");
+        tokenForm.add("code", code);
+
+        ClientResponse response = requestToken(tokenForm);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_CLIENT, node.at("/error").asText());
+    }
+
+    private void testRequestTokenAuthorizationInvalidRedirectUri (String code)
+            throws KustvaktException {
+        MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+        tokenForm.add("grant_type", "authorization_code");
+        tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        tokenForm.add("client_secret", "secret");
+        tokenForm.add("code", code);
+        tokenForm.add("redirect_uri", "https://blahblah.com");
+
+        ClientResponse response = requestToken(tokenForm);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
+    }
+
+    private void testRequestTokenAuthorizationRevoked (String code, String uri)
+            throws KustvaktException {
+        MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+        tokenForm.add("grant_type", "authorization_code");
+        tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        tokenForm.add("client_secret", "secret");
+        tokenForm.add("code", code);
+        tokenForm.add("redirect_uri", uri);
+
+        ClientResponse response = requestToken(tokenForm);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuthError.TokenResponse.INVALID_GRANT,
+                node.at("/error").asText());
+        assertEquals("Invalid authorization",
+                node.at("/error_description").asText());
+    }
+
+
     @Test
     public void testRequestTokenPasswordGrantConfidential ()
             throws KustvaktException {
@@ -228,7 +362,7 @@
         assertEquals("Missing parameters: client_id",
                 node.at("/error_description").asText());
     }
-    
+
     @Test
     public void testRequestTokenPasswordGrantPublic ()
             throws KustvaktException {
@@ -293,6 +427,30 @@
     }
 
     @Test
+    public void testRequestTokenClientCredentialsGrantReducedScope ()
+            throws KustvaktException {
+
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", "client_credentials");
+        form.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        form.add("client_secret", "secret");
+        form.add("scope", "read_username read_client_info");
+
+        ClientResponse response = requestToken(form);
+        String entity = response.getEntity(String.class);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        JsonNode node = JsonUtils.readTree(entity);
+        // length?
+        assertNotNull(node.at("/access_token").asText());
+        assertNotNull(node.at("/refresh_token").asText());
+        assertEquals(TokenType.BEARER.toString(),
+                node.at("/token_type").asText());
+        assertNotNull(node.at("/expires_in").asText());
+        assertEquals("read_client_info", node.at("/scope").asText());
+    }
+
+    @Test
     public void testRequestTokenMissingGrantType () throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         ClientResponse response = requestToken(form);
diff --git a/full/src/test/resources/kustvakt-test.conf b/full/src/test/resources/kustvakt-test.conf
index b26384b..d9eda87 100644
--- a/full/src/test/resources/kustvakt-test.conf
+++ b/full/src/test/resources/kustvakt-test.conf
@@ -48,6 +48,10 @@
 ### oauth.password.authentication values)
 oauth.password.authentication = TEST
 oauth.native.client.host=korap.ids-mannheim.de
+oauth2.max.attempts = 2
+# -- scopes separated by space
+oauth2.default.scopes = read_username read_email 
+oauth2.client.credentials.scopes = read_client_info
 
 # JWT
 security.jwt.issuer=korap.ids-mannheim.de