Implemented authorization code request, simplified client
authentication, and added tests.

Change-Id: Id6695cacc6da75da64588499ea3a7c7b1ad64591
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
index 66d610f..b194e08 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
@@ -62,6 +62,7 @@
         json.setType(OAuth2ClientType.CONFIDENTIAL);
         json.setUrl("http://example.client.com");
         json.setRedirectURI("https://example.client.com/redirect");
+        json.setDescription("This is a confidential test client.");
 
         return resource().path("oauth2").path("client").path("register")
                 .header(Attributes.AUTHORIZATION,
@@ -89,8 +90,9 @@
         assertEquals(OAuthError.TokenResponse.INVALID_REQUEST,
                 node.at("/error").asText());
 
+        testDeregisterConfidentialClientMissingParameters();
         testDeregisterClientIncorrectCredentials(clientId);
-        testDeregisterConfidentialClient(clientId, clientSecret);
+        testDeregisterConfidentialClient(clientId,clientSecret);
     }
 
     @Test
@@ -101,6 +103,7 @@
         json.setType(OAuth2ClientType.PUBLIC);
         json.setUrl("http://test.public.client.com");
         json.setRedirectURI("https://test.public.client.com/redirect");
+        json.setDescription("This is a public test client.");
 
         ClientResponse response = resource().path("oauth2").path("client")
                 .path("register")
@@ -129,6 +132,7 @@
         json.setType(OAuth2ClientType.PUBLIC);
         json.setUrl("http://korap.ids-mannheim.de/native");
         json.setRedirectURI("https://korap.ids-mannheim.de/native/redirect");
+        json.setDescription("This is a native test client.");
 
         ClientResponse response = resource().path("oauth2").path("client")
                 .path("register")
@@ -140,7 +144,7 @@
                 .entity(json).post(ClientResponse.class);
 
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        
+
         //EM: need to check native
     }
 
@@ -167,8 +171,6 @@
     private void testDeregisterConfidentialClient (String clientId,
             String clientSecret) throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        form.add("client_id", clientId);
 
         ClientResponse response = resource().path("oauth2").path("client")
                 .path("deregister").path("confidential")
@@ -178,16 +180,34 @@
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(HttpHeaders.CONTENT_TYPE,
                         ContentType.APPLICATION_FORM_URLENCODED)
-                .entity(form).delete(ClientResponse.class);
+                .delete(ClientResponse.class);
 
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
     }
 
