Added a new API: list plugins (e.g. for marketplace)

Change-Id: Iad8ea0f18b8219e6e4074cdcf8ead54732f3f041
diff --git a/full/Changes b/full/Changes
index ed83421..22123c1 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,5 +1,9 @@
 # version 0.68
 
+2022-05-25
+ - Added a new API: list plugins (e.g for marketplace)
+
+
 # version 0.67.1
 
 2022-05-12
@@ -9,6 +13,7 @@
  - Fixed null client_id
  - Updated ldap.config path in the kustvakt-test.conf
  
+
 # version 0.67
 
 2022-05-09
@@ -37,6 +42,7 @@
    description in the client redirect URI except for missing or 
    invalid client id or redirect URI.
 
+
 # version 0.65.2
 
 2022-03-03
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 989deb2..c528e86 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
@@ -23,6 +23,7 @@
 
 import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.dao.AdminDao;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
@@ -48,6 +49,8 @@
     private EntityManager entityManager;
     @Autowired
     private FullConfiguration config;
+    @Autowired
+    private AdminDao adminDao;
 
     public void registerClient (String id, String secretHashcode, String name,
             OAuth2ClientType type, String url, String redirectURI,
@@ -194,4 +197,25 @@
         return q.getResultList();
     }
 
+    public List<OAuth2Client> retrievePlugins (boolean isPermittedOnly)
+            throws KustvaktException {
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<OAuth2Client> query =
+                builder.createQuery(OAuth2Client.class);
+
+        Root<OAuth2Client> client = query.from(OAuth2Client.class);
+        Predicate restrictions =
+                builder.isNotNull(client.get(OAuth2Client_.SOURCE));
+        if (isPermittedOnly) {
+            restrictions = builder.and(restrictions,
+                    builder.isTrue(client.get(OAuth2Client_.IS_PERMITTED)));
+        }
+
+        query.select(client);
+        query.where(restrictions);
+        query.distinct(true);
+        TypedQuery<OAuth2Client> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
 }
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 f013023..45dbd6b 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
@@ -367,6 +367,15 @@
         return createClientDtos(userClients);
     }
     
+       
+    public List<OAuth2UserClientDto> listPlugins (boolean isPermitted)
+            throws KustvaktException {
+
+        List<OAuth2Client> plugins = clientDao.retrievePlugins(isPermitted);
+        Collections.sort(plugins);
+        return createClientDtos(plugins);
+    }
+
     private List<OAuth2UserClientDto> createClientDtos (
             List<OAuth2Client> userClients) throws KustvaktException {
         List<OAuth2UserClientDto> dtoList = new ArrayList<>(userClients.size());
@@ -391,4 +400,6 @@
                     OAuth2Error.UNAUTHORIZED_CLIENT);
         }
     }
+
+    
 }
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 63499cf..ee2a46e 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
@@ -206,7 +206,7 @@
     @Path("/list")
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
-    public List<OAuth2UserClientDto> listUserAuthorizedClients (
+    public List<OAuth2UserClientDto> listUserClients (
             @Context SecurityContext context,
             @FormParam("super_client_id") String superClientId,
             @FormParam("super_client_secret") String superClientSecret,
@@ -231,4 +231,28 @@
             throw responseHandler.throwit(e);
         }
     }
+    
+    @POST
+    @Path("/plugins")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+    public List<OAuth2UserClientDto> listPlugins (
+            @Context SecurityContext context,
+            @FormParam("super_client_id") String superClientId,
+            @FormParam("super_client_secret") String superClientSecret,
+            @FormParam("permitted_only") boolean permittedOnly) {
+
+        TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
+
+        try {
+            scopeService.verifyScope(tokenContext,
+                    OAuth2Scope.LIST_USER_CLIENT);
+
+            clientService.verifySuperClient(superClientId, superClientSecret);
+            return clientService.listPlugins(permittedOnly);
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+    }
 }
