Updated client registration requirement to allow desktop apps.

Change-Id: I637ebd3e4b3362e6f5b498ee3cb0e0f45a928007
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 d76b057..68b2ac6 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
@@ -203,36 +203,11 @@
             throws IOException, ParseException, JOSEException {
         setRsaKeyId(properties.getProperty("rsa.key.id", ""));
 
-        String rsaPublic = properties.getProperty("rsa.public", "");
-        String rsaPrivate = properties.getProperty("rsa.private", "");
+        String rsaPublic = properties.getProperty("rsa.public", null);
+        setPublicKeySet(rsaPublic);
 
-        File rsaPublicFile = new File(rsaPublic);
-        JWKSet jwkSet = null;
-        if (rsaPublicFile.exists()) {
-            jwkSet = JWKSet.load(rsaPublicFile);
-        }
-        else {
-            InputStream is =
-                    getClass().getClassLoader().getResourceAsStream(rsaPublic);
-            jwkSet = JWKSet.load(is);
-        }
-        setPublicKeySet(jwkSet);
-
-        File rsaPrivateFile = new File(rsaPrivate);
-        String keyString = null;
-        if (rsaPrivateFile.exists()) {
-            keyString = IOUtils.readFileToString(rsaPrivateFile,
-                    Charset.forName("UTF-8"));
-        }
-        else {
-            InputStream is =
-                    getClass().getClassLoader().getResourceAsStream(rsaPrivate);
-            keyString = IOUtils.readInputStreamToString(is,
-                    Charset.forName("UTF-8"));
-        }
-        RSAKey rsaKey = (RSAKey) JWK.parse(keyString);
-        RSAPrivateKey privateKey = (RSAPrivateKey) rsaKey.toPrivateKey();
-        setRsaPrivateKey(privateKey);
+        String rsaPrivate = properties.getProperty("rsa.private", null);
+        setRsaPrivateKey(rsaPrivate);
     }
 
     private void setOAuth2Configuration (Properties properties) {
@@ -251,8 +226,8 @@
                 Arrays.stream(scopes.split(" ")).collect(Collectors.toSet());
         setDefaultAccessScopes(scopeSet);
 
-        String clientScopes = properties.getProperty(
-                "oauth2.client.credentials.scopes", "client_info");
+        String clientScopes = properties
+                .getProperty("oauth2.client.credentials.scopes", "client_info");
         setClientCredentialsScopes(Arrays.stream(clientScopes.split(" "))
                 .collect(Collectors.toSet()));
     }
@@ -549,16 +524,48 @@
         return publicKeySet;
     }
 
