Handled scopes & added request token with authorization code tests.

Change-Id: I775141b8b94bf2d1c86ad873807fcb1b12f3914f
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;
     }
 }