diff --git a/full/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql b/full/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql
index 159c713..4e803d2 100644
--- a/full/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql
+++ b/full/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql
@@ -25,28 +25,28 @@
 
 INSERT INTO oauth2_client(id,name,secret,type,super,
   registered_by, description,url, registration_date, 
-  is_permitted) 
+  is_permitted,source) 
 VALUES ("52atrL0ajex_3_5imd9Mgw","confidential client 2",
   "$2a$08$vi1FbuN3p6GcI1tSxMAoeuIYL8Yw3j6A8wJthaN8ZboVnrQaTwLPq",
   "CONFIDENTIAL", 0,"system",
   "This is a test nonsuper confidential client.",
-  "http://example.client.de", CURRENT_TIMESTAMP, 1);
+  "http://example.client.de", CURRENT_TIMESTAMP, 1,'{"key":"value"}');
 
 INSERT INTO oauth2_client(id,name,secret,type,super,
   redirect_uri, registered_by, description, url, registration_date, 
-  is_permitted)
-VALUES ("8bIDtZnH6NvRkW2Fq","third party client",null,
-  "PUBLIC", 0,
+  is_permitted,source)
+VALUES ("8bIDtZnH6NvRkW2Fq","public client plugin with redirect uri",
+  null, "PUBLIC", 0,
   "https://third.party.client.com/redirect","system",
-  "This is a test public client.",
-  "http://third.party.client.com", CURRENT_TIMESTAMP,1);
+  "A public client that is a plugin with registered redirect URI",
+  "http://third.party.client.com", CURRENT_TIMESTAMP,1,'{"key":"value"}');
 
   
 INSERT INTO oauth2_client(id,name,secret,type,super,
   registered_by, description, url, registration_date, 
   is_permitted) 
 VALUES ("nW5qM63Rb2a7KdT9L","test public client",null,
-  "PUBLIC", 0, "https://korap.ids-mannheim.de/public/redirect",
+  "PUBLIC", 0, "without redirect uri",
   "system", "http://korap.ids-mannheim.de/public", 
   CURRENT_TIMESTAMP, 1);
   
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 f4b595d..528efba 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
@@ -81,7 +81,8 @@
         // public client
         JsonNode clientInfo = retrieveClientInfo(publicClientId, "system");
         assertEquals(publicClientId, clientInfo.at("/id").asText());
-        assertEquals("third party client", clientInfo.at("/name").asText());
+        assertEquals("public client plugin with redirect uri",
+                clientInfo.at("/name").asText());
         assertNotNull(clientInfo.at("/description"));
         assertNotNull(clientInfo.at("/url"));
         assertEquals("PUBLIC", clientInfo.at("/type").asText());
@@ -113,15 +114,9 @@
         String clientName = "OAuth2DoryClient";
         OAuth2ClientJson json = createOAuth2ClientJson(clientName,
                 OAuth2ClientType.PUBLIC, "Dory's client.");
-
         registerClient("dory", json);
 
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        form.add("super_client_id", superClientId);
-        form.add("super_client_secret", clientSecret);
-
-        JsonNode node = testListUserRegisteredClients("dory");
-        
+        JsonNode node = listUserRegisteredClients("dory");
         assertEquals(1, node.size());
         assertEquals(clientName, node.at("/0/client_name").asText());
         assertEquals(OAuth2ClientType.PUBLIC.name(),
@@ -151,40 +146,6 @@
     }
     
     @Test
-    public void testRegisterPlugin () throws UniformInterfaceException,
-            ClientHandlerException, KustvaktException {
-        JsonNode source = JsonUtils.readTree("{ \"plugin\" : \"source\"}");
-                
-        OAuth2ClientJson json = new OAuth2ClientJson();
-        json.setName("Plugin");
-        json.setType(OAuth2ClientType.CONFIDENTIAL);
-        json.setDescription("This is a plugin test client.");
-        json.setSource(source);
-        
-        ClientResponse response = registerClient(username, json);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
-        String clientId = node.at("/client_id").asText();
-        String clientSecret = node.at("/client_secret").asText();
-        assertNotNull(clientId);
-        assertNotNull(clientSecret);
-        
-        JsonNode clientInfo = retrieveClientInfo(clientId, username);
-        assertEquals(clientId, clientInfo.at("/id").asText());
-        assertEquals("Plugin", clientInfo.at("/name").asText());
-        assertEquals(OAuth2ClientType.CONFIDENTIAL.name(),
-                clientInfo.at("/type").asText());
-        assertEquals(username, clientInfo.at("/registered_by").asText());
-        assertNotNull(clientInfo.at("/registration_date"));
-        
-        assertFalse(clientInfo.at("/permitted").asBoolean());
-        assertNotNull(clientInfo.at("/source"));
-        
-        testListUserRegisteredClients(username);
-        deregisterConfidentialClient(username, clientId);
-    }
-
-    @Test
     public void testRegisterClientNameTooShort ()
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
@@ -538,9 +499,7 @@
 
     private void requestAuthorizedClientList (String userAuthHeader)
             throws KustvaktException {
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        form.add("super_client_id", superClientId);
-        form.add("super_client_secret", clientSecret);
+        MultivaluedMap<String, String> form = getSuperClientForm();
         form.add("authorized_only", "true");
 
         ClientResponse response = resource().path(API_VERSION).path("oauth2")
@@ -672,30 +631,6 @@
                 clientSecret, refreshToken);
     }
     