-    public void setPublicKeySet (JWKSet publicKeySet) {
-        this.publicKeySet = publicKeySet;
+    public void setPublicKeySet (String rsaPublic)
+            throws IOException, ParseException {
+        if (rsaPublic == null || rsaPublic.isEmpty()) {
+            return;
+        }
+
+        File rsaPublicFile = new File(rsaPublic);
+        JWKSet jwkSet = null;
+        InputStream is = null;
+        if (rsaPublicFile.exists()) {
+            jwkSet = JWKSet.load(rsaPublicFile);
+        }
+        else if ((is = getClass().getClassLoader()
+                .getResourceAsStream(rsaPublic)) != null) {
+            jwkSet = JWKSet.load(is);
+        }
+        this.publicKeySet = jwkSet;
     }
 
     public RSAPrivateKey getRsaPrivateKey () {
         return rsaPrivateKey;
     }
 
-    public void setRsaPrivateKey (RSAPrivateKey rsaPrivateKey) {
-        this.rsaPrivateKey = rsaPrivateKey;
+    public void setRsaPrivateKey (String rsaPrivate)
+            throws IOException, ParseException, JOSEException {
+        if (rsaPrivate == null || rsaPrivate.isEmpty()) {
+            return;
+        }
+        File rsaPrivateFile = new File(rsaPrivate);
+        String keyString = null;
+        InputStream is = null;
+        if (rsaPrivateFile.exists()) {
+            keyString = IOUtils.readFileToString(rsaPrivateFile,
+                    Charset.forName("UTF-8"));
+        }
+        else if ((is = getClass().getClassLoader()
+                .getResourceAsStream(rsaPrivate)) != null) {
+            keyString = IOUtils.readInputStreamToString(is,
+                    Charset.forName("UTF-8"));
+        }
+        RSAKey rsaKey = (RSAKey) JWK.parse(keyString);
+        this.rsaPrivateKey = (RSAPrivateKey) rsaKey.toPrivateKey();
     }
 
     public String getRsaKeyId () {
diff --git a/full/src/main/java/de/ids_mannheim/korap/dto/OAuth2ClientDto.java b/full/src/main/java/de/ids_mannheim/korap/dto/OAuth2ClientDto.java
index 1840fd3..b714404 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dto/OAuth2ClientDto.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dto/OAuth2ClientDto.java
@@ -1,22 +1,34 @@
 package de.ids_mannheim.korap.dto;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
 
-import lombok.Getter;
-import lombok.Setter;
-
-@Getter
-@Setter
+@JsonInclude(Include.NON_EMPTY)
 public class OAuth2ClientDto {
 
     private String client_id;
-    @JsonInclude(JsonInclude.Include.NON_EMPTY)
     private String client_secret;
 
     public OAuth2ClientDto () {}
 
     public OAuth2ClientDto (String id, String secret) {
-        this.client_id = id;
-        this.client_secret = secret;
+        this.setClient_id(id);
+        this.setClient_secret(secret);
+    }
+
+    public String getClient_id () {
+        return client_id;
+    }
+
+    public void setClient_id (String client_id) {
+        this.client_id = client_id;
+    }
+
+    public String getClient_secret () {
+        return client_secret;
+    }
+
+    public void setClient_secret (String client_secret) {
+        this.client_secret = client_secret;
     }
 }
\ No newline at end of file
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
index 1bad7fa..89f2d49 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
@@ -32,8 +32,10 @@
         ParameterChecker.checkStringValue(id, "client id");
         ParameterChecker.checkStringValue(name, "client name");
         ParameterChecker.checkObjectValue(type, "client type");
-        ParameterChecker.checkStringValue(url, "client url");
-        ParameterChecker.checkStringValue(redirectURI, "client redirect uri");
+        ParameterChecker.checkStringValue(description, "client description");
+        // ParameterChecker.checkStringValue(url, "client url");
+        // ParameterChecker.checkStringValue(redirectURI, "client
+        // redirect uri");
         ParameterChecker.checkStringValue(registeredBy, "registeredBy");
 
         OAuth2Client client = new OAuth2Client();
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
index dff46ae..3ae7c54 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
@@ -43,7 +43,8 @@
 @Service
 public class OAuth2ClientService {
 
-    private Logger jlog = Logger.getLogger(OAuth2ClientService.class);
+    // private Logger jlog =
+    // Logger.getLogger(OAuth2ClientService.class);
 
     @Autowired
     private OAuth2ClientDao clientDao;
@@ -52,20 +53,32 @@
     @Autowired
     private UrlValidator redirectURIValidator;
     @Autowired
+    private UrlValidator urlValidator;
+    @Autowired
     private EncryptionIface encryption;
     @Autowired
     private FullConfiguration config;
 
     public OAuth2ClientDto registerClient (OAuth2ClientJson clientJson,
             String registeredBy) throws KustvaktException {
-        if (!redirectURIValidator.isValid(clientJson.getUrl())) {
-            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
-                    clientJson.getUrl() + " is invalid.",
-                    OAuth2Error.INVALID_REQUEST);
+        String url = clientJson.getUrl();
+        int urlHashCode = 0;
+        if (url != null && !url.isEmpty()) {
+            urlHashCode = clientJson.getUrl().hashCode();
+            if (!redirectURIValidator.isValid(url)) {
+                throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                        url + " is invalid.", OAuth2Error.INVALID_REQUEST);
+            }
         }
-        
-        boolean isNative = isNativeClient(clientJson.getUrl(),
-                clientJson.getRedirectURI());
+
+        String redirectURI = clientJson.getRedirectURI();
+        if (redirectURI != null && !redirectURI.isEmpty()
+                && !urlValidator.isValid(redirectURI)) {
+            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+                    redirectURI + " is invalid.", OAuth2Error.INVALID_REQUEST);
+        }
+
+        boolean isNative = isNativeClient(url, redirectURI);
 
         String secret = null;
         String secretHashcode = null;
@@ -90,9 +103,8 @@
         String id = encryption.createRandomNumber();
         try {
             clientDao.registerClient(id, secretHashcode, clientJson.getName(),
-                    clientJson.getType(), isNative, clientJson.getUrl(),
-                    clientJson.getUrl().hashCode(), clientJson.getRedirectURI(),
-                    registeredBy, clientJson.getDescription());
+                    clientJson.getType(), isNative, url, urlHashCode,
+                    redirectURI, registeredBy, clientJson.getDescription());
         }
         catch (Exception e) {
             Throwable cause = e;
@@ -114,6 +126,11 @@
 
     private boolean isNativeClient (String url, String redirectURI)
             throws KustvaktException {
+        if (url == null || url.isEmpty() || redirectURI == null
+                || redirectURI.isEmpty()) {
+            return false;
+        }
+
         String nativeHost = config.getNativeClientHost();
         String urlHost = null;
         try {
@@ -124,6 +141,11 @@
                     "Invalid url :" + e.getMessage(),
                     OAuth2Error.INVALID_REQUEST);
         }
+
+        if (!urlHost.equals(nativeHost)) {
+            return false;
+        }
+
         String uriHost = null;
         try {
             uriHost = new URI(redirectURI).getHost();
@@ -133,10 +155,11 @@
                     "Invalid redirectURI: " + e.getMessage(),
                     OAuth2Error.INVALID_REQUEST);
         }
-        boolean isNative =
-                urlHost.equals(nativeHost) && uriHost.equals(nativeHost);
-        jlog.debug(urlHost + " " + uriHost + " " + isNative);
-        return isNative;
+        if (!uriHost.equals(nativeHost)) {
+            return false;
+        }
+
+        return true;
     }
 
 
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
index 4a70807..aff9e19 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
@@ -37,7 +37,7 @@
 import de.ids_mannheim.korap.web.utils.FormRequestWrapper;
 
 @Controller
-@Path("/oauth2")
+@Path("oauth2")
 public class OAuth2Controller {
 
     @Autowired
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 141e8ec..43e940b 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
@@ -45,7 +45,7 @@
 import de.ids_mannheim.korap.web.utils.MapUtils;
 
 @Controller
-@Path("/oauth2/openid")
+@Path("oauth2/openid")
 public class OAuth2WithOpenIdController {
 
     @Autowired
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 fe2c770..e4f2e72 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
@@ -42,7 +42,7 @@
  *
  */
 @Controller
-@Path("/oauth2/client")
+@Path("oauth2/client")
 public class OAuthClientController {
 
     @Autowired
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java b/full/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java
index 2d7afcd..381edc0 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/input/OAuth2ClientJson.java
@@ -1,24 +1,68 @@
 package de.ids_mannheim.korap.web.input;
 
 import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
-import lombok.Getter;
-import lombok.Setter;
 
-/** Defines required attributes to register an OAuth2 client. 
+/**
+ * Defines attributes to register an OAuth2 client. Application name,
+ * client type and description are required attributes. 
+ * 
+ * To accommodate desktop applications such as R, url and redirectURI
+ * are not compulsory.
  * 
  * @author margaretha
  *
  */
-@Setter
-@Getter
 public class OAuth2ClientJson {
 
-    // all required for registration
+    // required
     private String name;
     private OAuth2ClientType type;
-    private String url;
-    // redirect URI determines where the OAuth 2.0 service will return the user to 
-    // after they have authorized a client.
-    private String redirectURI;
     private String description;
+    
+    // optional
+    private String url;
+    // redirect URI determines where the OAuth 2.0 service will return
+    // the user to after they have authorized a client.
+    private String redirectURI;
+    
+
+    public String getName () {
+        return name;
+    }
+
+    public void setName (String name) {
+        this.name = name;
+    }
+
+    public OAuth2ClientType getType () {
+        return type;
+    }
+
+    public void setType (OAuth2ClientType type) {
+        this.type = type;
+    }
+
+    public String getUrl () {
+        return url;
+    }
+
+    public void setUrl (String url) {
+        this.url = url;
+    }
+
+    public String getRedirectURI () {
+        return redirectURI;
+    }
+
+    public void setRedirectURI (String redirectURI) {
+        this.redirectURI = redirectURI;
+    }
+
+    public String getDescription () {
+        return description;
+    }
+
+    public void setDescription (String description) {
+        this.description = description;
+    }
 }
diff --git a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
index fb85417..98e3c7b 100644
--- a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
@@ -4,14 +4,14 @@
 CREATE TABLE IF NOT EXISTS oauth2_client (
 	id VARCHAR(100) PRIMARY KEY NOT NULL,
 	name VARCHAR(200) NOT NULL,
-	secret VARCHAR(200),
+	secret VARCHAR(200) DEFAULT NULL,
 	type VARCHAR(200) NOT NULL,
 	native BOOLEAN DEFAULT FALSE,
-	url TEXT NOT NULL,
-	url_hashcode INTEGER NOT NULL,
-	redirect_uri TEXT NOT NULL,
-	registered_by VARCHAR(100) NOT NULL,
+	url TEXT DEFAULT NULL,
+	url_hashcode INTEGER,
+	redirect_uri TEXT DEFAULT NULL,
 	description VARCHAR(250) NOT NULL,
+	registered_by VARCHAR(100) NOT NULL,
 	UNIQUE INDEX unique_url(url_hashcode)
 );
 
diff --git a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
index 71a2d75..e6506cd 100644
--- a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
@@ -4,14 +4,14 @@
 CREATE TABLE IF NOT EXISTS oauth2_client (
 	id VARCHAR(100) PRIMARY KEY NOT NULL,
 	name VARCHAR(255) NOT NULL,
-	secret VARCHAR(255),
+	secret VARCHAR(255) DEFAULT NULL,
 	type VARCHAR(255) NOT NULL,
 	native BOOLEAN DEFAULT FALSE,
-	url TEXT NOT NULL,
-	url_hashcode INTEGER NOT NULL,
-	redirect_uri TEXT NOT NULL,
-	registered_by VARCHAR(100) NOT NULL,
-	description VARCHAR(255) NOT NULL
+	url TEXT DEFAULT NULL,
+	url_hashcode INTEGER,
+	redirect_uri TEXT DEFAULT NULL,
+	description VARCHAR(255) NOT NULL,
+	registered_by VARCHAR(100) NOT NULL
 );
 
 CREATE UNIQUE INDEX client_id_index on oauth2_client(id);
diff --git a/full/src/main/resources/default-config.xml b/full/src/main/resources/default-config.xml
index 5c2a050..bb5ac1e 100644
--- a/full/src/main/resources/default-config.xml
+++ b/full/src/main/resources/default-config.xml
@@ -184,6 +184,10 @@
 				static-field="org.apache.commons.validator.routines.UrlValidator.NO_FRAGMENTS" />
 		</constructor-arg>
 	</bean>
+	<bean id="urlValidator" class="org.apache.commons.validator.routines.UrlValidator">
+		<constructor-arg value="http,https" />
+	</bean>
+	
 
 	<bean id="kustvakt_rewrite" class="de.ids_mannheim.korap.rewrite.FullRewriteHandler">
 		<constructor-arg ref="kustvakt_config" />
@@ -193,7 +197,7 @@
 		<constructor-arg ref="kustvakt_db" />
 	</bean>
 
-	<bean id="kustvaktResponseHandler" class="de.ids_mannheim.korap.web.kustvaktResponseHandler">
+	<bean id="kustvaktResponseHandler" class="de.ids_mannheim.korap.web.KustvaktResponseHandler">
 		<constructor-arg index="0" name="iface" ref="kustvakt_auditing" />
 	</bean>
 
diff --git a/full/src/main/resources/kustvakt.conf b/full/src/main/resources/kustvakt.conf
index 8adf152..da7ec20 100644
--- a/full/src/main/resources/kustvakt.conf
+++ b/full/src/main/resources/kustvakt.conf
@@ -50,8 +50,8 @@
 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
+oauth2.default.scopes = username email 
+oauth2.client.credentials.scopes = client_info
 
 # JWT
 security.jwt.issuer=korap.ids-mannheim.de
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 c32ccac..b9d640f 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
@@ -87,12 +87,11 @@
         response = testRegisterConfidentialClient();
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
         node = JsonUtils.readTree(response.getEntity(String.class));
-        assertEquals(OAuth2Error.INVALID_REQUEST,
-                node.at("/error").asText());
+        assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
 
         testDeregisterConfidentialClientMissingParameters();
         testDeregisterClientIncorrectCredentials(clientId);
-        testDeregisterConfidentialClient(clientId,clientSecret);
+        testDeregisterConfidentialClient(clientId, clientSecret);
     }
 
     @Test
@@ -145,7 +144,32 @@
 
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
-        //EM: need to check native
+        // EM: need to check native
+    }
+
+    @Test
+    public void testRegisterDesktopApp () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        OAuth2ClientJson json = new OAuth2ClientJson();
+        json.setName("OAuth2DesktopClient");
+        json.setType(OAuth2ClientType.PUBLIC);
+        json.setDescription("This is a desktop test client.");
+
+        ClientResponse response = resource().path("oauth2").path("client")
+                .path("register")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue(username,
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .entity(json).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(entity);
+        String clientId = node.at("/client_id").asText();
+        assertNotNull(clientId);
+        assertTrue(node.at("/client_secret").isMissingNode());
     }
 
     private void testDeregisterPublicClient (String clientId)
@@ -199,8 +223,7 @@
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
 
         JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(OAuth2Error.INVALID_REQUEST,
-                node.at("/error").asText());
+        assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
         assertEquals("Missing parameters: client_secret client_id",
                 node.at("/error_description").asText());
     }
@@ -223,8 +246,7 @@
         assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
 
         JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(OAuth2Error.INVALID_CLIENT,
-                node.at("/error").asText());
+        assertEquals(OAuth2Error.INVALID_CLIENT, node.at("/error").asText());
         assertEquals("Invalid client credentials",
                 node.at("/error_description").asText());
 
diff --git a/full/src/test/resources/test-config.xml b/full/src/test/resources/test-config.xml
index 180be50..cad93aa 100644
--- a/full/src/test/resources/test-config.xml
+++ b/full/src/test/resources/test-config.xml
@@ -185,8 +185,9 @@
 				static-field="org.apache.commons.validator.routines.UrlValidator.NO_FRAGMENTS" />
 		</constructor-arg>
 	</bean>
-	<!-- <bean id="httpsValidator" class="org.apache.commons.validator.routines.UrlValidator"> 
-		<constructor-arg value="https" /> </bean> -->
+	<bean id="urlValidator" class="org.apache.commons.validator.routines.UrlValidator">
+		<constructor-arg value="http,https" />
+	</bean>
 
 	<bean id="kustvakt_rewrite" class="de.ids_mannheim.korap.rewrite.FullRewriteHandler">
 		<constructor-arg ref="kustvakt_config" />