+    private void testDeregisterConfidentialClientMissingParameters ()
+            throws KustvaktException {
+
+        ClientResponse response = resource().path("oauth2").path("client")
+                .path("deregister").path("confidential")
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .delete(ClientResponse.class);
+
+        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());
+        assertEquals("Missing parameters: client_secret client_id",
+                node.at("/error_description").asText());
+    }
+
     private void testDeregisterClientIncorrectCredentials (String clientId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        form.add("client_id", clientId);
 
         ClientResponse response = resource().path("oauth2").path("client")
                 .path("deregister").path("confidential")
@@ -197,7 +217,7 @@
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(HttpHeaders.CONTENT_TYPE,
                         ContentType.APPLICATION_FORM_URLENCODED)
-                .entity(form).delete(ClientResponse.class);
+                .delete(ClientResponse.class);
 
         String entity = response.getEntity(String.class);
         assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
@@ -205,7 +225,7 @@
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(OAuthError.TokenResponse.INVALID_CLIENT,
                 node.at("/error").asText());
-        assertEquals("Invalid client credentials.",
+        assertEquals("Invalid client credentials",
                 node.at("/error_description").asText());
 
         checkWWWAuthenticateHeader(response);
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 956e3ed..3214fda 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
@@ -2,6 +2,9 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.net.URI;
 
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response.Status;
@@ -14,9 +17,7 @@
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.net.HttpHeaders;
-import com.sun.jersey.api.client.ClientHandlerException;
 import com.sun.jersey.api.client.ClientResponse;
-import com.sun.jersey.api.client.UniformInterfaceException;
 import com.sun.jersey.core.util.MultivaluedMapImpl;
 
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
@@ -34,24 +35,99 @@
     @Autowired
     private HttpAuthorizationHandler handler;
 
-    private ClientResponse testRequestTokenConfidentialClient (
-            MultivaluedMap<String, String> form)
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
-        return resource().path("oauth2").path("token")
+    private ClientResponse requestAuthorizationConfidentialClient (
+            MultivaluedMap<String, String> form) throws KustvaktException {
+
+        return resource().path("oauth2").path("authorize")
                 .header(Attributes.AUTHORIZATION,
                         handler.createBasicAuthorizationHeaderValue(
-                                "fCBbQkAyYzI4NzUxMg==", "secret"))
+                                "fCBbQkAyYzI4NzUxMg", "secret"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(HttpHeaders.CONTENT_TYPE,
                         ContentType.APPLICATION_FORM_URLENCODED)
                 .entity(form).post(ClientResponse.class);
     }
 
-    private ClientResponse testRequestTokenPublicClient (
-            MultivaluedMap<String, String> form)
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+    @Test
+    public void testAuthorizeConfidentialClient () 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");
+        ClientResponse response = requestAuthorizationConfidentialClient(form);
+
+        assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(),
+                response.getStatus());
+        URI redirectUri = response.getLocation();
+        assertTrue(redirectUri.getQuery().startsWith("code"));
+    }
+
+    @Test
+    public void testAuthorizeInvalidRedirectUri () throws KustvaktException {
+        String redirectUri = "https://different.uri/redirect";
+
+        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("redirect_uri", redirectUri);
+        ClientResponse response = requestAuthorizationConfidentialClient(form);
+
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuthError.CodeResponse.INVALID_REQUEST,
+                node.at("/error").asText());
+        assertEquals(redirectUri + " is unknown",
+                node.at("/error_description").asText());
+    }
+
+    @Test
+    public void testAuthorizeMissingRequiredParameters ()
+            throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        // missing code
+        ClientResponse response = requestAuthorizationConfidentialClient(form);
+
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuthError.CodeResponse.INVALID_REQUEST,
+                node.at("/error").asText());
+        assertEquals("Missing response_type parameter value",
+                node.at("/error_description").asText());
+
+        // missing client_id
+        form.add("response_type", "code");
+        response = requestAuthorizationConfidentialClient(form);
+        entity = response.getEntity(String.class);
+        node = JsonUtils.readTree(entity);
+        assertEquals("Missing parameters: client_id",
+                node.at("/error_description").asText());
+    }
+
+    @Test
+    public void testAuthorizeInvalidResponseType () throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("response_type", "string");
+
+        ClientResponse response = requestAuthorizationConfidentialClient(form);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuthError.CodeResponse.INVALID_REQUEST,
+                node.at("/error").asText());
+        assertEquals("Invalid response_type parameter value",
+                node.at("/error_description").asText());
+    }
+
+    private ClientResponse requestToken (MultivaluedMap<String, String> form)
+            throws KustvaktException {
         return resource().path("oauth2").path("token")
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(HttpHeaders.CONTENT_TYPE,
@@ -61,12 +137,15 @@
 
     @Test
     public void testRequestTokenPasswordGrantConfidential ()
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+            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");
 
-        ClientResponse response = testRequestTokenConfidentialClient(form);
+        ClientResponse response = requestToken(form);
         String entity = response.getEntity(String.class);
 
         JsonNode node = JsonUtils.readTree(entity);
@@ -78,71 +157,80 @@
     }
 
     @Test
-    public void testRequestTokenConfidentialMissingSecret ()
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+    public void testRequestTokenPasswordGrantMissingClientSecret ()
+            throws KustvaktException {
 
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", "password");
-        form.add("client_id", "fCBbQkAyYzI4NzUxMg==");
+        form.add("username", "dory");
+        form.add("password", "password");
+        form.add("client_id", "fCBbQkAyYzI4NzUxMg");
 
-        ClientResponse response = testRequestTokenPublicClient(form);
-        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        ClientResponse response = requestToken(form);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
 
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(OAuthError.TokenResponse.INVALID_CLIENT,
+        assertEquals(OAuthError.TokenResponse.INVALID_REQUEST,
                 node.at("/error").asText());
-    }
-
-    @Test
-    public void testRequestTokenPasswordGrantPublic ()
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        form.add("grant_type", "password");
-        form.add("client_id", "iBr3LsTCxOj7D2o0A5m");
-
-        ClientResponse response = testRequestTokenPublicClient(form);
-        String entity = response.getEntity(String.class);
-
-        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());
+        assertEquals("Missing parameters: client_secret",
+                node.at("/error_description").asText());
     }
 
     @Test
     public void testRequestTokenPasswordGrantMissingClientId ()
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+            throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", "password");
+        form.add("username", "dory");
+        form.add("password", "password");
+        form.add("client_secret", "secret");
 
-        ClientResponse response = testRequestTokenPublicClient(form);
+        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_REQUEST,
                 node.at("/error").asText());
-        assertEquals("client_id is missing",
+        assertEquals("Missing parameters: client_id",
                 node.at("/error_description").asText());
     }
+    
+    @Test
+    public void testRequestTokenPasswordGrantPublic ()
+            throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", "password");
+        form.add("username", "dory");
+        form.add("password", "password");
+        form.add("client_id", "iBr3LsTCxOj7D2o0A5m");
+
+        ClientResponse response = requestToken(form);
+        String entity = response.getEntity(String.class);
+
+        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());
+    }
 
     @Test
     public void testRequestTokenPasswordGrantNonNative ()
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+            throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", "password");
-        form.add("client_id", "8bIDtZnH6NvRkW2Fq==");
+        form.add("username", "dory");
+        form.add("password", "password");
+        // confidential nonnative
+        form.add("client_id", "9aHsGW6QflV13ixNpez");
+        form.add("client_secret", "secret");
 