-    private JsonNode testListUserRegisteredClients (String username)
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        form.add("super_client_id", superClientId);
-        form.add("super_client_secret", clientSecret);
-
-        ClientResponse response = resource().path(API_VERSION).path("oauth2")
-                .path("client").path("list")
-                .header(Attributes.AUTHORIZATION,
-                        HttpAuthorizationHandler
-                                .createBasicAuthorizationHeaderValue(username,
-                                        "password"))
-                .header(HttpHeaders.CONTENT_TYPE,
-                        ContentType.APPLICATION_FORM_URLENCODED)
-                .entity(form).post(ClientResponse.class);
-
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-
-        String entity = response.getEntity(String.class);
-//        System.out.println(entity);
-        return JsonUtils.readTree(entity);
-    }
-
     private void testRevokeAllTokenViaSuperClient (String clientId,
             String userAuthHeader, String accessToken)
             throws KustvaktException {
@@ -704,10 +639,8 @@
         JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
         assertTrue(node.at("/matches").size() > 0);
 
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        MultivaluedMap<String, String> form = getSuperClientForm();
         form.add("client_id", clientId);
-        form.add("super_client_id", superClientId);
-        form.add("super_client_secret", clientSecret);
 
         response = resource().path(API_VERSION).path("oauth2").path("revoke")
                 .path("super").path("all")
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
new file mode 100644
index 0000000..f5efc97
--- /dev/null
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
@@ -0,0 +1,217 @@
+package de.ids_mannheim.korap.web.controller;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.http.entity.ContentType;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+import com.sun.jersey.api.client.ClientHandlerException;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.ClientResponse.Status;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.core.util.MultivaluedMapImpl;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
+
+public class OAuth2PluginTest extends OAuth2TestBase {
+
+    private String username = "plugin-user";
+
+    @Test
+    public void testRegisterPlugin () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        JsonNode source = JsonUtils.readTree("{ \"plugin\" : \"source\"}");
+
+        String clientName = "Plugin";
+        OAuth2ClientJson json = new OAuth2ClientJson();
+        json.setName(clientName);
+        json.setType(OAuth2ClientType.CONFIDENTIAL);
+        json.setDescription("This is a plugin test client.");
+        json.setSource(source);
+
+        ClientResponse response = registerClient(username, json);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
+        String clientId = node.at("/client_id").asText();
+        String clientSecret = node.at("/client_secret").asText();
+        assertNotNull(clientId);
+        assertNotNull(clientSecret);
+
+        testRetrievePluginInfo(clientId);
+
+        node = listPlugins(false);
+        assertEquals(3, node.size());
+        node = listPlugins(true); // permitted only
+        assertEquals(2, node.size());
+
+        testListUserRegisteredPlugins(username, clientId, clientName);
+        deregisterConfidentialClient(username, clientId);
+    }
+
+    private void testRetrievePluginInfo (String clientId)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        JsonNode clientInfo = retrieveClientInfo(clientId, username);
+        assertEquals(clientId, clientInfo.at("/id").asText());
+        assertEquals("Plugin", clientInfo.at("/name").asText());
+        assertEquals(OAuth2ClientType.CONFIDENTIAL.name(),
+                clientInfo.at("/type").asText());
+        assertEquals(username, clientInfo.at("/registered_by").asText());
+        assertNotNull(clientInfo.at("/registration_date"));
+
+        assertFalse(clientInfo.at("/permitted").asBoolean());
+        assertNotNull(clientInfo.at("/source"));
+    }
+
+    private void testListUserRegisteredPlugins (String username,
+            String clientId, String clientName)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+
+        JsonNode node = listUserRegisteredClients(username);
+        assertEquals(1, node.size());
+        assertEquals(clientId, node.at("/0/client_id").asText());
+        assertEquals(clientName, node.at("/0/client_name").asText());
+        assertEquals(OAuth2ClientType.CONFIDENTIAL.name(),
+                node.at("/0/client_type").asText());
+        assertFalse(node.at("/0/permitted").asBoolean());
+        assertFalse(node.at("/0/registration_date").isMissingNode());
+        assertFalse(node.at("/0/source").isMissingNode());
+    }
+
+    @Test
+    public void testListPluginsUnauthorizedPublic ()
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("super_client_id", publicClientId);
+        testListPluginsClientUnauthorized(form);
+    }
+
+    @Test
+    public void testListPluginsUnauthorizedConfidential ()
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("super_client_id", confidentialClientId2);
+        form.add("super_client_secret", clientSecret);
+        testListPluginsClientUnauthorized(form);
+    }
+
+    @Test
+    public void testListPluginsMissingClientSecret ()
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("super_client_id", confidentialClientId);
+
+        ClientResponse response = resource().path(API_VERSION).path("oauth2")
+                .path("client").path("plugins")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+        assertFalse(node.at("/error_description").isMissingNode());
+    }
+
+    private void testListPluginsClientUnauthorized (
+            MultivaluedMap<String, String> form)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        ClientResponse response = resource().path(API_VERSION).path("oauth2")
+                .path("client").path("plugins")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+
+        assertEquals(OAuth2Error.UNAUTHORIZED_CLIENT,
+                node.at("/error").asText());
+        assertFalse(node.at("/error_description").isMissingNode());
+    }
+
+    @Test
+    public void testListPluginsUserUnauthorized ()
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+
+        ClientResponse response = resource().path(API_VERSION).path("oauth2")
+                .path("client").path("plugins")
+                .header(Attributes.AUTHORIZATION, "Bearer blahblah")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(getSuperClientForm()).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.INVALID_ACCESS_TOKEN,
+                node.at("/errors/0/0").asInt());
+    }
+
+    @Test
+    public void testListAllPlugins () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        JsonNode node = listPlugins(false);
+        assertEquals(2, node.size());
+        assertFalse(node.at("/0/client_id").isMissingNode());
+        assertFalse(node.at("/0/client_name").isMissingNode());
+        assertFalse(node.at("/0/client_description").isMissingNode());
+        assertFalse(node.at("/0/client_type").isMissingNode());
+        assertFalse(node.at("/0/permitted").isMissingNode());
+        assertFalse(node.at("/0/registration_date").isMissingNode());
+        assertFalse(node.at("/0/source").isMissingNode());
+    }
+
+    private JsonNode listPlugins (boolean permitted_only)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+
+        MultivaluedMap<String, String> form = getSuperClientForm();
+        if (permitted_only) {
+            form.add("permitted_only", Boolean.toString(permitted_only));
+        }
+        ClientResponse response = resource().path(API_VERSION).path("oauth2")
+                .path("client").path("plugins")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        return JsonUtils.readTree(entity);
+    }
+
+}
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
index 9d56e5a..43cb07e 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
@@ -45,7 +45,7 @@
 
     @Autowired
     protected RefreshTokenDao refreshTokenDao;
