Implemented OAuth2 request token with resource owner password grant.

Change-Id: I516d5adf0091d711ff183470b3f0de8a6e502270
diff --git a/full/Changes b/full/Changes
index 733d853..6b13254 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,5 +1,5 @@
 version 0.60.2
-16/04/2018
+17/04/2018
 	- implemented OAuth2 client registration (margaretha)
 	- implemented OAuth2 client authentication (margaretha)
 	- changed virtual corpus search to retrieval (margaretha)
@@ -9,6 +9,7 @@
 	- fixed storing client secret (margaretha)
 	- implemented OAuth2 exception handler (margaretha)
 	- implemented OAuth2 request access token with client credentials grant (margaretha)
+	- implemented OAuth2 request access token with resource owner password grant (margaretha)
 	
 version 0.60.1
 28/03/2018
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
 
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 6e1904b..66d610f 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
@@ -53,7 +53,7 @@
             }
         }
     }
-    
+
     private ClientResponse testRegisterConfidentialClient ()
             throws KustvaktException {
 
@@ -121,6 +121,29 @@
         testDeregisterPublicClient(clientId);
     }
 
+    @Test
+    public void testRegisterNativeClient () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        OAuth2ClientJson json = new OAuth2ClientJson();
+        json.setName("NativeClient");
+        json.setType(OAuth2ClientType.PUBLIC);
+        json.setUrl("http://korap.ids-mannheim.de/native");
+        json.setRedirectURI("https://korap.ids-mannheim.de/native/redirect");
+
+        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);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        //EM: need to check native
+    }
+
     private void testDeregisterPublicClient (String clientId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
@@ -178,13 +201,13 @@
 
         String entity = response.getEntity(String.class);
         assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
-        
+
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(OAuthError.TokenResponse.INVALID_CLIENT,
                 node.at("/error").asText());
         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 46671ff..956e3ed 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
@@ -48,6 +48,109 @@
                 .entity(form).post(ClientResponse.class);
     }
 
+    private ClientResponse testRequestTokenPublicClient (
+            MultivaluedMap<String, String> form)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        return resource().path("oauth2").path("token")
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+    }
+
+    @Test
+    public void testRequestTokenPasswordGrantConfidential ()
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", "password");
+
+        ClientResponse response = testRequestTokenConfidentialClient(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 testRequestTokenConfidentialMissingSecret ()
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", "password");
+        form.add("client_id", "fCBbQkAyYzI4NzUxMg==");
+
+        ClientResponse response = testRequestTokenPublicClient(form);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuthError.TokenResponse.INVALID_CLIENT,
+                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());
+    }
+
+    @Test
+    public void testRequestTokenPasswordGrantMissingClientId ()
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", "password");
+
+        ClientResponse response = testRequestTokenPublicClient(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",
+                node.at("/error_description").asText());
+    }
+
+    @Test
+    public void testRequestTokenPasswordGrantNonNative ()
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", "password");
+        form.add("client_id", "8bIDtZnH6NvRkW2Fq==");
+
+        ClientResponse response = testRequestTokenPublicClient(form);
+        String entity = response.getEntity(String.class);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT,
+                node.at("/error").asText());
+        assertEquals("Password grant is not allowed for third party clients",
+                node.at("/error_description").asText());
+    }
+
     @Test
     public void testRequestTokenClientCredentialsGrant ()
             throws UniformInterfaceException, ClientHandlerException,
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
index 5da4c40..17b2b51 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
@@ -654,7 +654,7 @@
         JsonNode node = JsonUtils.readTree(entity);
 
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.MISSING_ARGUMENT,
+        assertEquals(StatusCodes.MISSING_PARAMETER,
                 node.at("/errors/0/0").asInt());
         assertEquals("groupId", node.at("/errors/0/1").asText());
         assertEquals("0", node.at("/errors/0/2").asText());
@@ -844,7 +844,7 @@
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
 
-        assertEquals(StatusCodes.MISSING_ARGUMENT,
+        assertEquals(StatusCodes.MISSING_PARAMETER,
                 node.at("/errors/0/0").asInt());
         assertEquals("groupId", node.at("/errors/0/1").asText());
         assertEquals("0", node.at("/errors/0/2").asText());
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
index ad2874a..42f36ae 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
@@ -689,7 +689,7 @@
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.MISSING_ARGUMENT,
+        assertEquals(StatusCodes.MISSING_PARAMETER,
                 node.at("/errors/0/0").asInt());
         assertEquals("vcId", node.at("/errors/0/1").asText());
     }
diff --git a/full/src/test/resources/kustvakt-test.conf b/full/src/test/resources/kustvakt-test.conf
index c99ee5e..b26384b 100644
--- a/full/src/test/resources/kustvakt-test.conf
+++ b/full/src/test/resources/kustvakt-test.conf
@@ -43,13 +43,20 @@
 
 ## 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
+oauth.native.client.host=korap.ids-mannheim.de
+
+# JWT
+security.jwt.issuer=korap.ids-mannheim.de
+
+## token expiration
 security.longTokenTTL = 1D
 security.tokenTTL = 9S
 security.shortTokenTTL = 5S
 
-security.jwt.issuer=korap.ids-mannheim.de
-
 ## specifies the user data field that is used to salt user passwords
 security.passcode.salt=salt