-        ClientResponse response = testRequestTokenPublicClient(form);
+        ClientResponse response = requestToken(form);
         String entity = response.getEntity(String.class);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
 
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT,
@@ -153,13 +241,13 @@
 
     @Test
     public void testRequestTokenClientCredentialsGrant ()
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+            throws KustvaktException {
 
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", "client_credentials");
-
-        ClientResponse response = testRequestTokenConfidentialClient(form);
+        form.add("client_id", "fCBbQkAyYzI4NzUxMg");
+        form.add("client_secret", "secret");
+        ClientResponse response = requestToken(form);
         String entity = response.getEntity(String.class);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
@@ -173,11 +261,9 @@
     }
 
     @Test
-    public void testRequestTokenMissingGrantType ()
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+    public void testRequestTokenMissingGrantType () throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        ClientResponse response = testRequestTokenConfidentialClient(form);
+        ClientResponse response = requestToken(form);
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
 
         String entity = response.getEntity(String.class);
@@ -187,9 +273,7 @@
     }
 
     @Test
-    public void testRequestTokenUnsupportedGrant ()
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+    public void testRequestTokenUnsupportedGrant () throws KustvaktException {
 
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", "blahblah");
diff --git a/full/src/test/resources/test-config.xml b/full/src/test/resources/test-config.xml
index 0445d65..c0f7a2e 100644
--- a/full/src/test/resources/test-config.xml
+++ b/full/src/test/resources/test-config.xml
@@ -167,10 +167,10 @@
 
 	<!-- URLValidator -->
 	<bean id="urlValidator" class="org.apache.commons.validator.routines.UrlValidator">
-		<constructor-arg value="http,https"/>
+		<constructor-arg value="http,https" />
 	</bean>
 	<bean id="httpsValidator" class="org.apache.commons.validator.routines.UrlValidator">
-		<constructor-arg value="https"/>
+		<constructor-arg value="https" />
 	</bean>
 
 	<bean id="kustvakt_rewrite" class="de.ids_mannheim.korap.rewrite.FullRewriteHandler">
@@ -184,10 +184,18 @@
 	<bean id="kustvaktExceptionHandler" class="de.ids_mannheim.korap.web.KustvaktExceptionHandler">
 		<constructor-arg index="0" name="iface" ref="kustvakt_auditing" />
 	</bean>
+
+	<!-- OAuth -->
 	<bean id="oauth2ResponseHandler" class="de.ids_mannheim.korap.web.OAuth2ResponseHandler">
 		<constructor-arg index="0" name="iface" ref="kustvakt_auditing" />
 	</bean>
 
+	<bean id="mdGenerator" class="org.apache.oltu.oauth2.as.issuer.MD5Generator">
+	</bean>
+	<bean id="oauthIssuer" class="org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl">
+		<constructor-arg index="0" ref="mdGenerator" />
+	</bean>
+
 	<bean id="kustvakt_userdb" class="de.ids_mannheim.korap.handlers.EntityDao">
 		<constructor-arg ref="kustvakt_db" />
 	</bean>
@@ -200,8 +208,7 @@
 		<constructor-arg ref="kustvakt_db" />
 	</bean>
 
-	<bean name="kustvakt_encryption"
-		class="de.ids_mannheim.korap.encryption.KustvaktEncryption">
+	<bean name="kustvakt_encryption" class="de.ids_mannheim.korap.encryption.KustvaktEncryption">
 		<constructor-arg ref="kustvakt_config" />
 	</bean>
 
@@ -267,8 +274,8 @@
 	<!-- specify type for constructor argument -->
 	<bean id="kustvakt_authenticationmanager"
 		class="de.ids_mannheim.korap.authentication.KustvaktAuthenticationManager">
-		<constructor-arg
-			type="de.ids_mannheim.korap.interfaces.EntityHandlerIface" ref="kustvakt_userdb" />
+		<constructor-arg type="de.ids_mannheim.korap.interfaces.EntityHandlerIface"
+			ref="kustvakt_userdb" />
 		<constructor-arg type="de.ids_mannheim.korap.interfaces.EncryptionIface"
 			ref="kustvakt_encryption" />
 		<constructor-arg ref="kustvakt_config" />
@@ -309,8 +316,8 @@
 
 	<!-- mail -->
 	<bean id="authenticator" class="de.ids_mannheim.korap.service.MailAuthenticator">
-		<constructor-arg index="0" value="${mail.username}"/>
-		<constructor-arg index="1" value="${mail.password}"/>
+		<constructor-arg index="0" value="${mail.username}" />
+		<constructor-arg index="1" value="${mail.password}" />
 	</bean>
 	<bean id="smtpSession" class="javax.mail.Session" factory-method="getInstance">
 		<constructor-arg index="0">
@@ -323,7 +330,7 @@
 				<prop key="mail.smtp.connectiontimeout">${mail.connectiontimeout}</prop>
 			</props>
 		</constructor-arg>
-		<constructor-arg index="1" ref="authenticator"/>
+		<constructor-arg index="1" ref="authenticator" />
 	</bean>
 	<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
 		<property name="session" ref="smtpSession" />