-    
+
     protected String publicClientId = "8bIDtZnH6NvRkW2Fq";
     // without registered redirect URI
     protected String publicClientId2 = "nW5qM63Rb2a7KdT9L";
@@ -57,14 +57,21 @@
 
     public static String ACCESS_TOKEN_TYPE = "access_token";
     public static String REFRESH_TOKEN_TYPE = "refresh_token";
-    
+
     protected String clientURL = "http://example.client.com";
     protected String clientRedirectUri = "https://example.client.com/redirect";
-    
+
     protected MultivaluedMap<String, String> getQueryParamsFromURI (URI uri) {
         return UriComponent.decodeQuery(uri, true);
     };
 
+    protected MultivaluedMap<String, String> getSuperClientForm () {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("super_client_id", superClientId);
+        form.add("super_client_secret", clientSecret);
+        return form;
+    }
+
     protected ClientResponse requestAuthorizationCode (String responseType,
             String clientId, String redirectUri, String scope, String state,
             String authHeader) throws KustvaktException {
@@ -92,11 +99,11 @@
                 .get(ClientResponse.class);
     }
 
-    protected String requestAuthorizationCode (String clientId, 
+    protected String requestAuthorizationCode (String clientId,
             String authHeader) throws KustvaktException {
 
-        ClientResponse response = requestAuthorizationCode("code", clientId,
-                "", "", "", authHeader);
+        ClientResponse response = requestAuthorizationCode("code", clientId, "",
+                "", "", authHeader);
         assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(),
                 response.getStatus());
         URI redirectUri = response.getLocation();
@@ -105,9 +112,9 @@
                 .fromUri(redirectUri).build().getQueryParams();
         return params.getFirst("code");
     }
