Updated client registration requirement to allow desktop apps.
Change-Id: I637ebd3e4b3362e6f5b498ee3cb0e0f45a928007
diff --git a/full/Changes b/full/Changes
index d33aeca..581dfab 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,5 +1,5 @@
version 0.60.4
-28/06/2018
+02/07/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)
@@ -13,6 +13,9 @@
- 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)
+ - updated redirect URI validator (margaretha)
+ - updated client registration requirement to allow desktop applications (margaretha)
+ - fixed RSA key configuration (margaretha)
version 0.60.3
06/06/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 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" />