Implemented OAuth2 token request with password grant using Nimbus lib.

Change-Id: I0bde6a11c12c03badd18c32d77eebc24a1b3a443
diff --git a/full/Changes b/full/Changes
index dc048a4..d33aeca 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,5 +1,5 @@
 version 0.60.4
-27/06/2018
+28/06/2018
     - implemented OAuth2 authorization code request with OpenID Authentication (margaretha)
     - enabled OAuth2 authorization without OpenID authentication using Nimbus library (margaretha)
     - implemented response handler for OpenID authentication errors in authorization requests (margaretha)
@@ -12,6 +12,7 @@
     - implemented OpenId configuration (margaretha) 
     - added authentication time and support for auth_time in id_token (margaretha)
     - implemented support for nonce and max_age parameters in OpenID authentication (margaretha)
+    - implemented OAuth2 token request with password grant using Nimbus library (margaretha)
     
 version 0.60.3
 06/06/2018
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 913db9f..4e89d5e 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
@@ -40,7 +40,7 @@
 
         if (grantType.equals(GrantType.AUTHORIZATION_CODE.toString())) {
             Authorization authorization =
-                    requestAccessTokenWithAuthorizationCode(
+                    retrieveAuthorization(
                             oAuthRequest.getCode(),
                             oAuthRequest.getRedirectURI(),
                             oAuthRequest.getClientId(),
@@ -48,7 +48,7 @@
             return createsAccessTokenResponse(authorization);
         }
         else if (grantType.equals(GrantType.PASSWORD.toString())) {
-            ZonedDateTime authenticationTime = requestAccessTokenWithPassword(
+            ZonedDateTime authenticationTime = authenticateClientAndUser(
                     oAuthRequest.getUsername(), oAuthRequest.getPassword(),
                     oAuthRequest.getScopes(), oAuthRequest.getClientId(),
                     oAuthRequest.getClientSecret());
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 0d943c0..eca6fa2 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
@@ -5,6 +5,7 @@
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.Set;
 
 import org.springframework.stereotype.Service;
@@ -21,6 +22,7 @@
 import com.nimbusds.oauth2.sdk.AuthorizationGrant;
 import com.nimbusds.oauth2.sdk.GrantType;
 import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.ResourceOwnerPasswordCredentialsGrant;
 import com.nimbusds.oauth2.sdk.Scope;
 import com.nimbusds.oauth2.sdk.TokenRequest;
 import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
@@ -28,6 +30,7 @@
 import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
 import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
 import com.nimbusds.oauth2.sdk.id.Audience;
+import com.nimbusds.oauth2.sdk.id.ClientID;
 import com.nimbusds.oauth2.sdk.id.Issuer;
 import com.nimbusds.oauth2.sdk.id.Subject;
 import com.nimbusds.oauth2.sdk.token.AccessToken;
@@ -71,26 +74,18 @@
         GrantType grantType = grant.getType();
         ClientAuthentication clientAuthentication =
                 tokenRequest.getClientAuthentication();
-        String[] clientCredentials =
-                extractClientCredentials(clientAuthentication);
+        ClientID clientId = tokenRequest.getClientID();
 
         if (grantType.equals(GrantType.AUTHORIZATION_CODE)) {
-            AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) grant;
-            String authorizationCode =
-                    codeGrant.getAuthorizationCode().getValue();
-            URI redirectionURI = codeGrant.getRedirectionURI();
-            String redirectURI = null;
-            if (redirectionURI != null) {
-                redirectURI = redirectionURI.toString();
-            }
-            Authorization authorization =
-                    requestAccessTokenWithAuthorizationCode(authorizationCode,
-                            redirectURI, clientCredentials[0],
-                            clientCredentials[1]);
-            return createsAccessTokenResponse(authorization);
+            return requestAccessTokenWithAuthorizationCode(grant,
+                    clientAuthentication, clientId);
         }
         else if (grantType.equals(GrantType.PASSWORD)) {
-
+            ResourceOwnerPasswordCredentialsGrant passwordGrant =
+                    (ResourceOwnerPasswordCredentialsGrant) grant;
+            return requestAccessTokenWithPassword(passwordGrant.getUsername(),
+                    passwordGrant.getPassword().getValue(),
+                    tokenRequest.getScope(), clientAuthentication, clientId);
         }
         else if (grantType.equals(GrantType.CLIENT_CREDENTIALS)) {
 
@@ -103,6 +98,75 @@
         return null;
     }
 
+    private AccessTokenResponse requestAccessTokenWithPassword (String username,
+            String password, Scope scope,
+            ClientAuthentication clientAuthentication, ClientID clientId)
+            throws KustvaktException {
+
+        Set<String> scopes = null;
+        if (scope != null) {
+            scopes = new HashSet<String>();
+            scopes.addAll(scope.toStringList());
+        }
+
+        ZonedDateTime authenticationTime;
+        String clientIdStr = null;
+        if (clientAuthentication == null) {
+            if (clientId == null) {
+                throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                        "Missing parameters: client_id",
+                        OAuth2Error.INVALID_REQUEST);
+            }
+            else {
+                authenticationTime = authenticateClientAndUser(username,
+                        password, scopes, clientId.getValue(), null);
+                clientIdStr = clientId.getValue();
+            }
+        }
+        else {
+            String[] clientCredentials =
+                    extractClientCredentials(clientAuthentication);
+            clientIdStr = clientCredentials[0];
+            authenticationTime = authenticateClientAndUser(username, password,
+                    scopes, clientCredentials[0], clientCredentials[1]);
+        }
+        return createsAccessTokenResponse(scope, clientIdStr, username,
+                authenticationTime, null);
+    }
+
+    private AccessTokenResponse requestAccessTokenWithAuthorizationCode (
+            AuthorizationGrant grant, ClientAuthentication clientAuthentication,
+            ClientID clientId) throws KustvaktException {
+        AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) grant;
+        String authorizationCode = codeGrant.getAuthorizationCode().getValue();
+        URI redirectionURI = codeGrant.getRedirectionURI();
+        String redirectURI = null;
+        if (redirectionURI != null) {
+            redirectURI = redirectionURI.toString();
+        }
+
+        Authorization authorization = null;
+        if (clientAuthentication == null) {
+            if (clientId == null) {
+                throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                        "Missing parameters: client_id",
+                        OAuth2Error.INVALID_REQUEST);
+            }
+            else {
+                authorization = retrieveAuthorization(authorizationCode,
+                        redirectURI, clientId.getValue(), null);
+            }
+        }
+        else {
+            String[] clientCredentials =
+                    extractClientCredentials(clientAuthentication);
+            authorization = retrieveAuthorization(authorizationCode,
+                    redirectURI, clientCredentials[0], clientCredentials[1]);
+        }
+
+        return createsAccessTokenResponse(authorization);
+
+    }
 
     private AccessTokenResponse createsAccessTokenResponse (
             Authorization authorization) throws KustvaktException {
@@ -110,15 +174,24 @@
         String[] scopeArray = scopes.stream().map(scope -> scope.toString())
                 .toArray(String[]::new);
         Scope scope = new Scope(scopeArray);
+        return createsAccessTokenResponse(scope, authorization.getClientId(),
+                authorization.getUserId(),
+                authorization.getUserAuthenticationTime(),
+                authorization.getNonce());
+    }
+
+    private AccessTokenResponse createsAccessTokenResponse (Scope scope,
+            String clientId, String userId,
+            ZonedDateTime userAuthenticationTime, String nonce)
+            throws KustvaktException {
+
         AccessToken accessToken =
                 new BearerAccessToken(config.getTokenTTL(), scope);
         RefreshToken refreshToken = new RefreshToken();
 
-        if (scope.contains("openid")) {
-            JWTClaimsSet claims = createIdTokenClaims(
-                    authorization.getClientId(), authorization.getUserId(),
-                    authorization.getUserAuthenticationTime(),
-                    authorization.getNonce());
+        if (scope != null && scope.contains("openid")) {
+            JWTClaimsSet claims = createIdTokenClaims(clientId, userId,
+                    userAuthenticationTime, nonce);
             SignedJWT idToken = signIdToken(claims,
                     // default
                     new JWSHeader(JWSAlgorithm.RS256),
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 fd251be..d36a077 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
@@ -54,13 +54,13 @@
      * @param clientId
      *            required if there is no authorization header
      * @param clientSecret
-     *            clilent_secret, required if client_secret was issued
+     *            client_secret, required if client_secret was issued
      *            for the client in client registration.
      * @return an authorization
      * @throws OAuthSystemException
      * @throws KustvaktException
      */
-    protected Authorization requestAccessTokenWithAuthorizationCode (
+    protected Authorization retrieveAuthorization (
             String authorizationCode, String redirectURI, String clientId,
             String clientSecret) throws KustvaktException {
 
@@ -107,7 +107,7 @@
      * @throws KustvaktException
      * @throws OAuthSystemException
      */
-    protected ZonedDateTime requestAccessTokenWithPassword (String username,
+    protected ZonedDateTime authenticateClientAndUser (String username,
             String password, Set<String> scopes, String clientId,
             String clientSecret) throws KustvaktException {
 
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java b/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java
index 32152de..3729bd2 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java
@@ -10,7 +10,6 @@
 import javax.ws.rs.core.Response.Status;
 
 import org.apache.http.HttpHeaders;
-import org.apache.http.HttpStatus;
 import org.springframework.stereotype.Service;
 
 import com.nimbusds.oauth2.sdk.AccessTokenResponse;
@@ -24,7 +23,6 @@
 import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
 
 import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.interfaces.db.AuditingIface;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
 import net.minidev.json.JSONObject;
@@ -133,15 +131,14 @@
                     && !errorCode.equals("[]")) {
                 errorObject = new ErrorObject(e.getEntity(), e.getMessage());
             }
-            else{
+            else {
                 throw throwit(e);
             }
         }
         return errorObject;
     }
 
-    public Response createAuthorizationErrorResponse (ParseException e,
-            boolean isAuthentication, State state) {
+    public Response createErrorResponse (ParseException e, State state) {
         ErrorObject errorObject = e.getErrorObject();
         if (errorObject == null) {
             errorObject = com.nimbusds.oauth2.sdk.OAuth2Error.INVALID_REQUEST;
@@ -160,20 +157,25 @@
 
     }
 
-    public void createTokenErrorResponse (KustvaktException e) {
+    public Response createTokenErrorResponse (KustvaktException e) {
 
         String errorCode = e.getEntity();
         ErrorObject errorObject = tokenErrorObjectMap.get(errorCode);
         if (errorObject == null) {
             errorObject = errorObjectMap.get(errorCode);
-            if (errorObject == null) {
+            if (errorCode != null && !errorCode.isEmpty()
+                    && !errorCode.equals("[]")) {
                 errorObject = new ErrorObject(e.getEntity(), e.getMessage());
             }
+            else {
+                throw throwit(e);
+            }
         }
 
+        errorObject = errorObject.setDescription(e.getMessage());
         TokenErrorResponse errorResponse = new TokenErrorResponse(errorObject);
         Status status = determineErrorStatus(errorCode);
-        createResponse(errorResponse, status);
+        return createResponse(errorResponse, status);
     }
 
     public Response createResponse (AccessTokenResponse tokenResponse,
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
index a579866..141e8ec 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
@@ -142,8 +142,7 @@
                     isAuthentication, authTime);
         }
         catch (ParseException e) {
-            return openIdResponseHandler.createAuthorizationErrorResponse(e,
-                    isAuthentication, state);
+            return openIdResponseHandler.createErrorResponse(e, state);
         }
         catch (KustvaktException e) {
             return openIdResponseHandler.createAuthorizationErrorResponse(e,
@@ -186,15 +185,11 @@
                     Status.OK);
         }
         catch (ParseException e) {
-            // TODO Auto-generated catch block
-            e.printStackTrace();
+            return openIdResponseHandler.createErrorResponse(e, null);
         }
         catch (KustvaktException e) {
-            openIdResponseHandler.createTokenErrorResponse(e);
+            return openIdResponseHandler.createTokenErrorResponse(e);
         }
-
-        return null;
-
     }
 
     /**
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
index 9032c67..efdab41 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
@@ -30,6 +30,7 @@
 import com.nimbusds.jose.jwk.RSAKey;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.GrantType;
 import com.sun.jersey.api.client.ClientHandlerException;
 import com.sun.jersey.api.client.ClientResponse;
 import com.sun.jersey.api.client.UniformInterfaceException;
@@ -286,7 +287,7 @@
     }
 
     @Test
-    public void testRequestAccessToken ()
+    public void testRequestAccessTokenWithAuthorizationCode ()
             throws KustvaktException, ParseException, InvalidKeySpecException,
             NoSuchAlgorithmException, JOSEException {
         String client_id = "fCBbQkAyYzI4NzUxMg";
@@ -307,15 +308,17 @@
         String code = params.getFirst("code");
 
         MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+        testRequestAccessTokenMissingGrant(tokenForm);
         tokenForm.add("grant_type", "authorization_code");
-        tokenForm.add("redirect_uri", redirectUri);
-        tokenForm.add("client_id", client_id);
-        tokenForm.add("client_secret", "secret");
         tokenForm.add("code", code);
+        testRequestAccessTokenMissingClientId(tokenForm);
+        tokenForm.add("client_id", client_id);
+        testRequestAccessTokenMissingClientSecret(tokenForm);
+        tokenForm.add("client_secret", "secret");
+        tokenForm.add("redirect_uri", redirectUri);
 
         ClientResponse tokenResponse = sendTokenRequest(tokenForm);
         String entity = tokenResponse.getEntity(String.class);
-
         JsonNode node = JsonUtils.readTree(entity);
         assertNotNull(node.at("/access_token").asText());
         assertNotNull(node.at("/refresh_token").asText());
@@ -328,6 +331,36 @@
         verifyingIdToken(id_token, username, client_id, nonce);
     }
 
+    private void testRequestAccessTokenMissingGrant (
+            MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+        ClientResponse response = sendTokenRequest(tokenForm);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+        assertEquals("Invalid request: Missing \"grant_type\" parameter",
+                node.at("/error_description").asText());
+    }
+
+    private void testRequestAccessTokenMissingClientId (
+            MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+        ClientResponse response = sendTokenRequest(tokenForm);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+        assertEquals("Invalid request: Missing required \"client_id\" "
+                + "parameter", node.at("/error_description").asText());
+    }
+
+    private void testRequestAccessTokenMissingClientSecret (
+            MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+        ClientResponse response = sendTokenRequest(tokenForm);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+        assertEquals("Missing parameters: client_secret",
+                node.at("/error_description").asText());
+    }
+
     private void verifyingIdToken (String id_token, String username,
             String client_id, String nonce) throws ParseException,
             InvalidKeySpecException, NoSuchAlgorithmException, JOSEException {
@@ -347,6 +380,59 @@
         assertEquals(nonce, claimsSet.getClaim("nonce"));
     }
 
+
+    // no openid
+    @Test
+    public void testRequestAccessTokenWithPassword ()
+            throws KustvaktException, ParseException, InvalidKeySpecException,
+            NoSuchAlgorithmException, JOSEException {
+        // public client
+        String client_id = "iBr3LsTCxOj7D2o0A5m";
+        MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+        testRequestAccessTokenMissingGrant(tokenForm);
+
+        tokenForm.add("grant_type", GrantType.PASSWORD.toString());
+        testRequestAccessTokenMissingUsername(tokenForm);
+
+        tokenForm.add("username", username);
+        testRequestAccessTokenMissingPassword(tokenForm);
+
+        tokenForm.add("password", "pass");
+        tokenForm.add("client_id", client_id);
+
+        ClientResponse tokenResponse = sendTokenRequest(tokenForm);
+        String entity = tokenResponse.getEntity(String.class);
+        System.out.println(entity);
+        
+        JsonNode node = JsonUtils.readTree(entity);
+        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());
+    }
+
+
+    private void testRequestAccessTokenMissingUsername (
+            MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+        ClientResponse response = sendTokenRequest(tokenForm);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+        assertEquals("Invalid request: Missing or empty \"username\" parameter",
+                node.at("/error_description").asText());
+    }
+
+    private void testRequestAccessTokenMissingPassword (
+            MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+        ClientResponse response = sendTokenRequest(tokenForm);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+        assertEquals("Invalid request: Missing or empty \"password\" parameter",
+                node.at("/error_description").asText());
+    }
+
     @Test
     public void testPublicKeyAPI () throws KustvaktException {
         ClientResponse response = resource().path("oauth2").path("openid")