-    
-    protected String requestAuthorizationCode (String clientId, String redirect_uri,
-            String authHeader) throws KustvaktException {
+
+    protected String requestAuthorizationCode (String clientId,
+            String redirect_uri, String authHeader) throws KustvaktException {
         ClientResponse response = requestAuthorizationCode("code", clientId,
                 redirect_uri, "", "", authHeader);
         assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(),
@@ -118,7 +125,7 @@
                 .fromUri(redirectUri).build().getQueryParams();
         return params.getFirst("code");
     }
-    
+
     protected ClientResponse requestToken (MultivaluedMap<String, String> form)
             throws KustvaktException {
         return resource().path(API_VERSION).path("oauth2").path("token")
@@ -144,7 +151,7 @@
                         ContentType.APPLICATION_FORM_URLENCODED)
                 .entity(form).post(ClientResponse.class);
     }
-    
+
     protected ClientResponse requestTokenWithAuthorizationCodeAndForm (
             String clientId, String clientSecret, String code,
             String redirectUri) throws KustvaktException {
@@ -154,7 +161,7 @@
         form.add("client_id", clientId);
         form.add("client_secret", clientSecret);
         form.add("code", code);
-        if (redirectUri!=null){
+        if (redirectUri != null) {
             form.add("redirect_uri", redirectUri);
         }
 
@@ -165,8 +172,9 @@
     }
 
     // client credentials in authorization header
-    protected JsonNode requestTokenWithAuthorizationCodeAndHeader (String clientId,
-            String code, String authHeader) throws KustvaktException {
+    protected JsonNode requestTokenWithAuthorizationCodeAndHeader (
+            String clientId, String code, String authHeader)
+            throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", "authorization_code");
         form.add("client_id", clientId);
@@ -200,7 +208,7 @@
 
         return requestToken(form);
     }
-    
+
     protected void testRequestTokenWithRevokedRefreshToken (String clientId,
             String clientSecret, String refreshToken) throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
@@ -225,7 +233,7 @@
         assertEquals("Refresh token has been revoked",
                 node.at("/error_description").asText());
     }
-    
+
     protected ClientResponse registerClient (String username,
             OAuth2ClientJson json) throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
@@ -239,7 +247,7 @@
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .entity(json).post(ClientResponse.class);
     }
-    
+
     protected ClientResponse registerConfidentialClient (String username)
             throws KustvaktException {
 
@@ -252,7 +260,7 @@
 
         return registerClient(username, json);
     }
-    
+
     protected void testConfidentialClientInfo (String clientId, String username)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
@@ -263,17 +271,18 @@
                 clientInfo.at("/type").asText());
         assertEquals(username, clientInfo.at("/registered_by").asText());
         assertEquals(clientURL, clientInfo.at("/url").asText());
-        assertEquals(clientRedirectUri, clientInfo.at("/redirect_uri").asText());
+        assertEquals(clientRedirectUri,
+                clientInfo.at("/redirect_uri").asText());
         assertNotNull(clientInfo.at("/description"));
         assertNotNull(clientInfo.at("/registration_date"));
         assertTrue(clientInfo.at("/permitted").asBoolean());
         assertTrue(clientInfo.at("/source").isMissingNode());
-        
+
     }
-    
-    protected void deregisterConfidentialClient (String username, String clientId)
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+
+    protected void deregisterConfidentialClient (String username,
+            String clientId) throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
 
         ClientResponse response = resource().path(API_VERSION).path("oauth2")
                 .path("client").path("deregister").path(clientId)
@@ -306,19 +315,20 @@
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .get(ClientResponse.class);
     }
-    
-    protected void testRevokeTokenViaSuperClient (String token, String userAuthHeader) {
+
+    protected void testRevokeTokenViaSuperClient (String token,
+            String userAuthHeader) {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("token", token);
         form.add("super_client_id", superClientId);
         form.add("super_client_secret", clientSecret);
 
-        ClientResponse response = resource().path(API_VERSION)
-                .path("oauth2").path("revoke").path("super")
+        ClientResponse response = resource().path(API_VERSION).path("oauth2")
+                .path("revoke").path("super")
                 .header(HttpHeaders.CONTENT_TYPE,
                         ContentType.APPLICATION_FORM_URLENCODED)
-                .header(Attributes.AUTHORIZATION, userAuthHeader)
-                .entity(form).post(ClientResponse.class);
+                .header(Attributes.AUTHORIZATION, userAuthHeader).entity(form)
+                .post(ClientResponse.class);
 
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         assertEquals("SUCCESS", response.getEntity(String.class));
@@ -343,4 +353,21 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         assertEquals("SUCCESS", response.getEntity(String.class));
     }
+
+    protected JsonNode listUserRegisteredClients (String username)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        ClientResponse response = resource().path(API_VERSION).path("oauth2")
+                .path("client").path("list")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pwd"))
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(getSuperClientForm()).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        String entity = response.getEntity(String.class);
+        return JsonUtils.readTree(entity);
+    }
 }