Implemented OAuth2 request token with resource owner password grant.
Change-Id: I516d5adf0091d711ff183470b3f0de8a6e502270
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java b/full/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java
index acef1d5..991e8f4 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java
@@ -200,6 +200,10 @@
// IdM/LDAP: (09.02.17/FB)
user = authenticateIdM(username, password, attributes);
break;
+ // EM: added a dummy authentication for testing
+ case TEST:
+ user = getUser(username);
+ break;
default:
user = authenticate(username, password, attributes);
break;
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 61b158e..b0da1f8 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
@@ -6,6 +6,7 @@
import java.util.Properties;
import java.util.regex.Pattern;
+import de.ids_mannheim.korap.constant.AuthenticationMethod;
import de.ids_mannheim.korap.interfaces.EncryptionIface;
/** Configuration for Kustvakt full version including properties concerning
@@ -45,7 +46,10 @@
private boolean isSoftDeleteGroupMember;
private EncryptionIface.Encryption encryption;
-
+
+ private AuthenticationMethod OAuth2passwordAuthentication;
+ private String nativeClientHost;
+
public FullConfiguration (Properties properties) throws IOException {
super(properties);
}
@@ -65,6 +69,16 @@
setEncryption(Enum.valueOf(EncryptionIface.Encryption.class,
properties.getProperty("security.encryption", "BCRYPT")));
+
+ setOAuth2Configuration(properties);
+ }
+
+ private void setOAuth2Configuration (Properties properties) {
+ setOAuth2passwordAuthentication(
+ Enum.valueOf(AuthenticationMethod.class, properties
+ .getProperty("oauth.password.authentication", "TEST")));
+ setNativeClientHost(properties.getProperty("oauth.native.client.host",
+ "korap.ids-mannheim.de"));
}
private void setMailConfiguration (Properties properties) {
@@ -297,4 +311,21 @@
this.encryption = encryption;
}
+ public AuthenticationMethod getOAuth2passwordAuthentication () {
+ return OAuth2passwordAuthentication;
+ }
+
+ public void setOAuth2passwordAuthentication (
+ AuthenticationMethod oAuth2passwordAuthentication) {
+ OAuth2passwordAuthentication = oAuth2passwordAuthentication;
+ }
+
+ public String getNativeClientHost () {
+ return nativeClientHost;
+ }
+
+ public void setNativeClientHost (String nativeClientHost) {
+ this.nativeClientHost = nativeClientHost;
+ }
+
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/constant/AuthenticationMethod.java b/full/src/main/java/de/ids_mannheim/korap/constant/AuthenticationMethod.java
index 1beec64..7c7dbae 100644
--- a/full/src/main/java/de/ids_mannheim/korap/constant/AuthenticationMethod.java
+++ b/full/src/main/java/de/ids_mannheim/korap/constant/AuthenticationMethod.java
@@ -10,5 +10,5 @@
*
*/
public enum AuthenticationMethod {
- LDAP, SHIBBOLETH, DATABASE;
+ LDAP, SHIBBOLETH, DATABASE, TEST;
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
index d6878f4..6e75f65 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
@@ -27,7 +27,7 @@
private EntityManager entityManager;
public void registerClient (String id, String secretHashcode, String name,
- OAuth2ClientType type, String url, int urlHashCode,
+ OAuth2ClientType type, boolean isNative, String url, int urlHashCode,
String redirectURI, String registeredBy) throws KustvaktException {
ParameterChecker.checkStringValue(id, "client id");
ParameterChecker.checkStringValue(name, "client name");
@@ -41,6 +41,7 @@
client.setName(name);
client.setSecret(secretHashcode);
client.setType(type);
+ client.setNative(isNative);
client.setUrl(url);
client.setUrlHashCode(urlHashCode);
client.setRedirectURI(redirectURI);
diff --git a/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java b/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
index 9383929..760fc13 100644
--- a/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
+++ b/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
@@ -28,6 +28,8 @@
private String secret;
@Enumerated(EnumType.STRING)
private OAuth2ClientType type;
+ @Column(name = "native")
+ private boolean isNative;
private String url;
@Column(name = "url_hashcode")
private int urlHashCode;
@@ -36,7 +38,6 @@
@Column(name = "registered_by")
private String registeredBy;
-
@Override
public String toString () {
return "id=" + id + ", secret=" + secret + ", type=" + type + ", name="
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
index 92b8837..4072cf7 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
@@ -1,8 +1,13 @@
package de.ids_mannheim.korap.service;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
import java.sql.SQLException;
import org.apache.commons.validator.routines.UrlValidator;
+import org.apache.log4j.Logger;
import org.apache.oltu.oauth2.common.error.OAuthError;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -39,6 +44,8 @@
@Service
public class OAuth2ClientService {
+ private Logger jlog = Logger.getLogger(OAuth2ClientService.class);
+
@Autowired
private OAuth2ClientDao clientDao;
@Autowired
@@ -54,7 +61,6 @@
@Autowired
private FullConfiguration config;
-
public OAuth2ClientDto registerClient (OAuth2ClientJson clientJson,
String registeredBy) throws KustvaktException {
if (!urlValidator.isValid(clientJson.getUrl())) {
@@ -69,6 +75,9 @@
OAuthError.TokenResponse.INVALID_REQUEST);
}
+ boolean isNative = isNativeClient(clientJson.getUrl(),
+ clientJson.getRedirectURI());
+
String secret = null;
String secretHashcode = null;
if (clientJson.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
@@ -90,7 +99,7 @@
String id = encryption.createRandomNumber();
try {
clientDao.registerClient(id, secretHashcode, clientJson.getName(),
- clientJson.getType(), clientJson.getUrl(),
+ clientJson.getType(), isNative, clientJson.getUrl(),
clientJson.getUrl().hashCode(), clientJson.getRedirectURI(),
registeredBy);
}
@@ -113,6 +122,33 @@
}
+ private boolean isNativeClient (String url, String redirectURI)
+ throws KustvaktException {
+ String nativeHost = config.getNativeClientHost();
+ String urlHost = null;
+ try {
+ urlHost = new URL(url).getHost();
+ }
+ catch (MalformedURLException e) {
+ throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+ "Invalid url :" + e.getMessage(),
+ OAuthError.TokenResponse.INVALID_REQUEST);
+ }
+ String uriHost = null;
+ try {
+ uriHost = new URI(redirectURI).getHost();
+ }
+ catch (URISyntaxException e) {
+ throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+ "Invalid redirectURI: "+e.getMessage(), OAuthError.TokenResponse.INVALID_REQUEST);
+ }
+ boolean isNative =
+ urlHost.equals(nativeHost) && uriHost.equals(nativeHost);
+ jlog.debug(urlHost + " " + uriHost + " " + isNative);
+ return isNative;
+ }
+
+
public void deregisterPublicClient (String clientId, String username)
throws KustvaktException {
@@ -145,13 +181,32 @@
clientDao.deregisterClient(client);
}
+ public OAuth2Client authenticateClient (String authorization,
+ String clientId) throws KustvaktException {
+ OAuth2Client client;
+ if (authorization == null || authorization.isEmpty()) {
+ client = authenticateClientById(clientId);
+ if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+ throw new KustvaktException(
+ StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+ "Client authentication using authorization header is required.",
+ OAuthError.TokenResponse.INVALID_CLIENT);
+ }
+ }
+ else {
+ client = authenticateClientByBasicAuthorization(authorization,
+ clientId);
+ }
+ return client;
+ }
+
public OAuth2Client authenticateClientById (String clientId)
throws KustvaktException {
- if (clientId == null || clientId.isEmpty()) {
+ if (clientId == null || clientId.equals("null") || clientId.isEmpty()) {
throw new KustvaktException(
StatusCodes.CLIENT_AUTHENTICATION_FAILED,
- "client_id is missing.",
- OAuthError.TokenResponse.INVALID_CLIENT);
+ "client_id is missing",
+ OAuthError.TokenResponse.INVALID_REQUEST);
}
else {
return retrieveClientById(clientId);
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/OAuth2Service.java b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2Service.java
index 1944b2b..4ec5dac 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/OAuth2Service.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2Service.java
@@ -1,5 +1,7 @@
package de.ids_mannheim.korap.service;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletResponse;
@@ -18,11 +20,12 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import de.ids_mannheim.korap.config.Attributes;
import de.ids_mannheim.korap.config.FullConfiguration;
-import de.ids_mannheim.korap.constant.OAuth2ClientType;
import de.ids_mannheim.korap.entity.OAuth2Client;
import de.ids_mannheim.korap.exceptions.KustvaktException;
import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.interfaces.AuthenticationManagerIface;
@Service
public class OAuth2Service {
@@ -31,7 +34,8 @@
private OAuth2ClientService clientService;
@Autowired
private FullConfiguration config;
-
+ @Autowired
+ private AuthenticationManagerIface authenticationManager;
/**
* RFC 6749:
@@ -40,18 +44,11 @@
* credentials, the client MUST authenticate with the authorization server.
* @param request
*
+ * @param oAuthRequest
* @param authorization
- * @param grantType
- * @param scope
- * @param password
- * @param username
- * @param clientId required for authorization_code grant, otherwise optional
- * @param redirectURI
- * @param authorizationCode
- * @return
+ * @return
* @throws KustvaktException
- * @throws OAuthProblemException
- * @throws OAuthSystemException
+ * @throws OAuthSystemException
*/
public OAuthResponse requestAccessToken (OAuthTokenRequest oAuthRequest,
String authorization)
@@ -67,7 +64,7 @@
else if (grantType.equals(GrantType.PASSWORD.toString())) {
return requestAccessTokenWithPassword(authorization,
oAuthRequest.getUsername(), oAuthRequest.getPassword(),
- oAuthRequest.getScopes());
+ oAuthRequest.getScopes(), oAuthRequest.getClientId());
}
else if (grantType.equals(GrantType.CLIENT_CREDENTIALS.toString())) {
return requestAccessTokenWithClientCredentials(authorization,
@@ -95,39 +92,72 @@
private OAuthResponse requestAccessTokenWithAuthorizationCode (
String authorization, String authorizationCode, String redirectURI,
String clientId) throws KustvaktException {
- OAuth2Client client;
- if (authorization == null || authorization.isEmpty()) {
- client = clientService.authenticateClientById(clientId);
- if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
- throw new KustvaktException(
- StatusCodes.CLIENT_AUTHENTICATION_FAILED,
- "Client authentication using authorization header is required.",
- OAuthError.TokenResponse.INVALID_CLIENT);
- }
- }
- else {
- client = clientService.authenticateClientByBasicAuthorization(
- authorization, clientId);
- }
+ OAuth2Client client =
+ clientService.authenticateClient(authorization, clientId);
// TODO
return null;
}
- /** Confidential clients must authenticate
+
+
+ /** Third party apps must not be allowed to use password grant.
+ * MH: password grant is only allowed for trusted clients (korap frontend)
+ *
+ * A similar rule to that of authorization code grant is additionally
+ * applied, namely client_id is required when authorization header is not
+ * available.
+ *
+ * According to RFC 6749, client_id is optional for password grant,
+ * but without it, server would not be able to check the client
+ * type, thus cannot make sure that confidential clients authenticate.
*
* @param authorization
* @param username
* @param password
* @param scopes
+ * @param clientId
* @return
+ * @throws KustvaktException
+ * @throws OAuthSystemException
*/
private OAuthResponse requestAccessTokenWithPassword (String authorization,
- String username, String password, Set<String> scopes) {
+ String username, String password, Set<String> scopes,
+ String clientId) throws KustvaktException, OAuthSystemException {
+ OAuth2Client client =
+ clientService.authenticateClient(authorization, clientId);
+ if (!client.isNative()) {
+ throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+ "Password grant is not allowed for third party clients",
+ OAuthError.TokenResponse.UNAUTHORIZED_CLIENT);
+ }
- return null;
+ authenticateUser(username, password, scopes);
+ return createsAccessTokenResponse();
+ }
+
+ private void authenticateUser (String username, String password,
+ Set<String> scopes) throws KustvaktException {
+ if (username == null || username.isEmpty()) {
+ throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+ "username is missing.",
+ OAuthError.TokenResponse.INVALID_REQUEST);
+ }
+ if (password == null || password.isEmpty()) {
+ throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+ "password is missing",
+ OAuthError.TokenResponse.INVALID_REQUEST);
+ }
+
+ Map<String, Object> attributes = new HashMap<>();
+ if (scopes != null && !scopes.isEmpty()) {
+ attributes.put(Attributes.SCOPES, scopes);
+ }
+ authenticationManager.authenticate(
+ config.getOAuth2passwordAuthentication(), username, password,
+ attributes);
}
/** Clients must authenticate
@@ -174,7 +204,7 @@
r = OAuthASResponse.tokenResponse(HttpServletResponse.SC_OK)
.setAccessToken(accessToken)
.setTokenType(TokenType.BEARER.toString())
- .setExpiresIn(String.valueOf(config.getLongTokenTTL()))
+ .setExpiresIn(String.valueOf(config.getTokenTTL()))
.setRefreshToken(refreshToken).buildJSONMessage();
// scope
return r;
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/AnnotationController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/AnnotationController.java
index 704e4a7..5ddd8d1 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/AnnotationController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/AnnotationController.java
@@ -72,7 +72,7 @@
public List<FoundryDto> getFoundryDescriptions (String json) {
if (json == null || json.isEmpty()) {
throw kustvaktExceptionHandler
- .throwit(new KustvaktException(StatusCodes.MISSING_ARGUMENT,
+ .throwit(new KustvaktException(StatusCodes.MISSING_PARAMETER,
"Missing a json string.", ""));
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
index e075321..73181f4 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/AuthenticationController.java
@@ -179,7 +179,7 @@
headers.getRequestHeader(ContainerRequest.AUTHORIZATION);
if (auth == null || auth.isEmpty()) {
throw kustvaktExceptionHandler
- .throwit(new KustvaktException(StatusCodes.MISSING_ARGUMENT,
+ .throwit(new KustvaktException(StatusCodes.MISSING_PARAMETER,
"Authorization header is missing.",
"Authorization header"));
}
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 75cc85b..ece03f2 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,25 +37,18 @@
* requests representing user authorization for the client to access user
* resources.
*
- * EM: should we allow client_secret in the request body?
- *
- * @param securityContext
- * @param authorization
- * @param grantType
- * @param authorizationCode
- * @param redirectURI
- * @param client_id a client id required for authorization_code grant, otherwise optional
- * @param username
- * @param password
- * @param scope
- * @return
+ * @param request the request
+ * @param authorization authorization header
+ * @param form form parameters in a map
+ * @return a JSON object containing an access token, a refresh token,
+ * a token type and token expiry/life time (in seconds) if successful,
+ * an error code and an error description otherwise.
*/
@POST
@Path("token")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
public Response requestAccessToken (@Context HttpServletRequest request,
- @Context SecurityContext securityContext,
@HeaderParam("Authorization") String authorization,
MultivaluedMap<String, String> form) {
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/StatisticController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/StatisticController.java
index a468302..ebce268 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/StatisticController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/StatisticController.java
@@ -69,7 +69,7 @@
if (corpusQuery == null || corpusQuery.isEmpty()) {
throw kustvaktExceptionHandler
- .throwit(new KustvaktException(StatusCodes.MISSING_ARGUMENT,
+ .throwit(new KustvaktException(StatusCodes.MISSING_PARAMETER,
"Parameter corpusQuery is missing.",
"corpusQuery"));
}
diff --git a/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql b/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
index 8633a11..3cc2999 100644
--- a/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
+++ b/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
@@ -1,15 +1,22 @@
-- test clients
-- plain secret value is "secret"
-INSERT INTO oauth2_client(id,name,secret,type,url,url_hashcode,redirect_uri,
- registered_by)
+INSERT INTO oauth2_client(id,name,secret,type,native, url,url_hashcode,
+ redirect_uri,registered_by)
VALUES ("fCBbQkAyYzI4NzUxMg==","test confidential client",
"$2a$08$vi1FbuN3p6GcI1tSxMAoeuIYL8Yw3j6A8wJthaN8ZboVnrQaTwLPq",
- "CONFIDENTIAL","http://confidential.client.com", -1097645390,
- "https://confidential.client.com/redirect", "system");
+ "CONFIDENTIAL", 1, "http://korap.ids-mannheim.de/confidential", 2087150261,
+ "https://korap.ids-mannheim.de/confidential/redirect", "system");
-INSERT INTO oauth2_client(id,name,secret,type,url,url_hashcode,redirect_uri,
- registered_by)
-VALUES ("8bIDtZnH6NvRkW2Fq==","test public client",null,
- "PUBLIC","http://public.client.com", -1408041551,
- "https://public.client.com/redirect","system");
\ No newline at end of file
+INSERT INTO oauth2_client(id,name,secret,type,url,url_hashcode,
+ redirect_uri, registered_by)
+VALUES ("8bIDtZnH6NvRkW2Fq==","third party client",null,
+ "PUBLIC","http://third.party.client.com", -2137275617,
+ "https://third.party.client.com/redirect","system");
+
+INSERT INTO oauth2_client(id,name,secret,type,native,url,url_hashcode,
+ redirect_uri, registered_by)
+VALUES ("iBr3LsTCxOj7D2o0A5m","test public client",null,
+ "PUBLIC", 1, "http://korap.ids-mannheim.de/public", 1360724310,
+ "https://korap.ids-mannheim.de/public/redirect","system");
+
\ No newline at end of file
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 ad3af94..c55ef43 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
@@ -6,6 +6,7 @@
name VARCHAR(200) NOT NULL,
secret VARCHAR(200),
type VARCHAR(200) NOT NULL,
+ native BOOLEAN DEFAULT FALSE,
url TEXT NOT NULL,
url_hashcode UNIQUE INTEGER NOT NULL,
redirect_uri TEXT NOT NULL,
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 835acd6..89014f2 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
@@ -6,6 +6,7 @@
name VARCHAR(200) NOT NULL,
secret VARCHAR(200),
type VARCHAR(200) NOT NULL,
+ native BOOLEAN DEFAULT FALSE,
url TEXT NOT NULL,
url_hashcode INTEGER NOT NULL,
redirect_uri TEXT NOT NULL,
diff --git a/full/src/main/resources/kustvakt.conf b/full/src/main/resources/kustvakt.conf
index 31d6f11..99e3b94 100644
--- a/full/src/main/resources/kustvakt.conf
+++ b/full/src/main/resources/kustvakt.conf
@@ -43,13 +43,19 @@
## options referring to the security module!
-## token expiration time in minutes!
+## OAuth
+### (see de.ids_mannheim.korap.constant.AuthenticationMethod for possible
+### oauth.password.authentication values)
+oauth.password.authentication = TEST
+
+# JWT
+security.jwt.issuer=korap.ids-mannheim.de
+
+## token expiration
security.longTokenTTL=150D
security.tokenTTL=72H
security.shortTokenTTL=45M
-security.jwt.issuer=korap.ids-mannheim.de
-
## specifies the user data field that is used to salt user passwords
security.passcode.salt=salt