diff --git a/.gitignore b/.gitignore
index 410aba1b0d99a0e9e9997fe1f279461e8d09eb4f..793bc232224a8f08354552b90a87b14396b51fd2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,12 +54,10 @@ nbactions.xml
 ### VS Code ###
 .vscode/
 
-/gms-ui/target/
 /gms/nbactions-release-profile.xml
 
-/gms-client/gms-client-lib/target/
-/gms-client/gms-cli/target/
 /gms/node/
+**/target/*
 
 nb-configuration.xml
 dependency-reduced-pom.xml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eb81edda0645763bb29f8524ef811a54f0b4db3d
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,42 @@
+stages:
+  - build
+  - test
+  - deploy
+
+build_gms_client:
+  stage: build
+  tags:
+    - docker
+  script:
+    - cd gms-client/gms-client
+    - mvn clean package -DskipTests -DfinalName=gms-client
+  artifacts:
+    paths:
+      - gms-client/gms-client/target/gms-client.jar
+      - gms-client/gms-client/pom.xml
+    expire_in: 7 days
+  only:
+    - master
+
+test_gms_client:
+  stage: test
+  tags:
+    - docker
+  script:
+    - cd gms-client/gms-client
+    - mvn clean test
+  only:
+    - master
+
+deploy_gms_client:
+  stage: deploy
+  tags:
+    - docker
+  script:
+    - mvn deploy:deploy-file
+        -Dfile=gms-client/gms-client/target/gms-client.jar
+        -DrepositoryId=ia2.snapshots
+        -DpomFile=gms-client/gms-client/pom.xml
+        -Durl=${IA2_MVN_REPO_SNAPSHOTS}
+  only:
+    - master
diff --git a/gms-client/gms-cli/gms.properties b/gms-client/gms-cli/gms.properties
index facdd476b33bf19050e1f17165c4de0bd35f01d6..b7f4928fcf75092b831c7005d049ef5a19aac333 100644
--- a/gms-client/gms-cli/gms.properties
+++ b/gms-client/gms-cli/gms.properties
@@ -1,2 +1,4 @@
-base_url=http://localhost:8082/gms/ws/jwt
-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjM0ZmU4MDcwMDVhNTcxMTYifQ.eyJpc3MiOiJzc28uaWEyLmluYWYuaXQiLCJzdWIiOiIyMzg2IiwiaWF0IjoxNTg3NjU5NzYxLCJleHAiOjE1ODc3NDYxNjEsImF1ZCI6ImdtcyJ9.KcXRAciG3ApqlE8MFM8VYW9WAX3hEZb7Vk8jB9uJtWsOMU48ha_Ybb4k_f0nrD2jhOxwaNn2QMxWZuflwCf1N-KiCj5Ff9f8xKOrrXZrl-w1H3_dwtMlIS8t2b0-w0WwRJ7UIhrwVBzmCcWinD3qJhFPzyO2pi-A4aXV57RpJ68VXfALQXeHK0sslrf-RgAU3xWYOgjGTUoGB5BQYC9huA_bZ0eV1HFcancs9pDdoTusqZs8OkPFCJbo7-L5eibsuykqnLHztYdCcP2Vtvtwb0pww-ofWZblIHzoMI8i-ipnfLJETG8Dpc7FrhjCYLw3AEGZg4U1wYTeqG3HRbPXSQ
+gms_url=http://localhost:8082/gms/ws/jwt
+client_id=gms_cli
+client_secret=gms
+rap_url=http://localhost/rap-ia2
diff --git a/gms-client/gms-cli/pom.xml b/gms-client/gms-cli/pom.xml
index cfaad419eeaad50ae0942a437f3bffb821ce11ee..26e78d841a057a4814d77f1cde517c0a0c15f284 100644
--- a/gms-client/gms-cli/pom.xml
+++ b/gms-client/gms-cli/pom.xml
@@ -17,7 +17,7 @@
     <dependencies>
         <dependency>
             <groupId>${project.groupId}</groupId>
-            <artifactId>gms-client-lib</artifactId>
+            <artifactId>gms-client</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>
     </dependencies>
@@ -46,4 +46,10 @@
             </plugin>
         </plugins>
     </build>
+    <repositories>
+        <repository>
+            <id>ia2.snapshot</id>
+            <url>http://repo.ia2.inaf.it/maven/repository/snapshots</url>
+        </repository>
+    </repositories>
 </project>
diff --git a/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java b/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
index 4d4ff78503c87ba822095512ea700b1e59fb01ba..71b413bee175693c0df3ccc602667e6106a26f5b 100644
--- a/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
+++ b/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
@@ -1,8 +1,10 @@
 package it.inaf.ia2.gms.cli;
 
+import it.inaf.ia2.client.ClientException;
 import it.inaf.ia2.gms.client.GmsClient;
-import it.inaf.ia2.gms.client.GmsClientBuilder;
 import it.inaf.ia2.gms.client.model.Permission;
+import it.inaf.ia2.rap.client.RapClient;
+import it.inaf.ia2.rap.data.AccessTokenResponse;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -61,7 +63,11 @@ public class CLI {
                 default:
                     verifyConfigLoaded();
                     createClient();
-                    parseCommand();
+                    try {
+                        parseCommand();
+                    } catch (ClientException ex) {
+                        System.err.println(ex.getMessage());
+                    }
                     commandParsed = true;
                     break;
             }
@@ -96,17 +102,17 @@ public class CLI {
     }
 
     private void createClient() {
-        GmsClientBuilder clientBuilder = new GmsClientBuilder()
-                .setGmsBaseUrl(gmsBaseUrl);
+
+        client = new GmsClient(gmsBaseUrl);
 
         if (token != null) {
-            client = clientBuilder.build();
             client.setAccessToken(token);
         } else {
-            client = clientBuilder.setClientId(clientId)
-                    .setClientSecret(clientSecret)
-                    .setRapBaseUrl(rapBaseUrl)
-                    .build();
+            RapClient rapClient = new RapClient(rapBaseUrl)
+                    .setClientId(clientId)
+                    .setClientSecret(clientSecret);
+            AccessTokenResponse accessTokenResponse = rapClient.getAccessTokenFromClientCredentials();
+            client.setAccessToken(accessTokenResponse.getAccessToken());
         }
     }
 
@@ -118,7 +124,7 @@ public class CLI {
         }
 
         Properties properties = new Properties();
-        try (InputStream in = new FileInputStream(config)) {
+        try ( InputStream in = new FileInputStream(config)) {
             properties.load(in);
         } catch (IOException ex) {
             throw new UncheckedIOException(ex);
@@ -141,7 +147,7 @@ public class CLI {
             System.exit(1);
         }
 
-        try (InputStream in = new FileInputStream(tokenFile)) {
+        try ( InputStream in = new FileInputStream(tokenFile)) {
             java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\A");
             token = s.next().trim();
         } catch (IOException ex) {
diff --git a/gms-client/gms-cli/src/main/resources/application.properties b/gms-client/gms-cli/src/main/resources/application.properties
deleted file mode 100644
index 2a867fa5f89b8104fe8d51524d699a0d5c9b064f..0000000000000000000000000000000000000000
--- a/gms-client/gms-cli/src/main/resources/application.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-spring.main.banner-mode=off
-logging.level.root=OFF
\ No newline at end of file
diff --git a/gms-client/gms-client-lib/pom.xml b/gms-client/gms-client-lib/pom.xml
deleted file mode 100644
index 571ed7f18cf704af8107649566019753a04996f9..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/pom.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <groupId>it.inaf.ia2</groupId>
-    <artifactId>gms-client-lib</artifactId>
-    <version>1.0-SNAPSHOT</version>
-    <packaging>jar</packaging>
-    <properties>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <maven.compiler.source>12</maven.compiler.source>
-        <maven.compiler.target>12</maven.compiler.target>
-    </properties>
-    <dependencies>
-        <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-to-slf4j</artifactId>
-            <version>2.12.1</version>
-        </dependency>
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>4.12</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <version>2.23.4</version>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-</project>
\ No newline at end of file
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
deleted file mode 100644
index 8dcf0419ed854c95e77acb73fc553302bf2b24a6..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package it.inaf.ia2.gms.client;
-
-import it.inaf.ia2.gms.client.call.AddInvitedRegistrationCall;
-import it.inaf.ia2.gms.client.call.HttpClientWrapper;
-import it.inaf.ia2.gms.client.call.AddMemberCall;
-import it.inaf.ia2.gms.client.call.AddPermissionCall;
-import it.inaf.ia2.gms.client.call.CreateGroupCall;
-import it.inaf.ia2.gms.client.call.DeleteGroupCall;
-import it.inaf.ia2.gms.client.call.GetGroupPermissionsCall;
-import it.inaf.ia2.gms.client.call.GetMemberEmailAddresses;
-import it.inaf.ia2.gms.client.call.GetUserGroupsCall;
-import it.inaf.ia2.gms.client.call.GetUserPermissionsCall;
-import it.inaf.ia2.gms.client.call.ListGroupsCall;
-import it.inaf.ia2.gms.client.call.RemoveMemberCall;
-import it.inaf.ia2.gms.client.call.RemovePermissionCall;
-import it.inaf.ia2.gms.client.call.SetPermissionCall;
-import it.inaf.ia2.gms.client.model.GroupPermission;
-import it.inaf.ia2.gms.client.model.Permission;
-import it.inaf.ia2.gms.client.model.UserPermission;
-import java.util.List;
-import java.util.Map;
-
-public class GmsClient {
-
-    private final HttpClientWrapper httpClientWrapper;
-
-    GmsClient(HttpClientWrapper httpClientWrapper) {
-        this.httpClientWrapper = httpClientWrapper;
-    }
-
-    public GmsClient setAccessToken(String accessToken) {
-        httpClientWrapper.setAccessToken(accessToken);
-        return this;
-    }
-
-    public List<String> getMyGroups(String prefix) {
-        return new GetUserGroupsCall(httpClientWrapper).getUserGroups(prefix);
-    }
-
-    public List<String> listGroups(String prefix) {
-        return new ListGroupsCall(httpClientWrapper).listGroups(prefix);
-    }
-
-    public List<String> getUserGroups(String userId, String prefix) {
-        return new GetUserGroupsCall(httpClientWrapper).getUserGroups(userId, prefix);
-    }
-
-    public void createGroup(String completeGroupName, boolean leaf) {
-        new CreateGroupCall(httpClientWrapper).createGroup(completeGroupName, leaf);
-    }
-
-    public void deleteGroup(String completeGroupName) {
-        new DeleteGroupCall(httpClientWrapper).deleteGroup(completeGroupName);
-    }
-
-    public void addMember(String completeGroupName, String userId) {
-        new AddMemberCall(httpClientWrapper).addMember(completeGroupName, userId);
-    }
-
-    public void removeMember(String completeGroupName, String userId) {
-        new RemoveMemberCall(httpClientWrapper).removeMember(completeGroupName, userId);
-    }
-
-    public void addPermission(String completeGroupName, String userId, Permission permission) {
-        new AddPermissionCall(httpClientWrapper).addPermission(completeGroupName, userId, permission);
-    }
-
-    public void setPermission(String completeGroupName, String userId, Permission permission) {
-        new SetPermissionCall(httpClientWrapper).setPermission(completeGroupName, userId, permission);
-    }
-
-    public void removePermission(String completeGroupName, String userId) {
-        new RemovePermissionCall(httpClientWrapper).removePermission(completeGroupName, userId);
-    }
-
-    public List<UserPermission> getUserPermissions(String userId) {
-        return new GetUserPermissionsCall(httpClientWrapper).getUserPermissions(userId);
-    }
-
-    public List<GroupPermission> getGroupPermissions(String groupId) {
-        return new GetGroupPermissionsCall(httpClientWrapper).getGroupPermissions(groupId);
-    }
-
-    public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) {
-        new AddInvitedRegistrationCall(httpClientWrapper).addInvitedRegistration(token, email, groupsPermissions);
-    }
-
-    public List<String> getMemberEmailAddresses(String groupId, Permission permission) {
-        return new GetMemberEmailAddresses(httpClientWrapper).getMemberEmailAddresses(groupId, permission);
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java
deleted file mode 100644
index 002081d041f2f0912b9678c9a83144537463ab80..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package it.inaf.ia2.gms.client;
-
-import it.inaf.ia2.gms.client.call.HttpClientWrapper;
-
-public class GmsClientBuilder {
-
-    private String gmsBaseUrl;
-    private String rapBaseUrl;
-    private String clientId;
-    private String clientSecret;
-
-    public GmsClientBuilder setGmsBaseUrl(String gmsBaseUrl) {
-        this.gmsBaseUrl = gmsBaseUrl;
-        return this;
-    }
-
-    public GmsClientBuilder setRapBaseUrl(String rapBaseUrl) {
-        this.rapBaseUrl = rapBaseUrl;
-        return this;
-    }
-
-    public GmsClientBuilder setClientId(String clientId) {
-        this.clientId = clientId;
-        return this;
-    }
-
-    public GmsClientBuilder setClientSecret(String clientSecret) {
-        this.clientSecret = clientSecret;
-        return this;
-    }
-
-    public GmsClient build() {
-        HttpClientWrapper clientWrapper = new HttpClientWrapper(gmsBaseUrl);
-        if (rapBaseUrl != null && clientId != null && clientSecret != null) {
-            clientWrapper.setRapBaseUrl(rapBaseUrl)
-                    .setClientId(clientId).setClientSecret(clientSecret);
-        }
-        return new GmsClient(clientWrapper);
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
deleted file mode 100644
index e76242d179518ab4c98e12864012875cf785e728..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublishers;
-import java.net.http.HttpResponse;
-
-public class AddMemberCall extends BaseGmsCall {
-
-    public AddMemberCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean addMember(String completeGroupName, String userId) {
-
-        String endpoint = "membership";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(BodyPublishers.ofString("user_id=" + userId))
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to add member to group");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
deleted file mode 100644
index 42acad909f324d1c0ea255fca62c433c2b5c161a..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import it.inaf.ia2.gms.client.model.Permission;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublisher;
-import java.net.http.HttpRequest.BodyPublishers;
-import java.net.http.HttpResponse;
-
-public class AddPermissionCall extends BaseGmsCall {
-
-    public AddPermissionCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean addPermission(String completeGroupName, String userId, Permission permission) {
-
-        String endpoint = "permission";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-
-        BodyPublisher requestBody = BodyPublishers.ofString(
-                "user_id=" + userId + "&permission=" + permission);
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(requestBody)
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to add permission");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java
deleted file mode 100644
index f5304b7d1623eed3af43d2914b29fd11cee9b7d8..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.io.InputStream;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.Builder;
-import java.net.http.HttpResponse;
-import java.util.Scanner;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public abstract class BaseGmsCall {
-
-    private static final Logger LOGGER = LoggerFactory.getLogger(BaseGmsCall.class);
-
-    protected final HttpClientWrapper clientWrapper;
-
-    public BaseGmsCall(HttpClientWrapper clientWrapper) {
-        this.clientWrapper = clientWrapper;
-    }
-
-    protected HttpClient getClient() {
-        return clientWrapper.getClient();
-    }
-
-    protected Builder newHttpRequest(String endpoint) {
-        return clientWrapper.newHttpRequest(endpoint);
-    }
-
-    protected static void logServerError(HttpRequest request, HttpResponse<String> response) {
-        LOGGER.error("Error while reading " + request.uri()
-                + "\nServer response status code is " + response.statusCode()
-                + "\nServer response text is " + response.body());
-    }
-
-    protected static void logServerErrorInputStream(HttpRequest request, HttpResponse<InputStream> response) {
-        Scanner s = new Scanner(response.body()).useDelimiter("\\A");
-        String responseBody = s.hasNext() ? s.next() : "";
-        String error = "Error while reading " + request.uri()
-                + "\nServer response status code is " + response.statusCode()
-                + "\nServer response text is " + responseBody;
-        LOGGER.error(error);
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
deleted file mode 100644
index 1cbb2e1f0a96f9d19524f3b89be7d7ba78701aa5..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublishers;
-import java.net.http.HttpResponse;
-
-public class CreateGroupCall extends BaseGmsCall {
-
-    public CreateGroupCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean createGroup(String completeGroupName, boolean leaf) {
-
-        HttpRequest groupsRequest = newHttpRequest(completeGroupName)
-                .header("Accept", "text/plain")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(BodyPublishers.ofString("leaf=" + leaf))
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 201) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to create group");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
deleted file mode 100644
index 56b7b9893ccbe30ba3612a8d5bdc9d3a99601075..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class DeleteGroupCall extends BaseGmsCall {
-
-    public DeleteGroupCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean deleteGroup(String completeGroupName) {
-
-        HttpRequest groupsRequest = newHttpRequest(completeGroupName)
-                .header("Accept", "text/plain")
-                .DELETE()
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 204) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to delete group");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java
deleted file mode 100644
index 7d90d7276ee959ad5de2e65d17d7ebbc2716256a..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.Builder;
-import java.net.http.HttpResponse;
-import java.util.Base64;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class HttpClientWrapper {
-
-    private final String baseGmsUri;
-    private final HttpClient client;
-
-    private String rapBaseUrl;
-    private String clientId;
-    private String clientSecret;
-
-    private String accessToken;
-
-    public HttpClientWrapper(String baseGmsUri) {
-        String uri = baseGmsUri;
-        if (!uri.endsWith("/")) {
-            uri += "/";
-        }
-        this.baseGmsUri = uri;
-
-        this.client = HttpClient.newBuilder()
-                .followRedirects(HttpClient.Redirect.ALWAYS)
-                .build();
-    }
-
-    public HttpClientWrapper setAccessToken(String accessToken) {
-        this.accessToken = accessToken;
-        return this;
-    }
-
-    public HttpClientWrapper setRapBaseUrl(String rapBaseUrl) {
-        if (!rapBaseUrl.endsWith("/")) {
-            rapBaseUrl += "/";
-        }
-        this.rapBaseUrl = rapBaseUrl;
-        return this;
-    }
-
-    public HttpClientWrapper setClientId(String clientId) {
-        this.clientId = clientId;
-        return this;
-    }
-
-    public HttpClientWrapper setClientSecret(String clientSecret) {
-        this.clientSecret = clientSecret;
-        return this;
-    }
-
-    Builder newHttpRequest(String endpoint) {
-        if (accessToken == null) {
-            accessToken = getAccessTokenFromClientCredentials();
-        }
-        return HttpRequest.newBuilder()
-                .uri(URI.create(baseGmsUri + endpoint))
-                .header("Authorization", "Bearer " + accessToken);
-    }
-
-    private String getAccessTokenFromClientCredentials() {
-        if (rapBaseUrl == null || clientId == null || clientSecret == null) {
-            throw new IllegalStateException("Access token is null and client credentials are not configured");
-        }
-
-        String basicAuthHeader = clientId + ":" + clientSecret;
-
-        HttpRequest tokenRequest = HttpRequest.newBuilder()
-                .uri(URI.create(rapBaseUrl + "auth/oauth2/token"))
-                .header("Authorization", "Basic " + Base64.getEncoder().encodeToString(basicAuthHeader.getBytes()))
-                .header("Accept", "application/json")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(HttpRequest.BodyPublishers.ofString("grant_type=client_credentials"))
-                .build();
-
-        return client.sendAsync(tokenRequest, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return getAccessTokenFromResponse(response.body());
-                    }
-                    BaseGmsCall.logServerError(tokenRequest, response);
-
-                    throw new IllegalStateException("Unable to retrieve access token");
-                }).join();
-    }
-
-    protected String getAccessTokenFromResponse(String body) {
-        Pattern codePattern = Pattern.compile(".*\"access_token\":\\s*\"([^\"]+).*");
-        Matcher matcher = codePattern.matcher(body);
-        if (matcher.find()) {
-            return matcher.group(1);
-        }
-        throw new IllegalStateException("Unable to extract access token from body");
-    }
-
-    HttpClient getClient() {
-        return client;
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
deleted file mode 100644
index cc5b01a3ed2e8ec9c4f70c6cd32f29383c8dc09e..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class RemoveMemberCall extends BaseGmsCall {
-
-    public RemoveMemberCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean removeMember(String completeGroupName, String userId) {
-
-        String endpoint = "membership";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-        endpoint += "?user_id=" + userId;
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .DELETE()
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 204) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to remove member from group");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
deleted file mode 100644
index 7f17e76e96cc34875a6b0447e18afa181404f783..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class RemovePermissionCall extends BaseGmsCall {
-
-    public RemovePermissionCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean removePermission(String completeGroupName, String userId) {
-
-        String endpoint = "permission";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-        endpoint += "?user_id=" + userId;
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .DELETE()
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 204) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to remove permission");
-                }).join();
-    }
-
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
deleted file mode 100644
index c7eee108a06374c035e701f2018dee78e8d45324..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import static it.inaf.ia2.gms.client.call.BaseGmsCall.logServerErrorInputStream;
-import it.inaf.ia2.gms.client.model.Permission;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class SetPermissionCall extends BaseGmsCall {
-
-    public SetPermissionCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean setPermission(String completeGroupName, String userId, Permission permission) {
-
-        String endpoint = "permission";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-
-        HttpRequest.BodyPublisher requestBody = HttpRequest.BodyPublishers.ofString(
-                "user_id=" + userId + "&permission=" + permission);
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .PUT(requestBody)
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to set permission");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
deleted file mode 100644
index ce84caa0998ffded7ad83ec2cdf9f1fc1a4344b4..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
+++ /dev/null
@@ -1,255 +0,0 @@
-package it.inaf.ia2.gms.client;
-
-import it.inaf.ia2.gms.client.call.HttpClientWrapper;
-import it.inaf.ia2.gms.client.call.MockedHttpClientWrapper;
-import it.inaf.ia2.gms.client.model.Permission;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.net.http.HttpResponse.BodySubscriber;
-import java.net.http.HttpResponse.BodySubscribers;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Flow;
-import static org.junit.Assert.assertEquals;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.AdditionalMatchers;
-import org.mockito.ArgumentMatcher;
-import org.mockito.ArgumentMatchers;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import org.mockito.junit.MockitoJUnitRunner;
-
-@RunWith(MockitoJUnitRunner.class)
-public class GmsClientTest {
-
-    private static final String BASE_URL = "http://base-url";
-
-    private HttpClient httpClient;
-    private GmsClient client;
-
-    @Before
-    public void setUp() {
-
-        httpClient = mock(HttpClient.class);
-
-        HttpClientWrapper clientWrapper = new MockedHttpClientWrapper(BASE_URL, httpClient);
-        clientWrapper.setAccessToken("foo");
-
-        client = new GmsClient(clientWrapper);
-    }
-
-    @Test
-    public void testGetMyGroups() {
-
-        String body = "LBT.INAF\n"
-                + "LBT.AZ";
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200, body));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        List<String> groups = client.getMyGroups("LBT.");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "search"), any());
-
-        assertEquals(2, groups.size());
-        assertEquals("INAF", groups.get(0));
-        assertEquals("AZ", groups.get(1));
-    }
-
-    @Test
-    public void testListGroups() {
-
-        String body = "INAF\n"
-                + "AZ";
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200, body));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        List<String> groups = client.listGroups("LBT.");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "list/LBT."), any());
-
-        assertEquals(2, groups.size());
-        assertEquals("INAF", groups.get(0));
-        assertEquals("AZ", groups.get(1));
-    }
-
-    @Test
-    public void testCreateGroup() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(201));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.createGroup("LBT.INAF", false);
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "LBT.INAF"), any());
-    }
-
-    @Test
-    public void testDeleteGroup() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.deleteGroup("LBT.INAF");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "LBT.INAF"), any());
-    }
-
-    @Test
-    public void testAddMember() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.addMember("LBT.INAF", "user");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "membership/LBT.INAF"), any());
-    }
-
-    @Test
-    public void testRemoveMember() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.removeMember("LBT.INAF", "user");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "membership/LBT.INAF?user_id=user"), any());
-    }
-
-    @Test
-    public void testAddPermission() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.addPermission("LBT.INAF", "user", Permission.ADMIN);
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "permission/LBT.INAF"), any());
-    }
-
-    @Test
-    public void testRemovePermission() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.removePermission("LBT.INAF", "user");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "permission/LBT.INAF?user_id=user"), any());
-    }
-
-    @Test
-    public void testInvitedRegistration() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(201));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        Map<String, Permission> permissionsMap = new HashMap<>();
-        permissionsMap.put("group1", Permission.MANAGE_MEMBERS);
-        permissionsMap.put("group2", Permission.MANAGE_MEMBERS);
-        client.addInvitedRegistration("bvjsgqu423", "email", permissionsMap);
-        // hash = AOyojiwaRR7BHPde6Tomg3+BMoQQggNM3wUHEarXuNQ=
-
-        verify(httpClient, times(1)).sendAsync(
-                AdditionalMatchers.and(
-                        endpointEq("POST", "invited-registration"),
-                        ArgumentMatchers.argThat(req -> {
-                            String reqbody = req.bodyPublisher().map(p -> {
-                                var bodySubscriber = BodySubscribers.ofString(StandardCharsets.UTF_8);
-                                var flowSubscriber = new StringSubscriber(bodySubscriber);
-                                p.subscribe(flowSubscriber);
-                                return bodySubscriber.getBody().toCompletableFuture().join();
-                            }).get();
-
-                            // If the resulting hash contains a + symbol it has to be encoded to %2B,
-                            // otherwise it will be interpreted as a space and wrong value will be
-                            // stored into the database
-                            String expectedBody = "token_hash=AOyojiwaRR7BHPde6Tomg3%2BBMoQQggNM3wUHEarXuNQ="
-                                    + "&email=email&groups=group2 MANAGE_MEMBERS\n"
-                                    + "group1 MANAGE_MEMBERS";
-
-                            return reqbody.equals(expectedBody);
-                        })), any());
-    }
-
-    /**
-     * Credit: https://stackoverflow.com/a/55816685/771431
-     */
-    static final class StringSubscriber implements Flow.Subscriber<ByteBuffer> {
-
-        final BodySubscriber<String> wrapped;
-
-        StringSubscriber(BodySubscriber<String> wrapped) {
-            this.wrapped = wrapped;
-        }
-
-        @Override
-        public void onSubscribe(Flow.Subscription subscription) {
-            wrapped.onSubscribe(subscription);
-        }
-
-        @Override
-        public void onNext(ByteBuffer item) {
-            wrapped.onNext(List.of(item));
-        }
-
-        @Override
-        public void onError(Throwable throwable) {
-            wrapped.onError(throwable);
-        }
-
-        @Override
-        public void onComplete() {
-            wrapped.onComplete();
-        }
-    }
-
-    private HttpResponse getMockedResponse(int statusCode, String body) {
-        HttpResponse response = getMockedResponse(statusCode);
-        InputStream in = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
-        when(response.body()).thenReturn(in);
-        return response;
-    }
-
-    private HttpResponse getMockedResponse(int statusCode) {
-        HttpResponse response = mock(HttpResponse.class);
-        when(response.statusCode()).thenReturn(statusCode);
-        return response;
-    }
-
-    private HttpRequest endpointEq(String expectedMethod, String expectedEndpoint) {
-        return ArgumentMatchers.argThat(endpointEqArgumentMatcher(expectedMethod, expectedEndpoint));
-    }
-
-    private ArgumentMatcher<HttpRequest> endpointEqArgumentMatcher(String expectedMethod, String expectedEndpoint) {
-
-        return new ArgumentMatcher<HttpRequest>() {
-
-            private final String expectedUri = BASE_URL + "/" + expectedEndpoint;
-
-            @Override
-            public boolean matches(HttpRequest request) {
-                return expectedMethod.equals(request.method()) && expectedUri.equals(request.uri().toString());
-            }
-
-            @Override
-            public String toString() {
-                return expectedMethod + " " + expectedUri;
-            }
-        };
-    }
-}
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java
deleted file mode 100644
index 4bc09244ebcbd82e1407ba97a64ef2a21a7e2a5c..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import static org.junit.Assert.assertEquals;
-import org.junit.Test;
-
-public class HttpClientWrapperTest {
-
-    @Test
-    public void testExtractAccessToken() {
-        String response = "{\"access_token\":\"TEST_TOKEN\",\"token_type\":\"Bearer\",\"expires_in\":3600}";
-
-        HttpClientWrapper clientWrapper = new HttpClientWrapper("http://localhost");
-        assertEquals("TEST_TOKEN", clientWrapper.getAccessTokenFromResponse(response));
-    }
-}
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java
deleted file mode 100644
index d287f9039f03d5c1c156ff2b4f91362c09a5dedc..0000000000000000000000000000000000000000
--- a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpClient;
-
-public class MockedHttpClientWrapper extends HttpClientWrapper {
-
-    private final HttpClient mockedClient;
-
-    public MockedHttpClientWrapper(String baseGmsUri, HttpClient mockedClient) {
-        super(baseGmsUri);
-        this.mockedClient = mockedClient;
-    }
-
-    @Override
-    HttpClient getClient() {
-        return mockedClient;
-    }
-}
diff --git a/gms-client/gms-client/pom.xml b/gms-client/gms-client/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0fb3618a910376617cf3bf90c7a6fda0c5355f76
--- /dev/null
+++ b/gms-client/gms-client/pom.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>it.inaf.ia2</groupId>
+    <artifactId>gms-client</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <maven.compiler.source>14</maven.compiler.source>
+        <maven.compiler.target>14</maven.compiler.target>
+        <junit-jupiter.version>5.6.0</junit-jupiter.version>
+        <mockito.version>3.5.13</mockito.version>
+        <finalName>${project.artifactId}-${project.version}</finalName>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>rap-client</artifactId>
+            <version>1.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>${junit-jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <version>${junit-jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit-jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-junit-jupiter</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <finalName>${finalName}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>0.8.6</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>report</id>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>report</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.22.2</version>
+            </plugin>
+        </plugins>
+    </build>
+    <repositories>
+        <repository>
+            <id>ia2.snapshot</id>
+            <url>http://repo.ia2.inaf.it/maven/repository/snapshots</url>
+        </repository>
+    </repositories>
+</project>
\ No newline at end of file
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/GmsClient.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
new file mode 100644
index 0000000000000000000000000000000000000000..304df22c334f1d71a1cdc0225e27e69db1f1cbdb
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
@@ -0,0 +1,160 @@
+package it.inaf.ia2.gms.client;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import it.inaf.ia2.client.BaseClient;
+import it.inaf.ia2.gms.client.call.AddInvitedRegistrationCall;
+import it.inaf.ia2.gms.client.call.AddMemberCall;
+import it.inaf.ia2.gms.client.call.AddPermissionCall;
+import it.inaf.ia2.gms.client.call.CreateGroupCall;
+import it.inaf.ia2.gms.client.call.DeleteGroupCall;
+import it.inaf.ia2.gms.client.call.GetGroupPermissionsCall;
+import it.inaf.ia2.gms.client.call.GetGroupStatusCall;
+import it.inaf.ia2.gms.client.call.GetMemberEmailAddresses;
+import it.inaf.ia2.gms.client.call.GetUserGroupsCall;
+import it.inaf.ia2.gms.client.call.GetUserPermissionsCall;
+import it.inaf.ia2.gms.client.call.ListGroupsCall;
+import it.inaf.ia2.gms.client.call.RemoveMemberCall;
+import it.inaf.ia2.gms.client.call.RemovePermissionCall;
+import it.inaf.ia2.gms.client.call.SetPermissionCall;
+import it.inaf.ia2.gms.client.model.GroupPermission;
+import it.inaf.ia2.gms.client.model.Permission;
+import it.inaf.ia2.gms.client.model.UserPermission;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+public class GmsClient extends BaseClient {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private String accessToken;
+
+    public GmsClient(String gmsBaseUri) {
+        super(gmsBaseUri);
+    }
+
+    public GmsClient setAccessToken(String accessToken) {
+        this.accessToken = accessToken;
+        return this;
+    }
+
+    public List<String> getMyGroups(String prefix) {
+        return new GetUserGroupsCall(this).getUserGroups(prefix);
+    }
+
+    @Override
+    public HttpRequest.Builder newRequest(String endpoint) {
+        if (accessToken == null) {
+            throw new IllegalStateException("Access token is null");
+        }
+        return super.newRequest(getUri(endpoint))
+                .setHeader("Authorization", "Bearer " + accessToken);
+    }
+
+    @Override
+    public HttpRequest.Builder newRequest(URI uri) {
+        if (accessToken == null) {
+            throw new IllegalStateException("Access token is null");
+        }
+        return super.newRequest(uri)
+                .setHeader("Authorization", "Bearer " + accessToken);
+    }
+
+    @Override
+    protected <T> String getInvalidStatusCodeExceptionMessage(HttpRequest request, HttpResponse<T> response) {
+        return response.headers().firstValue("Content-Type")
+                .map(contentType -> {
+                    try {
+                        if (contentType.startsWith("text/plain")) {
+                            String errorResponseBody = null;
+                            if (response.body() instanceof String) {
+                                errorResponseBody = (String) response.body();
+                            } else if (response.body() instanceof InputStream) {
+                                errorResponseBody = new String(((InputStream) response.body()).readAllBytes(), StandardCharsets.UTF_8);
+                            }
+                            if (errorResponseBody != null && !errorResponseBody.isBlank()) {
+                                return errorResponseBody;
+                            }
+                        } else if (contentType.startsWith("application/json")
+                                || contentType.startsWith("text/json")) {
+                            Map<String, Object> errorResponseBody = null;
+                            if (response.body() instanceof String) {
+                                errorResponseBody = MAPPER.readValue((String) response.body(), Map.class);
+                            } else if (response.body() instanceof InputStream) {
+                                errorResponseBody = MAPPER.readValue((InputStream) response.body(), Map.class);
+                            }
+                            if (errorResponseBody != null && errorResponseBody.containsKey("error")) {
+                                return (String) errorResponseBody.get("error");
+                            }
+                        }
+                    } catch (IOException ex) {
+                    }
+                    return null;
+                }).orElse(super.getInvalidStatusCodeExceptionMessage(request, response));
+    }
+
+    public List<String> listGroups(String prefix) {
+        return new ListGroupsCall(this).listGroups(prefix);
+    }
+
+    public List<String> getUserGroups(String userId) {
+        return new GetUserGroupsCall(this).getUserGroups(userId);
+    }
+
+    public List<String> getUserGroups(String userId, String prefix) {
+        return new GetUserGroupsCall(this).getUserGroups(userId, prefix);
+    }
+
+    public void createGroup(String completeGroupName, boolean leaf) {
+        new CreateGroupCall(this).createGroup(completeGroupName, leaf);
+    }
+
+    public void deleteGroup(String completeGroupName) {
+        new DeleteGroupCall(this).deleteGroup(completeGroupName);
+    }
+
+    public void addMember(String completeGroupName, String userId) {
+        new AddMemberCall(this).addMember(completeGroupName, userId);
+    }
+
+    public void removeMember(String completeGroupName, String userId) {
+        new RemoveMemberCall(this).removeMember(completeGroupName, userId);
+    }
+
+    public String addPermission(String completeGroupName, String userId, Permission permission) {
+        return new AddPermissionCall(this).addPermission(completeGroupName, userId, permission);
+    }
+
+    public String setPermission(String completeGroupName, String userId, Permission permission) {
+        return new SetPermissionCall(this).setPermission(completeGroupName, userId, permission);
+    }
+
+    public void removePermission(String completeGroupName, String userId) {
+        new RemovePermissionCall(this).removePermission(completeGroupName, userId);
+    }
+
+    public List<UserPermission> getUserPermissions(String userId) {
+        return new GetUserPermissionsCall(this).getUserPermissions(userId);
+    }
+
+    public List<GroupPermission> getGroupPermissions(String groupId) {
+        return new GetGroupPermissionsCall(this).getGroupPermissions(groupId);
+    }
+
+    public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) {
+        new AddInvitedRegistrationCall(this).addInvitedRegistration(token, email, groupsPermissions);
+    }
+
+    public List<String> getMemberEmailAddresses(String groupId, Permission permission) {
+        return new GetMemberEmailAddresses(this).getMemberEmailAddresses(groupId, permission);
+    }
+
+    public List<String[]> getStatus(String groupCompleteName) {
+        return new GetGroupStatusCall(this).getStatus(groupCompleteName);
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
similarity index 66%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
index d0a1ad4ce706082acb6fc4ead041c149421370b3..533eea8f185b6ef4fa63954f1264b08dc9d0b73b 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
@@ -1,8 +1,11 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import it.inaf.ia2.gms.client.model.Permission;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -10,10 +13,10 @@ import java.util.Base64;
 import java.util.Map;
 import java.util.stream.Collectors;
 
-public class AddInvitedRegistrationCall extends BaseGmsCall {
+public class AddInvitedRegistrationCall extends BaseCall<GmsClient> {
 
-    public AddInvitedRegistrationCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public AddInvitedRegistrationCall(GmsClient client) {
+        super(client);
     }
 
     public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) {
@@ -29,20 +32,13 @@ public class AddInvitedRegistrationCall extends BaseGmsCall {
                         .stream().map(e -> e.getKey() + " " + e.getValue())
                         .collect(Collectors.toList()));
 
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
+        HttpRequest groupsRequest = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(HttpRequest.BodyPublishers.ofString(bodyParams))
+                .POST(BodyPublishers.ofString(bodyParams))
                 .build();
 
-        getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 201) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to create invited registration");
-                }).join();
+        client.call(groupsRequest, BodyHandlers.ofInputStream(), 201, res -> true);
     }
 
     private String getTokenHash(String token) {
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
new file mode 100644
index 0000000000000000000000000000000000000000..70bb477eb621127bacf3f24140c33a757da671c5
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
@@ -0,0 +1,30 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class AddMemberCall extends BaseCall<GmsClient> {
+
+    public AddMemberCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean addMember(String completeGroupName, String userId) {
+
+        String endpoint = "ws/jwt/membership";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(BodyPublishers.ofString("user_id=" + userId))
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200, res -> true);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
new file mode 100644
index 0000000000000000000000000000000000000000..ace30eccf540a97fb56e84fdee2dca5eea1d21a6
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
@@ -0,0 +1,35 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import it.inaf.ia2.gms.client.model.Permission;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class AddPermissionCall extends BaseCall<GmsClient> {
+
+    public AddPermissionCall(GmsClient client) {
+        super(client);
+    }
+
+    public String addPermission(String completeGroupName, String userId, Permission permission) {
+
+        String endpoint = "ws/jwt/permission";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+
+        BodyPublisher requestBody = BodyPublishers.ofString(
+                "user_id=" + userId + "&permission=" + permission);
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(requestBody)
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofString(), 200, res -> res);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
new file mode 100644
index 0000000000000000000000000000000000000000..edb5158ce23c9027e23f0dea538a61f4a39d8026
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
@@ -0,0 +1,25 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class CreateGroupCall extends BaseCall<GmsClient> {
+
+    public CreateGroupCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean createGroup(String completeGroupName, boolean leaf) {
+
+        HttpRequest groupsRequest = client.newRequest("ws/jwt/" + completeGroupName)
+                .header("Accept", "text/plain")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(BodyPublishers.ofString("leaf=" + leaf))
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 201, res -> true);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
new file mode 100644
index 0000000000000000000000000000000000000000..bbde41dc7dfe37f830a3228a13a641b278f5dc23
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
@@ -0,0 +1,23 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class DeleteGroupCall extends BaseCall<GmsClient> {
+
+    public DeleteGroupCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean deleteGroup(String completeGroupName) {
+
+        HttpRequest groupsRequest = client.newRequest("ws/jwt/" + completeGroupName)
+                .header("Accept", "text/plain")
+                .DELETE()
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 204, res -> true);
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
similarity index 58%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
index 6c0b99635b606af4b4bfd1d7a5a4445398718e8a..5e782d12c0d473c475e0522626fab8cc312da2da 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
@@ -1,41 +1,36 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import it.inaf.ia2.gms.client.model.GroupPermission;
 import it.inaf.ia2.gms.client.model.Permission;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class GetGroupPermissionsCall extends BaseGmsCall {
+public class GetGroupPermissionsCall extends BaseCall<GmsClient> {
 
-    public GetGroupPermissionsCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public GetGroupPermissionsCall(GmsClient client) {
+        super(client);
     }
 
     public List<GroupPermission> getGroupPermissions(String groupId) {
 
         List<GroupPermission> groupPermissions = new ArrayList<>();
 
-        String endpoint = "permission";
+        String endpoint = "ws/jwt/permission";
         endpoint += "/" + groupId;
 
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
+        HttpRequest groupsRequest = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -48,6 +43,6 @@ public class GetGroupPermissionsCall extends BaseGmsCall {
                         }
                     }
                     return groupPermissions;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetGroupStatusCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetGroupStatusCall.java
new file mode 100644
index 0000000000000000000000000000000000000000..e81ea2dc5c2cdbe411d93b13142d7cb75d884654
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetGroupStatusCall.java
@@ -0,0 +1,27 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.List;
+
+public class GetGroupStatusCall extends BaseCall<GmsClient> {
+
+    public GetGroupStatusCall(GmsClient client) {
+        super(client);
+    }
+
+    public List<String[]> getStatus(String groupCompleteName) {
+
+        String uri = "group/status?groupName=" + groupCompleteName;
+
+        HttpRequest request = client.newRequest(uri)
+                .header("Accept", "application/json")
+                .GET()
+                .build();
+
+        return client.call(request, HttpResponse.BodyHandlers.ofInputStream(), 200,
+                in -> parseJsonList(in, String[].class));
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
similarity index 53%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
index a69913beda35fad563f60363cc3dc7759572297f..c240bcc725ca64fe750bd11722de3065daa6965e 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
@@ -1,16 +1,18 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import it.inaf.ia2.gms.client.model.Permission;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class GetMemberEmailAddresses extends BaseGmsCall {
+public class GetMemberEmailAddresses extends BaseCall<GmsClient> {
 
-    public GetMemberEmailAddresses(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public GetMemberEmailAddresses(GmsClient client) {
+        super(client);
     }
 
     public List<String> getMemberEmailAddresses(String group, Permission permission) {
@@ -22,21 +24,14 @@ public class GetMemberEmailAddresses extends BaseGmsCall {
             endpoint += "?permission=" + permission;
         }
 
-        HttpRequest request = newHttpRequest(endpoint)
+        HttpRequest request = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(request, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(request, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -45,6 +40,6 @@ public class GetMemberEmailAddresses extends BaseGmsCall {
                         }
                     }
                     return emailAddresses;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
similarity index 56%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
index 57ab0ffe8b6ba2e1d88e6ca10ab255a59c05a802..a6a4b66438be803b9c0f4b49f5208f427e21bce4 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
@@ -1,15 +1,17 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class GetUserGroupsCall extends BaseGmsCall {
+public class GetUserGroupsCall extends BaseCall<GmsClient> {
 
-    public GetUserGroupsCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public GetUserGroupsCall(GmsClient client) {
+        super(client);
     }
 
     /**
@@ -20,21 +22,14 @@ public class GetUserGroupsCall extends BaseGmsCall {
 
         List<String> groups = new ArrayList<>();
 
-        HttpRequest groupsRequest = newHttpRequest("search")
+        HttpRequest groupsRequest = client.newRequest("vo/search")
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -50,34 +45,27 @@ public class GetUserGroupsCall extends BaseGmsCall {
                         }
                     }
                     return groups;
-                }).join();
+                });
     }
 
     public List<String> getUserGroups(String userId, String prefix) {
 
         List<String> groups = new ArrayList<>();
 
-        String endpoint = "membership";
+        String endpoint = "ws/jwt/membership";
         if (prefix != null && !prefix.isBlank()) {
             endpoint += "/" + prefix;
         }
         endpoint += "?user_id=" + userId;
 
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
+        HttpRequest groupsRequest = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -86,6 +74,6 @@ public class GetUserGroupsCall extends BaseGmsCall {
                         }
                     }
                     return groups;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
similarity index 58%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
index 34db2f11f5796ac57e7da7e11c0d2d5bd9bfaf2d..8f4e9ed7916d1af0a752321a9fb4797a70181b85 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
@@ -1,41 +1,36 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import it.inaf.ia2.gms.client.model.Permission;
 import it.inaf.ia2.gms.client.model.UserPermission;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class GetUserPermissionsCall extends BaseGmsCall {
+public class GetUserPermissionsCall extends BaseCall<GmsClient> {
 
-    public GetUserPermissionsCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public GetUserPermissionsCall(GmsClient client) {
+        super(client);
     }
 
     public List<UserPermission> getUserPermissions(String userId) {
 
         List<UserPermission> userPermissions = new ArrayList<>();
 
-        String endpoint = "permission";
+        String endpoint = "ws/jwt/permission";
         endpoint += "?user_id=" + userId;
 
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
+        HttpRequest groupsRequest = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -48,6 +43,6 @@ public class GetUserPermissionsCall extends BaseGmsCall {
                         }
                     }
                     return userPermissions;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
similarity index 54%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
index 7170ebaac74285e8be472d82762e08888b3e40ca..2c3123ef896e211fe76c41fec92224d5030620c6 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
@@ -1,15 +1,17 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class ListGroupsCall extends BaseGmsCall {
+public class ListGroupsCall extends BaseCall<GmsClient> {
 
-    public ListGroupsCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public ListGroupsCall(GmsClient client) {
+        super(client);
     }
 
     /**
@@ -21,26 +23,19 @@ public class ListGroupsCall extends BaseGmsCall {
 
         List<String> groups = new ArrayList<>();
 
-        String uri = "list";
+        String uri = "ws/jwt/list";
         if (prefix != null && !prefix.isBlank()) {
             uri += "/" + prefix;
         }
 
-        HttpRequest groupsRequest = newHttpRequest(uri)
+        HttpRequest groupsRequest = client.newRequest(uri)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to list groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -49,6 +44,6 @@ public class ListGroupsCall extends BaseGmsCall {
                         }
                     }
                     return groups;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
new file mode 100644
index 0000000000000000000000000000000000000000..7daf15ecd214c59a820b8ad03ed071216b01b385
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
@@ -0,0 +1,29 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class RemoveMemberCall extends BaseCall<GmsClient> {
+
+    public RemoveMemberCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean removeMember(String completeGroupName, String userId) {
+
+        String endpoint = "ws/jwt/membership";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+        endpoint += "?user_id=" + userId;
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .DELETE()
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 204, res -> true);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
new file mode 100644
index 0000000000000000000000000000000000000000..3602a551cc25a27f31a971a3876b0e108064eec0
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
@@ -0,0 +1,29 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class RemovePermissionCall extends BaseCall<GmsClient> {
+
+    public RemovePermissionCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean removePermission(String completeGroupName, String userId) {
+
+        String endpoint = "ws/jwt/permission";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+        endpoint += "?user_id=" + userId;
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .DELETE()
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 204, res -> true);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
new file mode 100644
index 0000000000000000000000000000000000000000..97ceb21c0b40fd67ab698ae1bff321a4a060f790
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import it.inaf.ia2.gms.client.model.Permission;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class SetPermissionCall extends BaseCall<GmsClient> {
+
+    public SetPermissionCall(GmsClient client) {
+        super(client);
+    }
+
+    public String setPermission(String completeGroupName, String userId, Permission permission) {
+
+        String endpoint = "ws/jwt/permission";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+
+        HttpRequest.BodyPublisher requestBody = HttpRequest.BodyPublishers.ofString(
+                "user_id=" + userId + "&permission=" + permission);
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .PUT(requestBody)
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofString(), 200, res -> res);
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/GroupPermission.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/GroupPermission.java
similarity index 100%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/GroupPermission.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/GroupPermission.java
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Permission.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/Permission.java
similarity index 100%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Permission.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/Permission.java
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/UserPermission.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/UserPermission.java
similarity index 100%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/UserPermission.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/UserPermission.java
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/BaseGmsClientTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/BaseGmsClientTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..69e22fedbd40f7ec99add1b8cb0e989157d7e15a
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/BaseGmsClientTest.java
@@ -0,0 +1,101 @@
+package it.inaf.ia2.gms.client;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import org.mockito.ArgumentMatcher;
+import org.mockito.ArgumentMatchers;
+import static org.mockito.ArgumentMatchers.any;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class BaseGmsClientTest {
+
+    private static final String BASE_URL = "http://base-url";
+
+    protected HttpClient httpClient;
+    protected GmsClient gmsClient;
+
+    public void init() {
+        httpClient = mock(HttpClient.class);
+        gmsClient = getMockedGmsClient(httpClient);
+    }
+
+    protected static String getResourceFileContent(String fileName) {
+        try ( InputStream in = BaseGmsClientTest.class.getClassLoader().getResourceAsStream(fileName)) {
+            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
+    }
+
+    protected static GmsClient getMockedGmsClient(HttpClient mockedHttpClient) {
+
+        HttpClient.Builder builder = mock(HttpClient.Builder.class);
+        when(builder.followRedirects(any())).thenReturn(builder);
+        when(builder.version(any())).thenReturn(builder);
+        when(builder.build()).thenReturn(mockedHttpClient);
+
+        try ( MockedStatic<HttpClient> staticMock = Mockito.mockStatic(HttpClient.class)) {
+            staticMock.when(HttpClient::newBuilder).thenReturn(builder);
+            return new GmsClient(BASE_URL).setAccessToken("foo");
+        }
+    }
+
+    protected static CompletableFuture<HttpResponse<InputStream>> getMockedStreamResponseFuture(int statusCode, String body) {
+        return CompletableFuture.completedFuture(getMockedStreamResponse(200, body));
+    }
+
+    protected static CompletableFuture<HttpResponse<String>> getMockedStringResponseFuture(int statusCode, String body) {
+        return CompletableFuture.completedFuture(getMockedStringResponse(200, body));
+    }
+
+    protected static HttpResponse<InputStream> getMockedStreamResponse(int statusCode, String body) {
+        HttpResponse response = getMockedResponse(statusCode);
+        InputStream in = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
+        when(response.body()).thenReturn(in);
+        return response;
+    }
+
+    protected static HttpResponse<String> getMockedStringResponse(int statusCode, String body) {
+        HttpResponse response = getMockedResponse(statusCode);
+        when(response.body()).thenReturn(body);
+        return response;
+    }
+
+    protected static HttpResponse getMockedResponse(int statusCode) {
+        HttpResponse response = mock(HttpResponse.class);
+        when(response.statusCode()).thenReturn(statusCode);
+        return response;
+    }
+
+    protected static HttpRequest endpointEq(String expectedMethod, String expectedEndpoint) {
+        return ArgumentMatchers.argThat(endpointEqArgumentMatcher(expectedMethod, expectedEndpoint));
+    }
+
+    protected static ArgumentMatcher<HttpRequest> endpointEqArgumentMatcher(String expectedMethod, String expectedEndpoint) {
+
+        return new ArgumentMatcher<HttpRequest>() {
+
+            private final String expectedUri = BASE_URL + "/" + expectedEndpoint;
+
+            @Override
+            public boolean matches(HttpRequest request) {
+                return expectedMethod.equals(request.method()) && expectedUri.equals(request.uri().toString());
+            }
+
+            @Override
+            public String toString() {
+                return expectedMethod + " " + expectedUri;
+            }
+        };
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddMemberTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddMemberTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..268b2f0f957e5908584de13e9b437984b377a8ee
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddMemberTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class AddMemberTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testAddMember() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.addMember("LBT.INAF", "user");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "ws/jwt/membership/LBT.INAF"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddPermissionTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddPermissionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4c68372d3e8ebdb3e4c62c2ba791145f3556c450
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddPermissionTest.java
@@ -0,0 +1,34 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import it.inaf.ia2.gms.client.model.Permission;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class AddPermissionTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testAddPermission() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.addPermission("LBT.INAF", "user", Permission.ADMIN);
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "ws/jwt/permission/LBT.INAF"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/CreateGroupTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/CreateGroupTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8c62a3fc3e52fe29fb60a77ddb16f243161e0548
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/CreateGroupTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class CreateGroupTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testCreateGroup() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(201));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.createGroup("LBT.INAF", false);
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "ws/jwt/LBT.INAF"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/DeleteGroupTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/DeleteGroupTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e5b1161ba230b8c11170ec0b9eb16a4e9cecdb0b
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/DeleteGroupTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class DeleteGroupTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testDeleteGroup() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.deleteGroup("LBT.INAF");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "ws/jwt/LBT.INAF"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/GetUserGroupsTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/GetUserGroupsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b2dee689ffc556752cb0af3027803482f97d130
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/GetUserGroupsTest.java
@@ -0,0 +1,61 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class GetUserGroupsTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testGetMyGroups() {
+
+        String body = "LBT.INAF\n"
+                + "LBT.AZ\n"
+                + "";
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedStreamResponse(200, body));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        List<String> groups = gmsClient.getMyGroups("LBT.");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "vo/search"), any());
+
+        assertEquals(2, groups.size());
+        assertEquals("INAF", groups.get(0));
+        assertEquals("AZ", groups.get(1));
+    }
+
+    @Test
+    public void testListGroups() {
+
+        String body = "INAF\n"
+                + "AZ";
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedStreamResponse(200, body));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        List<String> groups = gmsClient.listGroups("LBT.");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "ws/jwt/list/LBT."), any());
+
+        assertEquals(2, groups.size());
+        assertEquals("INAF", groups.get(0));
+        assertEquals("AZ", groups.get(1));
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/InvitedRegistrationTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/InvitedRegistrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..b234777c9ead015a0a06d9e247a9a68dbe2a9da4
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/InvitedRegistrationTest.java
@@ -0,0 +1,98 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import it.inaf.ia2.gms.client.model.Permission;
+import java.net.http.HttpResponse;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.AdditionalMatchers;
+import org.mockito.ArgumentMatchers;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class InvitedRegistrationTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testInvitedRegistration() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(201));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        Map<String, Permission> permissionsMap = new HashMap<>();
+        permissionsMap.put("group1", Permission.MANAGE_MEMBERS);
+        permissionsMap.put("group2", Permission.MANAGE_MEMBERS);
+        gmsClient.addInvitedRegistration("bvjsgqu423", "email", permissionsMap);
+        // hash = AOyojiwaRR7BHPde6Tomg3+BMoQQggNM3wUHEarXuNQ=
+
+        verify(httpClient, times(1)).sendAsync(
+                AdditionalMatchers.and(
+                        endpointEq("POST", "invited-registration"),
+                        ArgumentMatchers.argThat(req -> {
+                            String reqbody = req.bodyPublisher().map(p -> {
+                                var bodySubscriber = HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
+                                var flowSubscriber = new StringSubscriber(bodySubscriber);
+                                p.subscribe(flowSubscriber);
+                                return bodySubscriber.getBody().toCompletableFuture().join();
+                            }).get();
+
+                            // If the resulting hash contains a + symbol it has to be encoded to %2B,
+                            // otherwise it will be interpreted as a space and wrong value will be
+                            // stored into the database
+                            String expectedBody = "token_hash=AOyojiwaRR7BHPde6Tomg3%2BBMoQQggNM3wUHEarXuNQ="
+                                    + "&email=email&groups=group2 MANAGE_MEMBERS\n"
+                                    + "group1 MANAGE_MEMBERS";
+
+                            return reqbody.equals(expectedBody);
+                        })), any());
+    }
+
+    /**
+     * Credit: https://stackoverflow.com/a/55816685/771431
+     */
+    static final class StringSubscriber implements Flow.Subscriber<ByteBuffer> {
+
+        final HttpResponse.BodySubscriber<String> wrapped;
+
+        StringSubscriber(HttpResponse.BodySubscriber<String> wrapped) {
+            this.wrapped = wrapped;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            wrapped.onSubscribe(subscription);
+        }
+
+        @Override
+        public void onNext(ByteBuffer item) {
+            wrapped.onNext(List.of(item));
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            wrapped.onError(throwable);
+        }
+
+        @Override
+        public void onComplete() {
+            wrapped.onComplete();
+        }
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemoveMemberTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemoveMemberTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..845dbb21087a48ddc68d35a36dfafa95fd18873b
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemoveMemberTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class RemoveMemberTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testRemoveMember() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.removeMember("LBT.INAF", "user");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "ws/jwt/membership/LBT.INAF?user_id=user"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemovePermissionTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemovePermissionTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1a86b0c51085af59139c3f492ad6611c1a5868d
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemovePermissionTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class RemovePermissionTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testRemovePermission() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.removePermission("LBT.INAF", "user");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "ws/jwt/permission/LBT.INAF?user_id=user"), any());
+    }
+}
diff --git a/gms-ui/src/components/GroupsBreadcrumb.vue b/gms-ui/src/components/GroupsBreadcrumb.vue
index c638b20ee8b9f494da85e93d60ffe7f5c29cbf42..8911d9792d504016a7bb3ecf6d12f3cdd7de1926 100644
--- a/gms-ui/src/components/GroupsBreadcrumb.vue
+++ b/gms-ui/src/components/GroupsBreadcrumb.vue
@@ -6,7 +6,7 @@
       <span v-if="group.active">{{group.groupName}}</span>
     </li>
   </ol>
-  <a v-if="currentGroup" :href="'group/status/' + currentGroup.groupId" :download="currentGroup.groupName + '.csv'" id="csv-status-download" title="Download CSV">
+  <a v-if="currentGroup" :href="'group/status?groupId=' + currentGroup.groupId" :download="currentGroup.groupName + '.csv'" id="csv-status-download" title="Download CSV">
     <font-awesome-icon icon="download"></font-awesome-icon>
   </a>
 </nav>
diff --git a/gms/pom.xml b/gms/pom.xml
index af3d2cedda6621b8c6178b66cac6a104a65008ef..011a3eb4433a5152635ee5cf1be5f21a2d357e1c 100644
--- a/gms/pom.xml
+++ b/gms/pom.xml
@@ -40,7 +40,7 @@
         </dependency>
         <dependency>
             <groupId>${project.groupId}</groupId>
-            <artifactId>AuthLib</artifactId>
+            <artifactId>auth-lib</artifactId>
             <version>2.0.0-SNAPSHOT</version>
         </dependency>
         <dependency>
@@ -68,36 +68,46 @@
         </dependency>
     </dependencies>
 
+    <profiles>
+        <profile>
+            <id>build-gui</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.github.eirslett</groupId>
+                        <artifactId>frontend-maven-plugin</artifactId>
+                        <version>1.7.6</version>
+                        <configuration>
+                            <nodeVersion>v12.6.0</nodeVersion>
+                            <environmentVariables>
+                                <VUE_APP_SHOW_USER_ID_IN_SEARCH>${show.user_id_in_search}</VUE_APP_SHOW_USER_ID_IN_SEARCH>
+                            </environmentVariables>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <goals>
+                                    <goal>install-node-and-npm</goal>
+                                </goals>
+                            </execution>
+                            <execution>
+                                <id>npm install</id>
+                                <goals>
+                                    <goal>npm</goal>
+                                </goals>
+                                <configuration>
+                                    <arguments>run build --prefix ../gms-ui</arguments>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
     <build>
         <finalName>gms</finalName>
         <plugins>
-            <plugin>
-                <groupId>com.github.eirslett</groupId>
-                <artifactId>frontend-maven-plugin</artifactId>
-                <version>1.7.6</version>
-                <configuration>
-                    <nodeVersion>v12.6.0</nodeVersion>
-                    <environmentVariables>
-                        <VUE_APP_SHOW_USER_ID_IN_SEARCH>${show.user_id_in_search}</VUE_APP_SHOW_USER_ID_IN_SEARCH>
-                    </environmentVariables>
-                </configuration>
-                <executions>
-                    <execution>
-                        <goals>
-                            <goal>install-node-and-npm</goal>
-                        </goals>
-                    </execution>
-                    <execution>
-                        <id>npm install</id>
-                        <goals>
-                            <goal>npm</goal>
-                        </goals>
-                        <configuration>
-                            <arguments>run build --prefix ../gms-ui</arguments>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
             <plugin>
                 <artifactId>maven-resources-plugin</artifactId>
                 <version>3.1.0</version>
@@ -184,4 +194,10 @@
             </plugin>
         </plugins>
     </build>
+    <repositories>
+        <repository>
+            <id>ia2.snapshot</id>
+            <url>http://repo.ia2.inaf.it/maven/repository/snapshots</url>
+        </repository>
+    </repositories>
 </project>
diff --git a/gms/src/main/java/it/inaf/ia2/gms/GmsApplication.java b/gms/src/main/java/it/inaf/ia2/gms/GmsApplication.java
index 258cc648701faa5edc39702daeee6010f75b3a10..367d4223e336b49e22ce37953952b95259cee7bd 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/GmsApplication.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/GmsApplication.java
@@ -2,14 +2,18 @@ package it.inaf.ia2.gms;
 
 import it.inaf.ia2.aa.AuthConfig;
 import it.inaf.ia2.aa.ServiceLocator;
-import it.inaf.ia2.aa.UriCustomizer;
-import it.inaf.ia2.aa.jwt.QueryStringBuilder;
+import it.inaf.ia2.aa.data.ServletCodeRequestData;
+import it.inaf.ia2.client.QueryStringBuilder;
+import it.inaf.ia2.client.UriCustomizer;
 import static it.inaf.ia2.gms.authn.ClientDbFilter.CLIENT_DB;
 import it.inaf.ia2.gms.exception.BadRequestException;
+import it.inaf.ia2.rap.client.RapClient;
+import java.net.URI;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
@@ -20,19 +24,32 @@ public class GmsApplication {
 
     public static void main(String[] args) {
         SpringApplication.run(GmsApplication.class, args);
+    }
+
+    @Bean
+    public AuthConfig authConfig() {
+        return ServiceLocator.getInstance().getConfig();
+    }
+
+    @Bean
+    public RapClient rapClient(AuthConfig authConfig) {
+
+        URI defaultAuthorizationUri = URI.create(authConfig.getRapBaseUri())
+                .resolve(authConfig.getUserAuthorizationEndpoint());
 
-        AuthConfig authConfig = ServiceLocator.getInstance().getConfig();
+        URI defaultAccessTokenUri = URI.create(authConfig.getRapBaseUri())
+                .resolve(authConfig.getAccessTokenEndpoint());
 
-        final String defaultAuthorizationUri = authConfig.getUserAuthorizationUri();
+        RapClient rapClient = ServiceLocator.getInstance().getRapClient();
 
-        authConfig.setAuthorizationUriCustomizer(new UriCustomizer() {
+        rapClient.setAuthorizationUriCustomizer(new UriCustomizer<HttpServletRequest>() {
 
             @Override
-            public String getBaseUri(HttpServletRequest req) {
+            public URI getBaseUri(HttpServletRequest req) {
                 // for a better security we should check for allowed redirects
                 String redirect = req.getParameter("redirect");
                 if (redirect != null) {
-                    return redirect;
+                    return URI.create(redirect);
                 }
                 return defaultAuthorizationUri;
             }
@@ -53,14 +70,17 @@ public class GmsApplication {
             }
         });
 
-        final String defaultAccessTokenUri = authConfig.getAccessTokenUri();
-
-        authConfig.setAccessTokenUriCustomizer(req -> {
-            String redirect = req.getParameter("token_uri");
-            if (redirect != null) {
-                return redirect;
+        rapClient.setAccessTokenUriCustomizer(new UriCustomizer<ServletCodeRequestData>() {
+            @Override
+            public URI getBaseUri(ServletCodeRequestData req) {
+                String redirect = req.getCodeRequest().getParameter("token_uri");
+                if (redirect != null) {
+                    return URI.create(redirect);
+                }
+                return defaultAccessTokenUri;
             }
-            return defaultAccessTokenUri;
         });
+
+        return rapClient;
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/ClientDbFilter.java b/gms/src/main/java/it/inaf/ia2/gms/authn/ClientDbFilter.java
index 251dd4669ba03152e0571a3b5a326edbf45f2d91..526ffac5f802ccf08a111afaafe19ab7925b419b 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/authn/ClientDbFilter.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/authn/ClientDbFilter.java
@@ -1,41 +1,38 @@
 package it.inaf.ia2.gms.authn;
 
-import it.inaf.ia2.aa.ServiceLocator;
-import it.inaf.ia2.aa.jwt.JwksClient;
+import it.inaf.ia2.aa.AuthConfig;
+import it.inaf.ia2.rap.client.RapClient;
 import java.io.IOException;
+import java.net.URI;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
 
 public class ClientDbFilter implements Filter {
 
     public static final String CLIENT_DB = "client_db";
 
-    private String defaultJwksUri;
-    private JwksClient jwksClient;
+    private final RapClient rapClient;
+    private final String defaultJwksUri;
 
-    @Override
-    public void init(FilterConfig filterConfig) throws ServletException {
-        defaultJwksUri = ServiceLocator.getInstance().getConfig().getJwksUri();
-        jwksClient = ServiceLocator.getInstance().getJwksClient();
+    public ClientDbFilter(AuthConfig authConfig, RapClient rapClient) {
+        this.rapClient = rapClient;
+        defaultJwksUri = URI.create(authConfig.getRapBaseUri()).resolve(authConfig.getJwksEndpoint()).toString();
     }
 
     @Override
     public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) throws IOException, ServletException {
 
         HttpServletRequest request = (HttpServletRequest) req;
-        HttpServletResponse response = (HttpServletResponse) res;
 
         String clientDb = request.getParameter(CLIENT_DB);
         if (clientDb != null) {
             request.getSession().setAttribute(CLIENT_DB, clientDb);
             String newUrl = defaultJwksUri.replaceAll("\\?client_name=(.*)", "?client_name=" + clientDb);
-            jwksClient.addJwksUrl(newUrl);
+            rapClient.addJwksUri(URI.create(newUrl));
         }
 
         fc.doFilter(req, res);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/GmsLoginFilter.java b/gms/src/main/java/it/inaf/ia2/gms/authn/GmsLoginFilter.java
index a2a081a693173367e466b070a88378e43943602b..af5000bcdba79cff1d09dfcf334378e0d3277956 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/authn/GmsLoginFilter.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/authn/GmsLoginFilter.java
@@ -26,6 +26,11 @@ public class GmsLoginFilter extends LoginFilter {
 
     private boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
 
+        if (request.getUserPrincipal() != null) {
+            // Principal set using JWT
+            return true;
+        }
+
         // Allow CORS check
         if ("OPTIONS".equals(request.getMethod())) {
             return true;
diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/JWTFilter.java b/gms/src/main/java/it/inaf/ia2/gms/authn/JWTFilter.java
index 9013c25432423528b134c3b96fc986b0910b8de3..f2c047c6cfdd7ae965443cc2c1768f21b9d55752 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/authn/JWTFilter.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/authn/JWTFilter.java
@@ -1,10 +1,8 @@
 package it.inaf.ia2.gms.authn;
 
-import io.jsonwebtoken.Jwt;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.SigningKeyResolver;
-import it.inaf.ia2.aa.ServiceLocator;
+import it.inaf.ia2.aa.data.User;
 import it.inaf.ia2.gms.persistence.LoggingDAO;
+import it.inaf.ia2.rap.client.RapClient;
 import java.io.IOException;
 import java.security.Principal;
 import java.util.Map;
@@ -16,15 +14,16 @@ import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
 
 public class JWTFilter implements Filter {
 
     private final LoggingDAO loggingDAO;
-    private final SigningKeyResolver signingKeyResolver;
+    private final RapClient rapClient;
 
-    public JWTFilter(LoggingDAO loggingDAO) {
+    public JWTFilter(LoggingDAO loggingDAO, RapClient rapClient) {
         this.loggingDAO = loggingDAO;
-        this.signingKeyResolver = ServiceLocator.getInstance().getTokenManager().getSigningKeyResolver();
+        this.rapClient = rapClient;
     }
 
     @Override
@@ -34,19 +33,28 @@ public class JWTFilter implements Filter {
         HttpServletResponse response = (HttpServletResponse) res;
 
         String authHeader = request.getHeader("Authorization");
+
         if (authHeader == null) {
-            loggingDAO.logAction("Attempt to access WS without token", request);
-            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing Authorization token");
+
+            if (request.isRequestedSessionIdValid()) {
+                HttpSession session = request.getSession(false);
+                User user = (User) session.getAttribute("user_data");
+                if (user != null) {
+                    rapClient.setAccessToken(user.getAccessToken());
+                    ServletRequestWithSessionPrincipal wrappedRequest = new ServletRequestWithSessionPrincipal(request, user);
+                    fc.doFilter(wrappedRequest, res);
+                    return;
+                }
+            }
+
+            fc.doFilter(req, res);
             return;
         }
 
-        authHeader = authHeader.replace("Bearer", "").trim();
+        String token = authHeader.replace("Bearer", "").trim();
 
-        Jwt jwt = Jwts.parser()
-                .setSigningKeyResolver(signingKeyResolver)
-                .parse(authHeader);
-
-        Map<String, Object> claims = (Map<String, Object>) jwt.getBody();
+        rapClient.setAccessToken(token);
+        Map<String, Object> claims = rapClient.parseIdTokenClaims(token);
 
         if (claims.get("sub") == null) {
             loggingDAO.logAction("Attempt to access WS with invalid token", request);
@@ -60,9 +68,24 @@ public class JWTFilter implements Filter {
         fc.doFilter(wrappedRequest, res);
     }
 
+    private static class ServletRequestWithSessionPrincipal extends HttpServletRequestWrapper {
+
+        private final User principal;
+
+        public ServletRequestWithSessionPrincipal(HttpServletRequest request, User user) {
+            super(request);
+            this.principal = user;
+        }
+
+        @Override
+        public Principal getUserPrincipal() {
+            return principal;
+        }
+    }
+
     private static class ServletRequestWithJWTPrincipal extends HttpServletRequestWrapper {
 
-        private final Principal principal;
+        private final RapPrincipal principal;
 
         public ServletRequestWithJWTPrincipal(HttpServletRequest request, Map<String, Object> jwtClaims) {
             super(request);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java
index 996d011190bc35de63d2c63b5936a6051535e778..92e04da0c9fa6fbc040adc31caa1f3a7880fcfd4 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/authn/SecurityConfig.java
@@ -1,6 +1,8 @@
 package it.inaf.ia2.gms.authn;
 
+import it.inaf.ia2.aa.AuthConfig;
 import it.inaf.ia2.gms.persistence.LoggingDAO;
+import it.inaf.ia2.rap.client.RapClient;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
@@ -44,9 +46,9 @@ public class SecurityConfig {
     }
 
     @Bean
-    public FilterRegistrationBean clientDbFilter() {
+    public FilterRegistrationBean clientDbFilter(AuthConfig authConfig, RapClient rapClient) {
         FilterRegistrationBean bean = new FilterRegistrationBean();
-        bean.setFilter(new ClientDbFilter());
+        bean.setFilter(new ClientDbFilter(authConfig, rapClient));
         bean.addUrlPatterns("/*");
         bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
         return bean;
@@ -56,10 +58,10 @@ public class SecurityConfig {
      * Checks JWT for web services.
      */
     @Bean
-    public FilterRegistrationBean serviceJWTFilter(LoggingDAO loggingDAO) {
+    public FilterRegistrationBean serviceJWTFilter(LoggingDAO loggingDAO, RapClient rapClient) {
         FilterRegistrationBean bean = new FilterRegistrationBean();
-        bean.setFilter(new JWTFilter(loggingDAO));
-        bean.addUrlPatterns("/ws/jwt/*");
+        bean.setFilter(new JWTFilter(loggingDAO, rapClient));
+        bean.addUrlPatterns("/*");
         bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
         return bean;
     }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/authn/SessionData.java b/gms/src/main/java/it/inaf/ia2/gms/authn/SessionData.java
index 0de00ba5ab5505fbcaba89a9b6d9bc206d1f9b1f..6d4194efdf445900f8dc366b037bd07fc80213fd 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/authn/SessionData.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/authn/SessionData.java
@@ -1,6 +1,7 @@
 package it.inaf.ia2.gms.authn;
 
 import it.inaf.ia2.aa.data.User;
+import it.inaf.ia2.rap.client.RapClient;
 import javax.annotation.PostConstruct;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
@@ -14,58 +15,44 @@ public class SessionData {
 
     private static final String USER_DATA = "user_data";
 
+    private User user;
+
     @Autowired
     private HttpServletRequest request;
 
-    private String userId;
-    private String userName;
-    private String accessToken;
-    private String refreshToken;
-    private long expiration;
+    @Autowired
+    private RapClient rapClient;
 
     @PostConstruct
     public void init() {
-
         HttpSession session = request.getSession(false);
         if (session != null && session.getAttribute(USER_DATA) != null) {
-            User user = (User) session.getAttribute(USER_DATA);
-            userId = user.getName();
-            userName = user.getUserLabel();
-            accessToken = user.getAccessToken();
-            refreshToken = user.getRefreshToken();
-            setExpiresIn(user.getExpiresIn());
+            setUser((User) session.getAttribute(USER_DATA));
         }
     }
 
-    public String getUserId() {
-        return userId;
-    }
-
-    public String getAccessToken() {
-        return accessToken;
+    public void setUser(User user) {
+        this.user = user;
+        rapClient.setAccessToken(user.getAccessToken());
     }
 
-    public void setAccessToken(String accessToken) {
-        this.accessToken = accessToken;
-    }
-
-    public String getRefreshToken() {
-        return refreshToken;
+    public String getUserId() {
+        return user.getName();
     }
 
-    public void setRefreshToken(String refreshToken) {
-        this.refreshToken = refreshToken;
+    public String getUserName() {
+        return user.getUserLabel();
     }
 
-    public String getUserName() {
-        return userName;
+    public String getAccessToken() {
+        return user.getAccessToken();
     }
 
-    public void setExpiresIn(long expiresIn) {
-        this.expiration = System.currentTimeMillis() + expiresIn * 1000;
+    public String getRefreshToken() {
+        return user.getRefreshToken();
     }
 
     public long getExpiresIn() {
-        return (expiration - System.currentTimeMillis()) / 1000;
+        return user.getExpiresIn();
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java
index fe9137ed62cca9a1d3337718d95fdb967923a464..d4c855a5bd5010e8727802b6dc43acde4f9b668d 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java
@@ -1,7 +1,7 @@
 package it.inaf.ia2.gms.controller;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.opencsv.CSVWriter;
-import it.inaf.ia2.gms.authn.SessionData;
 import it.inaf.ia2.gms.manager.GroupStatusManager;
 import it.inaf.ia2.gms.manager.GroupsManager;
 import it.inaf.ia2.gms.model.request.AddGroupRequest;
@@ -13,9 +13,13 @@ import it.inaf.ia2.gms.model.request.GroupsRequest;
 import it.inaf.ia2.gms.model.request.RenameGroupRequest;
 import it.inaf.ia2.gms.model.request.SearchFilterRequest;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.service.GroupNameService;
 import it.inaf.ia2.gms.service.GroupsTreeBuilder;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.util.List;
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -28,13 +32,16 @@ import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PutMapping;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
 public class GroupsController {
 
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
     @Autowired
-    private SessionData session;
+    private HttpServletRequest servletRequest;
 
     @Autowired
     private GroupsManager groupsManager;
@@ -48,6 +55,9 @@ public class GroupsController {
     @Autowired
     private GroupStatusManager groupStatusManager;
 
+    @Autowired
+    private GroupNameService groupNameService;
+
     @GetMapping(value = "/groups", produces = MediaType.APPLICATION_JSON_VALUE)
     public ResponseEntity<?> getGroupsTab(@Valid GroupsRequest request) {
         if (request.isOnlyPanel()) {
@@ -93,21 +103,40 @@ public class GroupsController {
         return ResponseEntity.ok(groupsPanel);
     }
 
-    @GetMapping(value = "/group/status/{groupId}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
-    public void downloadStatus(@PathVariable("groupId") String groupId, HttpServletResponse response) throws Exception {
+    @GetMapping(value = "/group/status", produces = {MediaType.APPLICATION_OCTET_STREAM_VALUE, MediaType.APPLICATION_JSON_VALUE})
+    public void downloadStatus(@RequestParam(value = "groupId", required = false) String groupId,
+            @RequestParam(value = "groupName", required = false) String groupName,
+            HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+        if (groupId == null && groupName == null) {
+            response.sendError(400, "Parameter groupId or groupName is required");
+            return;
+        }
+
+        if (groupId == null) {
+            GroupEntity group = groupNameService.getGroupFromNames(Optional.of(groupName));
+            groupId = group.getId();
+        }
+
+        List<String[]> status = groupStatusManager.generateStatus(groupId);
 
-        try (OutputStream out = response.getOutputStream();
-                CSVWriter writer = new CSVWriter(new OutputStreamWriter(out))) {
+        try ( OutputStream out = response.getOutputStream()) {
 
-            writer.writeNext(new String[]{"program", "email"});
+            if ("application/json".equals(request.getHeader("Accept"))) {
+                MAPPER.writeValue(out, status);
+            } else {
+                try ( CSVWriter writer = new CSVWriter(new OutputStreamWriter(out))) {
+                    writer.writeNext(new String[]{"program", "email"});
 
-            for (String[] row : groupStatusManager.generateStatus(groupId)) {
-                writer.writeNext(row);
+                    for (String[] row : status) {
+                        writer.writeNext(row);
+                    }
+                }
             }
         }
     }
 
     private <T extends PaginatedModelRequest & SearchFilterRequest> PaginatedData<GroupNode> getGroupsPanel(GroupEntity parentGroup, T request) {
-        return groupsTreeBuilder.listSubGroups(parentGroup, request, session.getUserId());
+        return groupsTreeBuilder.listSubGroups(parentGroup, request, servletRequest.getUserPrincipal().getName());
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java
index 2718b19cd8d586c6fd8ca882b31466a4538f29bb..2da6216a9c827234cdcf4f7e068cda6c49fbd5c7 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java
@@ -1,6 +1,5 @@
 package it.inaf.ia2.gms.controller;
 
-import it.inaf.ia2.gms.authn.SessionData;
 import it.inaf.ia2.gms.manager.GroupsManager;
 import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
@@ -10,6 +9,7 @@ import it.inaf.ia2.gms.model.response.GroupsTabResponse;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.service.GroupsService;
 import it.inaf.ia2.gms.service.GroupsTreeBuilder;
+import javax.servlet.http.HttpServletRequest;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -17,7 +17,7 @@ import org.springframework.stereotype.Component;
 public class GroupsTabResponseBuilder {
 
     @Autowired
-    private SessionData session;
+    HttpServletRequest servletRequest;
 
     @Autowired
     private PermissionsManager permissionsManager;
@@ -46,7 +46,7 @@ public class GroupsTabResponseBuilder {
         Permission permission = permissionsManager.getCurrentUserPermission(group);
         response.setPermission(permission);
 
-        response.setGroupsPanel(groupsListBuilder.listSubGroups(group, request, session.getUserId()));
+        response.setGroupsPanel(groupsListBuilder.listSubGroups(group, request, servletRequest.getUserPrincipal().getName()));
 
         response.setLeaf(group.isLeaf());
 
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java
index 90a02bb60bdf1dc60fbf8bfcf88f3220ca244e8c..8536f98d76a61c25ad914f4b5aa265fc4df1fa0b 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/HomePageController.java
@@ -83,6 +83,10 @@ public class HomePageController {
     @GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE)
     public String index(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 
+        // This page MUST NOT be cached to avoid losing the login redirect
+        response.setHeader("Cache-Control", "no-store, must-revalidate");
+        response.setHeader("Expires", "0");
+
         Optional<List<InvitedRegistration>> optReg = invitedRegistrationManager.completeInvitedRegistrationIfNecessary();
         if (optReg.isPresent()) {
             request.setAttribute("invited-registrations", optReg.get());
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
index 8607c4b2e1ca838e1dc166e2fb634b8d80135e2b..554d4437ef63752fda45efd5468b6cf01dc0b355 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/JWTWebServiceController.java
@@ -7,7 +7,6 @@ import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
 import it.inaf.ia2.gms.manager.MembershipManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.RapUser;
 import it.inaf.ia2.gms.model.response.UserPermission;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.PermissionsDAO;
@@ -19,6 +18,7 @@ import it.inaf.ia2.gms.service.GroupsService;
 import it.inaf.ia2.gms.service.JoinService;
 import it.inaf.ia2.gms.service.PermissionUtils;
 import it.inaf.ia2.gms.service.SearchService;
+import it.inaf.ia2.rap.data.RapUser;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.security.Principal;
@@ -39,15 +39,16 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 /**
- * Web service called by other web applications using JWT (delegation).
+ * This class needs some refactoring: it contains all endpoints that used JWT.
+ * Now all endpoints accept both a JWT token or a session, so some of them could
+ * be removed and others should be moved on dedicated classes. Some endpoints
+ * match 2 patters to achieve a smooth transition.
  */
 @RestController
-@RequestMapping("/ws/jwt")
 public class JWTWebServiceController {
 
     @Autowired
@@ -63,7 +64,7 @@ public class JWTWebServiceController {
     private GroupsService groupsService;
 
     @Autowired
-    private GroupNameService groupNameService;
+    protected GroupNameService groupNameService;
 
     @Autowired
     private MembershipManager membershipManager;
@@ -83,7 +84,7 @@ public class JWTWebServiceController {
     /**
      * This endpoint is compliant with the IVOA GMS standard.
      */
-    @GetMapping(value = "/search", produces = MediaType.TEXT_PLAIN_VALUE)
+    @GetMapping(value = {"/ws/jwt/search", "/vo/search"}, produces = MediaType.TEXT_PLAIN_VALUE)
     public void getGroups(HttpServletResponse response) throws IOException {
 
         List<GroupEntity> memberships = membershipManager.getCurrentUserMemberships();
@@ -104,10 +105,10 @@ public class JWTWebServiceController {
      * be defined adding ".+", otherwise Spring will think it is a file
      * extension (thanks https://stackoverflow.com/a/16333149/771431)
      */
-    @GetMapping(value = "/search/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
+    @GetMapping(value = {"/ws/jwt/search/{group:.+}", "/vo/search/{group:.+}"}, produces = MediaType.TEXT_PLAIN_VALUE)
     public void isMemberOf(@PathVariable("group") String group, HttpServletResponse response) throws IOException {
 
-        List<String> groupNames = extractGroupNames(group);
+        List<String> groupNames = groupNameService.extractGroupNames(group);
 
         boolean isMember = membershipManager.isCurrentUserMemberOf("ROOT");
         if (!isMember) {
@@ -135,13 +136,12 @@ public class JWTWebServiceController {
         // else: empty response (as defined by GMS standard)
     }
 
-    @GetMapping(value = {"/list/{group:.+}", "/list"}, produces = MediaType.TEXT_PLAIN_VALUE)
-    public void listGroups(@PathVariable("group") Optional<String> group, Principal principal, HttpServletResponse response) throws IOException {
+    @GetMapping(value = {"/ws/jwt/list/{group:.+}", "/ws/jwt/list"}, produces = MediaType.TEXT_PLAIN_VALUE)
+    public void listGroups(@PathVariable("group") Optional<String> groupNames, Principal principal, HttpServletResponse response) throws IOException {
 
         String userId = principal.getName();
 
-        List<String> groupNames = extractGroupNames(group);
-        GroupEntity parentGroup = getGroupFromNames(groupNames);
+        GroupEntity parentGroup = groupNameService.getGroupFromNames(groupNames);
 
         List<GroupEntity> allSubGroups = groupsDAO.getDirectSubGroups(parentGroup.getPath());
 
@@ -157,7 +157,7 @@ public class JWTWebServiceController {
 
         try ( PrintWriter pw = new PrintWriter(response.getOutputStream())) {
             for (String groupName : groupNameService.getGroupsNames(visibleSubgroups)) {
-                pw.println(getShortGroupName(groupName, group));
+                pw.println(groupNameService.getShortGroupName(groupName, groupNames));
             }
         }
     }
@@ -166,10 +166,10 @@ public class JWTWebServiceController {
      * Creates a group and its ancestors if they are missing. It doesn't fail if
      * the last group already exists.
      */
-    @PostMapping(value = "/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
+    @PostMapping(value = "/ws/jwt/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
     public void createGroup(@PathVariable("group") String groupParam, HttpServletRequest request, HttpServletResponse response) throws IOException {
 
-        List<String> groupNames = extractGroupNames(groupParam);
+        List<String> groupNames = groupNameService.extractGroupNames(groupParam);
 
         String leafParam = request.getParameter("leaf");
         boolean leaf = leafParam == null ? false : Boolean.valueOf(leafParam);
@@ -191,29 +191,29 @@ public class JWTWebServiceController {
         }
     }
 
-    @DeleteMapping(value = "/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
+    @DeleteMapping(value = "/ws/jwt/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
     public void deleteGroup(@PathVariable("group") String groupParam, HttpServletResponse response) {
-        GroupEntity group = getGroupFromNames(extractGroupNames(groupParam));
+        GroupEntity group = groupNameService.getGroupFromNames(Optional.of(groupParam));
         groupsDAO.deleteGroup(group);
         response.setStatus(HttpServletResponse.SC_NO_CONTENT);
     }
 
-    @GetMapping(value = {"/membership/{group:.+}", "/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
-    public void getMembership(@PathVariable("group") Optional<String> group, @RequestParam("user_id") String userId, HttpServletResponse response) throws IOException {
+    @GetMapping(value = {"/ws/jwt/membership/{group:.+}", "/ws/jwt/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
+    public void getMembership(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") String userId, HttpServletResponse response) throws IOException {
 
-        GroupEntity parent = getGroupFromNames(extractGroupNames(group));
+        GroupEntity parent = groupNameService.getGroupFromNames(groupNames);
 
         List<GroupEntity> groups = membershipManager.getUserGroups(parent, userId);
 
         try ( PrintWriter pw = new PrintWriter(response.getOutputStream())) {
             for (String groupName : groupNameService.getGroupsNames(groups)) {
-                pw.println(getShortGroupName(groupName, group));
+                pw.println(groupNameService.getShortGroupName(groupName, groupNames));
             }
         }
     }
 
-    @PostMapping(value = {"/membership/{group:.+}", "/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
-    public void addMember(@PathVariable("group") Optional<String> group, HttpServletRequest request, HttpServletResponse response) throws IOException {
+    @PostMapping(value = {"/ws/jwt/membership/{group:.+}", "/ws/jwt/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
+    public void addMember(@PathVariable("group") Optional<String> groupNames, HttpServletRequest request, HttpServletResponse response) throws IOException {
 
         String targetUserId = request.getParameter("user_id");
         if (targetUserId == null) {
@@ -221,64 +221,64 @@ public class JWTWebServiceController {
             return;
         }
 
-        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(group));
+        GroupEntity groupEntity = groupNameService.getGroupFromNames(groupNames);
 
         membershipManager.addMember(groupEntity, targetUserId);
     }
 
-    @DeleteMapping(value = {"/membership/{group:.+}", "/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
-    public void removeMember(@PathVariable("group") Optional<String> group, @RequestParam("user_id") String userId,
+    @DeleteMapping(value = {"/ws/jwt/membership/{group:.+}", "/ws/jwt/membership"}, produces = MediaType.TEXT_PLAIN_VALUE)
+    public void removeMember(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") String userId,
             HttpServletRequest request, HttpServletResponse response) throws IOException {
 
-        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(group));
+        GroupEntity groupEntity = groupNameService.getGroupFromNames(groupNames);
         membershipManager.removeMember(groupEntity, userId);
 
         response.setStatus(HttpServletResponse.SC_NO_CONTENT);
     }
 
-    @GetMapping(value = {"/permission/{group:.+}", "/permission"}, produces = MediaType.TEXT_PLAIN_VALUE)
+    @GetMapping(value = {"/ws/jwt/permission/{group:.+}", "/ws/jwt/permission"}, produces = MediaType.TEXT_PLAIN_VALUE)
     public void getUserPermission(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") Optional<String> userId, HttpServletRequest request, HttpServletResponse response) throws IOException {
 
+        GroupEntity groupEntity = groupNameService.getGroupFromNames(groupNames);
         if (userId.isPresent()) {
             try ( PrintWriter pw = new PrintWriter(response.getOutputStream())) {
-                for (UserPermission userPermission : searchService.getUserPermission(userId.get(), permissionsManager.getCurrentUserPermissions(getRoot()))) {
+                for (UserPermission userPermission : searchService.getUserPermission(groupEntity, userId.get(), permissionsManager.getCurrentUserPermissions(groupEntity))) {
                     String group = String.join(".", userPermission.getGroupCompleteName());
                     pw.println(group + " " + userPermission.getPermission());
                 }
             }
         } else {
-            GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
             try ( PrintWriter pw = new PrintWriter(response.getOutputStream())) {
-                for (it.inaf.ia2.gms.model.UserPermission up : permissionsManager.getAllPermissions(groupEntity)) {
+                for (it.inaf.ia2.gms.model.RapUserPermission up : permissionsManager.getAllPermissions(groupEntity)) {
                     pw.println(up.getUser().getId() + " " + up.getPermission());
                 }
             }
         }
     }
 
-    @PostMapping(value = {"/permission/{group:.+}", "/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+    @PostMapping(value = {"/ws/jwt/permission/{group:.+}", "/ws/jwt/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
     public void addPermission(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") String targetUserId, @RequestParam("permission") Permission permission) throws IOException {
-        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
+        GroupEntity groupEntity = groupNameService.getGroupFromNames(groupNames);
         permissionsManager.addPermission(groupEntity, targetUserId, permission);
     }
 
-    @PutMapping(value = {"/permission/{group:.+}", "/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+    @PutMapping(value = {"/ws/jwt/permission/{group:.+}", "/ws/jwt/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
     public void setPermission(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") String targetUserId, @RequestParam("permission") Permission permission) throws IOException {
-        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
+        GroupEntity groupEntity = groupNameService.getGroupFromNames(groupNames);
         permissionsManager.createOrUpdatePermission(groupEntity, targetUserId, permission);
     }
 
-    @DeleteMapping(value = {"/permission/{group:.+}", "/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE)
+    @DeleteMapping(value = {"/ws/jwt/permission/{group:.+}", "/ws/jwt/permission/"}, produces = MediaType.TEXT_PLAIN_VALUE)
     public void removePermission(@PathVariable("group") Optional<String> groupNames, @RequestParam("user_id") String userId,
             HttpServletRequest request, HttpServletResponse response) throws IOException {
 
-        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
+        GroupEntity groupEntity = groupNameService.getGroupFromNames(groupNames);
         permissionsManager.removePermission(groupEntity, userId);
 
         response.setStatus(HttpServletResponse.SC_NO_CONTENT);
     }
 
-    @GetMapping(value = "/check-invited-registration", produces = MediaType.TEXT_PLAIN_VALUE)
+    @GetMapping(value = {"/ws/jwt/check-invited-registration", "/check-invited-registration"}, produces = MediaType.TEXT_PLAIN_VALUE)
     public void completeInvitedRegistrationIfNecessary(Principal principal, HttpServletResponse response) throws IOException {
 
         String userId = principal.getName();
@@ -300,7 +300,7 @@ public class JWTWebServiceController {
         }
     }
 
-    @PostMapping(value = "/invited-registration", produces = MediaType.TEXT_PLAIN_VALUE)
+    @PostMapping(value = {"/ws/jwt/invited-registration", "/invited-registration"}, produces = MediaType.TEXT_PLAIN_VALUE)
     public void addInvitedRegistration(@RequestParam("token_hash") String tokenHash, @RequestParam("email") String email,
             @RequestParam("groups") String groupNamesAndPermissionsParam, HttpServletResponse response) {
 
@@ -311,7 +311,7 @@ public class JWTWebServiceController {
                 int lastSpaceIndex = param.lastIndexOf(" ");
                 String groupName = param.substring(0, lastSpaceIndex);
                 Permission permission = Permission.valueOf(param.substring(lastSpaceIndex + 1));
-                GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupName));
+                GroupEntity groupEntity = groupNameService.getGroupFromNames(Optional.of(groupName));
                 groupsPermissions.put(groupEntity, permission);
             }
         }
@@ -321,10 +321,10 @@ public class JWTWebServiceController {
         response.setStatus(HttpServletResponse.SC_CREATED);
     }
 
-    @GetMapping(value = "/email/{group:.+}", produces = MediaType.TEXT_PLAIN_VALUE)
+    @GetMapping(value = {"/ws/jwt/email/{group:.+}", "/email/{group:.+}"}, produces = MediaType.TEXT_PLAIN_VALUE)
     public void getEmailOfMembers(@PathVariable("group") String groupNames, @RequestParam("permission") Optional<Permission> permission, HttpServletResponse response) throws IOException {
 
-        GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupNames));
+        GroupEntity groupEntity = groupNameService.getGroupFromNames(Optional.of(groupNames));
 
         Set<String> selectedUserIds = null;
         if (permission.isPresent()) {
@@ -340,74 +340,13 @@ public class JWTWebServiceController {
         try ( PrintWriter pw = new PrintWriter(response.getOutputStream())) {
             for (RapUser member : membershipManager.getMembers(groupEntity)) {
                 if (selectedUserIds == null || selectedUserIds.contains(member.getId())) {
-                    pw.println(member.getPrimaryEmail());
+                    pw.println(member.getPrimaryEmailAddress());
                 }
             }
         }
     }
 
-    private GroupEntity getGroupFromNames(List<String> groupNames) {
-        if (groupNames.isEmpty()) {
-            return getRoot();
-        }
-        return getGroupFromNamesAndIndex(groupNames, groupNames.size() - 1);
-    }
-
-    private GroupEntity getGroupFromNamesAndIndex(List<String> groupNames, int index) {
-        String parentPath = ""; // starting from ROOT
-        GroupEntity group = null;
-        for (int i = 0; i < index + 1; i++) {
-            String groupName = groupNames.get(i);
-            group = groupsDAO.findGroupByParentAndName(parentPath, groupName)
-                    .orElseThrow(() -> new BadRequestException("Unable to find group " + groupName));
-            parentPath = group.getPath();
-        }
-        if (group == null) {
-            throw new IllegalStateException();
-        }
-        return group;
-    }
-
-    private GroupEntity getRoot() {
-        return groupsDAO.findGroupById("ROOT")
-                .orElseThrow(() -> new IllegalStateException("Missing root group"));
-    }
-
-    private List<String> extractGroupNames(Optional<String> group) {
-        return extractGroupNames(group.orElse(null));
-    }
-
-    private List<String> extractGroupNames(String groupStr) {
-
-        if (groupStr == null || groupStr.isEmpty()) {
-            return new ArrayList<>();
-        }
-
-        List<String> names = new ArrayList<>();
-        String currentName = "";
-        for (int i = 0; i < groupStr.length(); i++) {
-            char c = groupStr.charAt(i);
-            // dot is the group separator and it must be escaped if used inside
-            // group names
-            if (c == '.' && groupStr.charAt(i - 1) != '\\') {
-                names.add(currentName.replace("\\.", "."));
-                currentName = "";
-            } else {
-                currentName += c;
-            }
-        }
-        names.add(currentName);
-        return names;
-    }
-
-    private String getShortGroupName(String completeGroupName, Optional<String> groupPrefix) {
-        if (groupPrefix.isPresent()) {
-            return completeGroupName.substring(groupPrefix.get().length() + 1);
-        }
-        return completeGroupName;
-    }
-
-    @PostMapping(value = "/join", produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = {"/ws/jwt/join", "/join"}, produces = MediaType.APPLICATION_JSON_VALUE)
     public ResponseEntity<?> join(RapPrincipal principal) {
 
         String fromUser = principal.getName();
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/KeepAliveController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/KeepAliveController.java
index c41f012597cd6a7c5e59ce6d23db9c57049ab8b4..5e4b15fde34b823c852ad66b1523fe4d1873fda4 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/KeepAliveController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/KeepAliveController.java
@@ -1,8 +1,10 @@
 package it.inaf.ia2.gms.controller;
 
+import it.inaf.ia2.aa.ServiceLocator;
+import it.inaf.ia2.aa.UserManager;
 import it.inaf.ia2.gms.authn.SessionData;
-import it.inaf.ia2.gms.rap.RapClient;
 import java.util.HashMap;
+import javax.servlet.http.HttpServletRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -19,14 +21,17 @@ public class KeepAliveController {
     @Autowired
     private SessionData sessionData;
 
-    @Autowired
-    private RapClient rapClient;
+    private final UserManager userManager;
+
+    public KeepAliveController() {
+        userManager = ServiceLocator.getInstance().getUserManager();
+    }
 
     @GetMapping(value = "/keepAlive", produces = MediaType.APPLICATION_JSON_VALUE)
-    public ResponseEntity<?> keepAlive() {
+    public ResponseEntity<?> keepAlive(HttpServletRequest request) {
         LOG.trace("Keepalive called");
         if (sessionData.getExpiresIn() < 60) {
-            rapClient.refreshToken();
+            sessionData.setUser(userManager.refreshToken(request));
             LOG.trace("RAP token refreshed");
         }
         // empty JSON object response
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/MembersController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/MembersController.java
index f31966fc7c63369f672c1a745c4c310c9eae9691..3db24db6b7298493fc9b9c4ada31e10b6316d1cb 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/MembersController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/MembersController.java
@@ -4,12 +4,12 @@ import it.inaf.ia2.gms.manager.MembershipManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
 import it.inaf.ia2.gms.model.request.AddMemberRequest;
 import it.inaf.ia2.gms.model.response.PaginatedData;
-import it.inaf.ia2.gms.model.RapUser;
 import it.inaf.ia2.gms.model.request.PaginatedModelRequest;
 import it.inaf.ia2.gms.model.request.RemoveMemberRequest;
 import it.inaf.ia2.gms.model.request.TabRequest;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.service.GroupsService;
+import it.inaf.ia2.rap.data.RapUser;
 import java.util.Collections;
 import java.util.List;
 import javax.validation.Valid;
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/PermissionsController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/PermissionsController.java
index 9742396d930bb9bc279952e7c10f2661a78edbc0..84e3faf3a5063af96e787b1f33ecdacf13854066 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/PermissionsController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/PermissionsController.java
@@ -1,5 +1,6 @@
 package it.inaf.ia2.gms.controller;
 
+import it.inaf.ia2.gms.exception.BadRequestException;
 import it.inaf.ia2.gms.manager.GroupsManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
 import it.inaf.ia2.gms.model.request.AddPermissionRequest;
@@ -7,15 +8,20 @@ import it.inaf.ia2.gms.model.request.MemberRequest;
 import it.inaf.ia2.gms.model.response.PaginatedData;
 import it.inaf.ia2.gms.model.request.PaginatedModelRequest;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.UserPermission;
+import it.inaf.ia2.gms.model.RapUserPermission;
 import it.inaf.ia2.gms.model.request.TabRequest;
 import it.inaf.ia2.gms.model.request.UpdatePermissionRequest;
+import it.inaf.ia2.gms.model.response.UserPermission;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
+import it.inaf.ia2.gms.service.GroupNameService;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
 import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
@@ -39,10 +45,10 @@ public class PermissionsController {
     private PermissionsManager permissionsManager;
 
     @GetMapping(value = "/permissions", produces = MediaType.APPLICATION_JSON_VALUE)
-    public ResponseEntity<PaginatedData<UserPermission>> getPermissionsTab(TabRequest request) {
+    public ResponseEntity<PaginatedData<RapUserPermission>> getPermissionsTab(TabRequest request) {
 
         GroupEntity group = groupsManager.getGroupById(request.getGroupId());
-        PaginatedData<UserPermission> permissionsPanel = getPermissionsPanel(group, request);
+        PaginatedData<RapUserPermission> permissionsPanel = getPermissionsPanel(group, request);
 
         return ResponseEntity.ok(permissionsPanel);
     }
@@ -63,7 +69,7 @@ public class PermissionsController {
     }
 
     @PostMapping(value = "/permission", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
-    public ResponseEntity<PaginatedData<UserPermission>> addPermission(@Valid @RequestBody AddPermissionRequest request) {
+    public ResponseEntity<PaginatedData<RapUserPermission>> addPermission(@Valid @RequestBody AddPermissionRequest request) {
 
         GroupEntity group = groupsManager.getGroupById(request.getGroupId());
         if (request.isOverride()) {
@@ -88,7 +94,7 @@ public class PermissionsController {
     }
 
     @DeleteMapping(value = "/permission", produces = MediaType.APPLICATION_JSON_VALUE)
-    public ResponseEntity<PaginatedData<UserPermission>> deletePermission(@Valid MemberRequest request) {
+    public ResponseEntity<PaginatedData<RapUserPermission>> deletePermission(@Valid MemberRequest request) {
 
         GroupEntity group = groupsManager.getGroupById(request.getGroupId());
         permissionsManager.removePermission(group, request.getUserId());
@@ -96,8 +102,8 @@ public class PermissionsController {
         return ResponseEntity.ok(getPermissionsPanel(group, request));
     }
 
-    private PaginatedData<UserPermission> getPermissionsPanel(GroupEntity group, PaginatedModelRequest request) {
-        List<UserPermission> permissions = permissionsManager.getAllPermissions(group);
+    private PaginatedData<RapUserPermission> getPermissionsPanel(GroupEntity group, PaginatedModelRequest request) {
+        List<RapUserPermission> permissions = permissionsManager.getAllPermissions(group);
         Collections.sort(permissions, (p1, p2) -> {
             return p1.getUser().getDisplayName().compareTo(p2.getUser().getDisplayName());
         });
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/SearchController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/SearchController.java
index c612e9c4baba290bcdcd75c8641cc88da7c38b30..a7fcc04bd49d13e89763ad95920004261b1d32b6 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/SearchController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/SearchController.java
@@ -1,11 +1,10 @@
 package it.inaf.ia2.gms.controller;
 
-import it.inaf.ia2.gms.authn.SessionData;
-import it.inaf.ia2.gms.model.RapUser;
 import it.inaf.ia2.gms.model.response.PaginatedData;
 import it.inaf.ia2.gms.model.response.SearchResponseItem;
 import it.inaf.ia2.gms.model.response.UserSearchResponse;
 import it.inaf.ia2.gms.service.SearchService;
+import javax.servlet.http.HttpServletRequest;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
@@ -18,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
 public class SearchController {
 
     @Autowired
-    private SessionData sessionData;
+    private HttpServletRequest servletRequest;
 
     @Autowired
     private SearchService searchService;
@@ -27,14 +26,14 @@ public class SearchController {
     public ResponseEntity<PaginatedData<SearchResponseItem>> getSearchResults(@RequestParam("query") String query,
             @RequestParam("page") int page, @RequestParam("pageSize") int pageSize) {
 
-        PaginatedData<SearchResponseItem> response = searchService.search(query, sessionData.getUserId(), page, pageSize);
+        PaginatedData<SearchResponseItem> response = searchService.search(query, servletRequest.getUserPrincipal().getName(), page, pageSize);
         return ResponseEntity.ok(response);
     }
 
     @GetMapping(value = "/search/user/{userId}", produces = MediaType.APPLICATION_JSON_VALUE)
     public ResponseEntity<UserSearchResponse> getSearchResultUser(@PathVariable("userId") String userId) {
 
-        UserSearchResponse response = searchService.getUserSearchResult(sessionData.getUserId(), userId);
+        UserSearchResponse response = searchService.getUserSearchResult(servletRequest.getUserPrincipal().getName(), userId);
         return ResponseEntity.ok(response);
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/UsersController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/UsersController.java
index e5d908c265228008b552db7aa3efa6cfd0ee826e..5d96ed400e5379c49c8b7b45e8422c6c44fce729 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/UsersController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/UsersController.java
@@ -1,7 +1,7 @@
 package it.inaf.ia2.gms.controller;
 
-import it.inaf.ia2.gms.model.RapUser;
-import it.inaf.ia2.gms.rap.RapClient;
+import it.inaf.ia2.rap.client.RapClient;
+import it.inaf.ia2.rap.data.RapUser;
 import java.util.List;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
@@ -18,6 +18,6 @@ public class UsersController {
 
     @GetMapping(value = "users", produces = MediaType.APPLICATION_JSON_VALUE)
     public ResponseEntity<List<RapUser>> searchUsers(@RequestParam("search") String searchText) {
-        return ResponseEntity.ok(rapClient.searchUsers(searchText));
+        return ResponseEntity.ok(rapClient.getUsers(searchText));
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/GroupStatusManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/GroupStatusManager.java
index 7e756500e4488c73773af36f1f2b75875738eb12..f3792ca8d1a87e804212b7b8731c560b94251159 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/GroupStatusManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/GroupStatusManager.java
@@ -2,14 +2,13 @@ package it.inaf.ia2.gms.manager;
 
 import it.inaf.ia2.gms.exception.UnauthorizedException;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.RapUser;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.MembershipsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.MembershipEntity;
-import it.inaf.ia2.gms.rap.RapClient;
-import it.inaf.ia2.gms.service.GroupNameService;
 import it.inaf.ia2.gms.service.GroupsService;
+import it.inaf.ia2.rap.client.RapClient;
+import it.inaf.ia2.rap.data.RapUser;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -38,9 +37,6 @@ public class GroupStatusManager extends UserAwareComponent {
     @Autowired
     private MembershipsDAO membershipsDAO;
 
-    @Autowired
-    private GroupNameService groupNameService;
-
     @Autowired
     private RapClient rapClient;
 
@@ -50,14 +46,15 @@ public class GroupStatusManager extends UserAwareComponent {
 
         Permission groupPermission = permissionsManager.getCurrentUserPermission(parentGroup);
 
-        if (groupPermission != Permission.ADMIN) {
-            throw new UnauthorizedException("ADMIN permission is needed for performing this action");
+        if (!Permission.includes(groupPermission, Permission.VIEW_MEMBERS)) {
+            throw new UnauthorizedException("VIEW_MEMBERS permission is needed for performing this action");
         }
 
         List<GroupEntity> groups = groupsDAO.getAllChildren(parentGroup.getPath());
         groups.add(parentGroup);
 
-        List<String> names = groupNameService.getGroupsNames(groups);
+        Map<String, String> names = groupsDAO.getGroupCompleteNamesFromId(groups.stream()
+                .map(g -> g.getId()).collect(Collectors.toSet()));
 
         List<MembershipEntity> memberships = membershipsDAO.findByGroups(groups.stream()
                 .map(g -> g.getId()).collect(Collectors.toList()));
@@ -77,24 +74,26 @@ public class GroupStatusManager extends UserAwareComponent {
         Map<String, String> usersMap = new HashMap<>();
         for (RapUser user : rapClient.getUsers(memberships.stream()
                 .map(u -> u.getUserId()).collect(Collectors.toSet()))) {
-            usersMap.put(user.getId(), user.getPrimaryEmail());
+            usersMap.put(user.getId(), user.getPrimaryEmailAddress());
         }
 
         List<String[]> rows = new ArrayList<>();
 
         for (int i = 0; i < groups.size(); i++) {
             GroupEntity group = groups.get(i);
-            String groupName = names.get(i);
-            List<String> users = membersMap.get(group.getId());
-            if (users != null) {
-                for (String userId : users) {
-                    String email = usersMap.get(userId);
-                    if (email == null) {
-                        LOG.warn("Unable to retrieve information about user " + userId);
-                        continue;
+            String groupName = names.get(group.getId());
+            if (groupName != null) {
+                List<String> users = membersMap.get(group.getId());
+                if (users != null) {
+                    for (String userId : users) {
+                        String email = usersMap.get(userId);
+                        if (email == null) {
+                            LOG.warn("Unable to retrieve information about user " + userId);
+                            continue;
+                        }
+                        String[] row = new String[]{groupName, email};
+                        rows.add(row);
                     }
-                    String[] row = new String[]{groupName, email};
-                    rows.add(row);
                 }
             }
         }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
index dec9449f56626d78f3fdfd68f8a074f4d17a5192..7c9525d7e3ed319fb3c996df5e1c23ddc466369a 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
@@ -13,8 +13,8 @@ import it.inaf.ia2.gms.persistence.MembershipsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.InvitedRegistration;
 import it.inaf.ia2.gms.persistence.model.MembershipEntity;
-import it.inaf.ia2.gms.rap.RapClient;
 import it.inaf.ia2.gms.service.PermissionsService;
+import it.inaf.ia2.rap.client.RapClient;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -26,6 +26,7 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.UUID;
 import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -54,7 +55,7 @@ public class InvitedRegistrationManager extends UserAwareComponent {
     private RapClient rapClient;
 
     @Autowired
-    private SessionData sessionData;
+    private HttpServletRequest servletRequest;
 
     @Autowired
     private LoggingDAO loggingDAO;
@@ -104,7 +105,7 @@ public class InvitedRegistrationManager extends UserAwareComponent {
 
     public Optional<List<InvitedRegistration>> completeInvitedRegistrationIfNecessary() {
 
-        List<InvitedRegistration> invitedRegistrations = completeInvitedRegistrationIfNecessary(sessionData.getUserId());
+        List<InvitedRegistration> invitedRegistrations = completeInvitedRegistrationIfNecessary(servletRequest.getUserPrincipal().getName());
 
         InvitedRegistration invitedRegistrationFromToken = (InvitedRegistration) httpSession.getAttribute(INVITED_REGISTRATION);
 
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java
index 022e54205e9074e28e7782e39d800024bc57add7..83340a52446292d8dabfb169438254ade5a0cc47 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/MembershipManager.java
@@ -2,15 +2,15 @@ package it.inaf.ia2.gms.manager;
 
 import it.inaf.ia2.gms.exception.UnauthorizedException;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.RapUser;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.LoggingDAO;
 import it.inaf.ia2.gms.persistence.MembershipsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.MembershipEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
-import it.inaf.ia2.gms.rap.RapClient;
 import it.inaf.ia2.gms.service.PermissionUtils;
+import it.inaf.ia2.rap.client.RapClient;
+import it.inaf.ia2.rap.data.RapUser;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/PermissionsManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/PermissionsManager.java
index e6a655190684bb06718650b3ddce5c4ca99ef693..00a254533c3198b68fa9773ed5dc48218b9aba91 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/PermissionsManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/PermissionsManager.java
@@ -2,14 +2,14 @@ package it.inaf.ia2.gms.manager;
 
 import it.inaf.ia2.gms.exception.UnauthorizedException;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.RapUser;
-import it.inaf.ia2.gms.model.UserPermission;
+import it.inaf.ia2.gms.model.RapUserPermission;
 import it.inaf.ia2.gms.persistence.LoggingDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
-import it.inaf.ia2.gms.rap.RapClient;
 import it.inaf.ia2.gms.service.PermissionUtils;
 import it.inaf.ia2.gms.service.PermissionsService;
+import it.inaf.ia2.rap.client.RapClient;
+import it.inaf.ia2.rap.data.RapUser;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -34,7 +34,7 @@ public class PermissionsManager extends UserAwareComponent {
         this.loggingDAO = loggingDAO;
     }
 
-    public List<UserPermission> getAllPermissions(GroupEntity group) {
+    public List<RapUserPermission> getAllPermissions(GroupEntity group) {
 
         verifyUserCanManagePermissions(group);
 
@@ -47,12 +47,12 @@ public class PermissionsManager extends UserAwareComponent {
         Map<String, RapUser> users = rapClient.getUsers(userIdentifiers).stream()
                 .collect(Collectors.toMap(RapUser::getId, Function.identity()));
 
-        List<UserPermission> result = new ArrayList<>();
+        List<RapUserPermission> result = new ArrayList<>();
 
         for (PermissionEntity p : permissions) {
             RapUser rapUser = users.get(p.getUserId());
             if (rapUser != null) {
-                UserPermission permission = new UserPermission();
+                RapUserPermission permission = new RapUserPermission();
                 permission.setPermission(p.getPermission());
                 permission.setUser(rapUser);
                 result.add(permission);
@@ -117,6 +117,11 @@ public class PermissionsManager extends UserAwareComponent {
         }
     }
 
+    public List<PermissionEntity> findUserPermissions(GroupEntity group, String userId) {
+        verifyUserCanManagePermissions(group);
+        return permissionsService.findUserPermissions(group, userId);
+    }
+
     public Permission getUserPermission(GroupEntity group, String userId) {
         return getUserPermission(group, userId, true);
     }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/UserAwareComponent.java b/gms/src/main/java/it/inaf/ia2/gms/manager/UserAwareComponent.java
index f6af17ca33b29f359814e53043a3aa3eceb938d0..ad7dece5ef1060520d47968c7158f908b1afab76 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/UserAwareComponent.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/UserAwareComponent.java
@@ -1,23 +1,14 @@
 package it.inaf.ia2.gms.manager;
 
-import it.inaf.ia2.gms.authn.RapPrincipal;
-import it.inaf.ia2.gms.authn.SessionData;
 import javax.servlet.http.HttpServletRequest;
 import org.springframework.beans.factory.annotation.Autowired;
 
 public abstract class UserAwareComponent {
 
-    @Autowired(required = false)
-    private SessionData session;
-
     @Autowired
     private HttpServletRequest request;
 
     protected String getCurrentUserId() {
-        if (request.getUserPrincipal() != null && request.getUserPrincipal() instanceof RapPrincipal) {
-            return request.getUserPrincipal().getName();
-        } else {
-            return session.getUserId();
-        }
+        return request.getUserPrincipal().getName();
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/GroupPermission.java b/gms/src/main/java/it/inaf/ia2/gms/model/GroupPermission.java
new file mode 100644
index 0000000000000000000000000000000000000000..0846da98be18559f67eb2a676a8b327e846dc8a5
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/GroupPermission.java
@@ -0,0 +1,16 @@
+package it.inaf.ia2.gms.model;
+
+import it.inaf.ia2.gms.model.response.UserGroup;
+
+public class GroupPermission extends UserGroup {
+
+    private Permission permission;
+
+    public Permission getPermission() {
+        return permission;
+    }
+
+    public void setPermission(Permission permission) {
+        this.permission = permission;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/Identity.java b/gms/src/main/java/it/inaf/ia2/gms/model/Identity.java
deleted file mode 100644
index c879da9477a774321659fdb6e08ae145459b933e..0000000000000000000000000000000000000000
--- a/gms/src/main/java/it/inaf/ia2/gms/model/Identity.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package it.inaf.ia2.gms.model;
-
-public class Identity {
-
-    private IdentityType type;
-    private String typedId;
-    private String email;
-    private String name;
-    private String surname;
-    private boolean primary;
-
-    public IdentityType getType() {
-        return type;
-    }
-
-    public void setType(IdentityType type) {
-        this.type = type;
-    }
-
-    public String getTypedId() {
-        return typedId;
-    }
-
-    public void setTypedId(String typedId) {
-        this.typedId = typedId;
-    }
-
-    public String getEmail() {
-        return email;
-    }
-
-    public void setEmail(String email) {
-        this.email = email;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public String getSurname() {
-        return surname;
-    }
-
-    public void setSurname(String surname) {
-        this.surname = surname;
-    }
-
-    public boolean isPrimary() {
-        return primary;
-    }
-
-    public void setPrimary(boolean primary) {
-        this.primary = primary;
-    }
-}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/IdentityType.java b/gms/src/main/java/it/inaf/ia2/gms/model/IdentityType.java
deleted file mode 100644
index f5c80c80585b9905210f5cce4485279ab5b9f027..0000000000000000000000000000000000000000
--- a/gms/src/main/java/it/inaf/ia2/gms/model/IdentityType.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package it.inaf.ia2.gms.model;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonValue;
-import java.util.Arrays;
-
-public enum IdentityType {
-
-    EDU_GAIN("eduGAIN"),
-    X509("X.509"),
-    ORCID("OrcID"),
-    GOOGLE("Google"),
-    LINKEDIN("LinkedIn"),
-    FACEBOOK("Facebook"),
-    LOCAL_IDP("LocalIdP");
-
-    private final String value;
-
-    IdentityType(String value) {
-        this.value = value;
-    }
-
-    public String getValue() {
-        return value;
-    }
-
-    @JsonCreator
-    public static IdentityType forValue(String value) {
-        return Arrays.stream(IdentityType.values())
-                .filter(it -> value.equals(it.value)).findFirst()
-                .orElseThrow(() -> new IllegalArgumentException("Unsupported IdentityType " + value));
-    }
-
-    @JsonValue
-    public String toValue() {
-        return value;
-    }
-}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java b/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java
deleted file mode 100644
index 846b317d99017322f499d48a7b1bd8f5a8cdc31c..0000000000000000000000000000000000000000
--- a/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package it.inaf.ia2.gms.model;
-
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-public class RapUser {
-
-    private String id;
-    private List<Identity> identities;
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public List<Identity> getIdentities() {
-        return identities;
-    }
-
-    public void setIdentities(List<Identity> identities) {
-        this.identities = identities;
-    }
-
-    public String getDisplayName() {
-
-        String displayName = null;
-
-        // trying to extract name and surname
-        for (Identity identity : identities) {
-            if (identity.getName() != null && identity.getSurname() != null) {
-                displayName = String.format("%s %s", identity.getName(), identity.getSurname());
-                if (identity.isPrimary()) { // prefer always primary
-                    break;
-                }
-            }
-        }
-
-        if (displayName == null) { // No name and surname --> using primary email
-            displayName = getPrimaryEmail();
-        }
-
-        // Adding types
-        Set<String> types = identities.stream().map(i -> {
-            if (i.getType() == IdentityType.EDU_GAIN && i.getTypedId().endsWith("@ia2.inaf.it")) {
-                return "IA2";
-            }
-            return i.getType().getValue();
-        }).collect(Collectors.toSet());
-        displayName += String.format(" (%s)", String.join(", ", types));
-
-        return displayName;
-    }
-
-    public String getPrimaryEmail() {
-        Identity primaryIdentity = identities.stream().filter(i -> i.isPrimary()).findFirst()
-                .orElseThrow(() -> new IllegalStateException("No primary identity for user " + id));
-        return primaryIdentity.getEmail();
-    }
-}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/RapUserPermission.java b/gms/src/main/java/it/inaf/ia2/gms/model/RapUserPermission.java
new file mode 100644
index 0000000000000000000000000000000000000000..8417970e0bb3cb650b68d408e0132764e92d5c01
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/RapUserPermission.java
@@ -0,0 +1,25 @@
+package it.inaf.ia2.gms.model;
+
+import it.inaf.ia2.rap.data.RapUser;
+
+public class RapUserPermission {
+
+    private RapUser user;
+    private Permission permission;
+
+    public RapUser getUser() {
+        return user;
+    }
+
+    public void setUser(RapUser user) {
+        this.user = user;
+    }
+
+    public Permission getPermission() {
+        return permission;
+    }
+
+    public void setPermission(Permission permission) {
+        this.permission = permission;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/UserPermission.java b/gms/src/main/java/it/inaf/ia2/gms/model/UserPermission.java
index 66ab93a33f3bf02c4337ccd7b033c9ee91e94fdb..38b95068b9189546822c6e474af0407c6babec84 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/model/UserPermission.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/UserPermission.java
@@ -2,15 +2,15 @@ package it.inaf.ia2.gms.model;
 
 public class UserPermission {
 
-    private RapUser user;
+    private String userId;
     private Permission permission;
 
-    public RapUser getUser() {
-        return user;
+    public String getUserId() {
+        return userId;
     }
 
-    public void setUser(RapUser user) {
-        this.user = user;
+    public void setUserId(String userId) {
+        this.userId = userId;
     }
 
     public Permission getPermission() {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/response/UserSearchResponse.java b/gms/src/main/java/it/inaf/ia2/gms/model/response/UserSearchResponse.java
index 4d00c2489b6ff8f8c25f1aaf328e2b0aff2145b9..c3faa44f07338c9eddef86028af734a051dc1206 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/model/response/UserSearchResponse.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/response/UserSearchResponse.java
@@ -1,6 +1,6 @@
 package it.inaf.ia2.gms.model.response;
 
-import it.inaf.ia2.gms.model.RapUser;
+import it.inaf.ia2.rap.data.RapUser;
 import java.util.List;
 
 public class UserSearchResponse {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
index 3cbffa59790a17845926da0690266a4bf1e93048..abe3041c36bcd623a5c579ea29d16699d3d9838d 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
@@ -8,6 +8,7 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Types;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -159,6 +160,36 @@ public class GroupsDAO {
         });
     }
 
+    /**
+     * @param groupIds
+     * @return map having group id as keys and group complete name as values
+     */
+    public Map<String, String> getGroupCompleteNamesFromId(Set<String> groupIds) {
+
+        Map<String, String> result = new HashMap<>();
+
+        if (groupIds.isEmpty()) {
+            return result;
+        }
+
+        String sql = "SELECT id, complete_name FROM group_complete_name WHERE id IN ("
+                + String.join(",", Collections.nCopies(groupIds.size(), "?")) + ")";
+
+        jdbcTemplate.query(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            int i = 0;
+            for (String groupId : groupIds) {
+                ps.setString(++i, groupId);
+            }
+            return ps;
+        }, (rs, index) -> {
+            result.put(rs.getString("id"), rs.getString("complete_name"));
+            return null;
+        });
+
+        return result;
+    }
+
     public Optional<GroupEntity> findGroupByParentAndName(String parentPath, String childName) {
 
         String sql = "SELECT id, path, is_leaf, locked from gms_group WHERE name = ? AND path ~ ?";
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/LoggingDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/LoggingDAO.java
index cf7cf8d53fb68ad3d662d3cd51f1331528a53676..dc26fd488dede187917961d2a3ebad10d73551b0 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/LoggingDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/LoggingDAO.java
@@ -1,7 +1,5 @@
 package it.inaf.ia2.gms.persistence;
 
-import it.inaf.ia2.gms.authn.RapPrincipal;
-import it.inaf.ia2.gms.authn.SessionData;
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.sql.PreparedStatement;
@@ -9,7 +7,6 @@ import javax.servlet.http.HttpServletRequest;
 import javax.sql.DataSource;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.BeanCreationException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Component;
@@ -24,9 +21,6 @@ public class LoggingDAO {
     @Autowired(required = false)
     private HttpServletRequest request;
 
-    @Autowired(required = false)
-    private SessionData sessionData;
-
     @Autowired
     public LoggingDAO(DataSource dataSource) {
         jdbcTemplate = new JdbcTemplate(dataSource);
@@ -89,13 +83,8 @@ public class LoggingDAO {
     }
 
     private String getUser(HttpServletRequest request) {
-        if (request.getUserPrincipal() != null && request.getUserPrincipal() instanceof RapPrincipal) {
+        if (request != null && request.getUserPrincipal() != null) {
             return request.getUserPrincipal().getName();
-        } else if (request.getSession(false) != null) {
-            try {
-                return sessionData.getUserId();
-            } catch (BeanCreationException ex) {
-            }
         }
         return null;
     }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
index 48bc56e4ce55d914c0d7d76eb0874384c3bf5770..7191496a4b8903e0731e598fd3f0cf92a27f965f 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
@@ -81,6 +81,10 @@ public class PermissionsDAO {
         });
     }
 
+    /**
+     * Finds all direct user permissions for a given parent path (returns also
+     * all sub-groups permissions).
+     */
     public List<PermissionEntity> findUserPermissions(String userId, String path) {
 
         String sql = "SELECT group_id, permission, group_path FROM gms_permission WHERE user_id = ?\n"
diff --git a/gms/src/main/java/it/inaf/ia2/gms/rap/RapClient.java b/gms/src/main/java/it/inaf/ia2/gms/rap/RapClient.java
deleted file mode 100644
index 76cb9ba51274697087fbecb8b653a7b0f9b02c72..0000000000000000000000000000000000000000
--- a/gms/src/main/java/it/inaf/ia2/gms/rap/RapClient.java
+++ /dev/null
@@ -1,199 +0,0 @@
-package it.inaf.ia2.gms.rap;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import it.inaf.ia2.gms.authn.SessionData;
-import it.inaf.ia2.gms.model.RapUser;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Function;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpSession;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Component;
-import org.springframework.util.LinkedMultiValueMap;
-import org.springframework.util.MultiValueMap;
-import org.springframework.web.client.HttpClientErrorException;
-import org.springframework.web.client.HttpServerErrorException;
-import org.springframework.web.client.HttpStatusCodeException;
-import org.springframework.web.client.RestTemplate;
-
-@Component
-public class RapClient {
-
-    private static final Logger LOG = LoggerFactory.getLogger(RapClient.class);
-
-    @Value("${rap.ws-url}")
-    private String rapBaseUrl;
-
-    @Value("${security.oauth2.client.access-token-uri}")
-    private String accessTokenUri;
-
-    @Value("${security.oauth2.client.client-id}")
-    private String clientId;
-
-    @Value("${security.oauth2.client.client-secret}")
-    private String clientSecret;
-
-    @Value("${security.oauth2.client.scope}")
-    private String scope;
-
-    /* Use basic auth instead of JWT when asking for users 
-     * Needed for Franco's version. */
-    @Value("${rap.ws.basic-auth}")
-    private boolean basicAuth;
-
-    @Autowired
-    private HttpServletRequest request;
-
-    @Autowired(required = false)
-    private SessionData sessionData;
-
-    private final RestTemplate rapRestTemplate;
-
-    private final RestTemplate refreshTokenRestTemplate;
-
-    private final ObjectMapper objectMapper = new ObjectMapper();
-
-    @Autowired
-    public RapClient(RestTemplate rapRestTemplate) {
-        this.rapRestTemplate = rapRestTemplate;
-        this.refreshTokenRestTemplate = new RestTemplate();
-    }
-
-    public RapUser getUser(String userId) {
-
-        String url = rapBaseUrl + "/user/" + userId;
-
-        return httpCall(entity -> {
-            return rapRestTemplate.exchange(url, HttpMethod.GET, entity, new ParameterizedTypeReference<RapUser>() {
-            }).getBody();
-        });
-    }
-
-    public List<RapUser> getUsers(Set<String> identifiers) {
-
-        if (identifiers.isEmpty()) {
-            return new ArrayList<>();
-        }
-
-        String url = rapBaseUrl + "/user?identifiers=" + String.join(",", identifiers);
-
-        return httpCall(entity -> {
-            return rapRestTemplate.exchange(url, HttpMethod.GET, entity, new ParameterizedTypeReference<List<RapUser>>() {
-            }).getBody();
-        });
-    }
-
-    public List<RapUser> searchUsers(String searchText) {
-
-        if (searchText == null || searchText.trim().isEmpty()) {
-            return new ArrayList<>();
-        }
-
-        String url = rapBaseUrl + "/user?search=" + searchText;
-
-        return httpCall(entity -> {
-            return rapRestTemplate.exchange(url, HttpMethod.GET, entity, new ParameterizedTypeReference<List<RapUser>>() {
-            }).getBody();
-        });
-    }
-
-    private <R> R httpCall(Function<HttpEntity<?>, R> function) {
-        return httpCall(function, null);
-    }
-
-    private <R, T> R httpCall(Function<HttpEntity<?>, R> function, T body) {
-        try {
-            try {
-                return function.apply(getEntity(body));
-            } catch (HttpClientErrorException.Unauthorized ex) {
-                if (request.getSession(false) == null || sessionData.getExpiresIn() > 0) {
-                    // we can't refresh the token without a session
-                    throw ex;
-                }
-                refreshToken();
-                return function.apply(getEntity(body));
-            }
-        } catch (HttpStatusCodeException ex) {
-            try {
-                Map<String, String> map = objectMapper.readValue(ex.getResponseBodyAsString(), Map.class);
-                if (map.containsKey("error")) {
-                    String error = map.get("error");
-                    if (ex instanceof HttpClientErrorException) {
-                        throw new HttpClientErrorException(ex.getStatusCode(), error);
-                    } else if (ex instanceof HttpServerErrorException) {
-                        throw new HttpServerErrorException(ex.getStatusCode(), error);
-                    }
-                }
-            } catch (JsonProcessingException ignore) {
-            }
-            throw ex;
-        }
-    }
-
-    private <T> HttpEntity<T> getEntity(T body) {
-
-        HttpHeaders headers = new HttpHeaders();
-        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
-
-        if (basicAuth) { // Franco's version
-            String auth = clientId + ":" + clientSecret;
-            String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes());
-            headers.add("Authorization", "Basic " + encodedAuth);
-
-            HttpSession session = request.getSession(false);
-            if (session != null) {
-                String clientDb = (String) session.getAttribute("client_db");
-                if (clientDb != null) {
-                    headers.add("client_db", clientDb);
-                    LOG.debug("client_db=" + clientDb);
-                }
-            }
-        } else if (request.getSession(false) != null) {
-            headers.add("Authorization", "Bearer " + sessionData.getAccessToken());
-        } else {
-            // from JWT web service
-            headers.add("Authorization", request.getHeader("Authorization"));
-        }
-
-        return new HttpEntity<>(body, headers);
-    }
-
-    public void refreshToken() {
-
-        HttpHeaders headers = new HttpHeaders();
-        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
-        headers.setBasicAuth(clientId, clientSecret);
-
-        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
-
-        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
-        map.add("grant_type", "refresh_token");
-        map.add("refresh_token", sessionData.getRefreshToken());
-        map.add("scope", scope.replace(",", " "));
-
-        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
-
-        ResponseEntity<Map> response = refreshTokenRestTemplate.postForEntity(accessTokenUri, request, Map.class);
-
-        Map<String, Object> values = response.getBody();
-
-        sessionData.setAccessToken((String) values.get("access_token"));
-        sessionData.setRefreshToken((String) values.get("refresh_token"));
-        sessionData.setExpiresIn((int) values.get("expires_in"));
-    }
-}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java
index 5668fb21a63083f43acd0718666cb8f7d7a18242..98aaeadd5cf3beb79eba5d021a90334e72346e5b 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupNameService.java
@@ -1,15 +1,17 @@
 package it.inaf.ia2.gms.service;
 
+import it.inaf.ia2.gms.exception.BadRequestException;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
-import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -20,8 +22,12 @@ import org.springframework.stereotype.Service;
 @Service
 public class GroupNameService {
 
+    private final GroupsDAO groupsDAO;
+
     @Autowired
-    private GroupsDAO groupsDAO;
+    public GroupNameService(GroupsDAO groupsDAO) {
+        this.groupsDAO = groupsDAO;
+    }
 
     public List<String> getGroupsNamesFromIdentifiers(Set<String> groupIdentifiers) {
         return getGroupsNames(groupsDAO.findGroupsByIds(groupIdentifiers));
@@ -37,113 +43,113 @@ public class GroupNameService {
      */
     public List<String> getGroupsNames(List<GroupEntity> groups) {
 
-        // We need to return the complete group name, so it is necessary to load
-        // all the parents too.
-        Map<String, String> idNameMap = new HashMap<>();
-        Set<String> allIdentifiers = getAllIdentifiers(groups);
-        for (GroupEntity group : groupsDAO.findGroupsByIds(allIdentifiers)) {
-            idNameMap.put(group.getId(), group.getName());
-        }
+        Set<String> groupIds = groups.stream().map(g -> g.getId()).collect(Collectors.toSet());
 
-        List<String> names = new ArrayList<>();
-        for (GroupEntity group : groups) {
-            names.add(getGroupCompleteName(group, idNameMap));
-        }
-        return names;
-    }
+        List<String> names = new ArrayList<>(groupsDAO.getGroupCompleteNamesFromId(groupIds).values());
 
-    private Set<String> getAllIdentifiers(List<GroupEntity> groups) {
+        Collections.sort(names);
 
-        Set<String> allIdentifiers = new HashSet<>();
-        for (GroupEntity group : groups) {
-            if (!"".equals(group.getPath())) {
-                String[] ids = group.getPath().split("\\.");
-                for (String id : ids) {
-                    allIdentifiers.add(id);
-                }
-            }
-        }
-
-        return allIdentifiers;
+        return names;
     }
 
-    private String getGroupCompleteName(GroupEntity group, Map<String, String> idNameMap) {
+    /**
+     * @param groups
+     * @return map having group id as keys and group names as values
+     */
+    public Map<String, List<String>> getNames(Set<GroupEntity> groups) {
 
-        if ("ROOT".equals(group.getId())) {
-            return group.getName();
-        }
+        Set<String> groupIds = groups.stream()
+                .map(g -> g.getId()).collect(Collectors.toSet());
 
-        List<String> names = new ArrayList<>();
+        return getNamesFromIds(groupIds);
+    }
 
-        for (String groupId : group.getPath().split("\\.")) {
+    public Map<String, List<String>> getNamesFromIds(Set<String> groupIds) {
 
-            String groupName = idNameMap.get(groupId);
+        Map<String, List<String>> result = new HashMap<>();
 
-            // Dot inside names is considered a special character (because it is
-            // used to separate the group from its parents), so we use a
-            // backslash to escape it (client apps need to be aware of this).
-            groupName = groupName.replace("\\.", "\\\\.");
+        if (groupIds.contains("ROOT")) {
+            result.put("ROOT", Collections.singletonList(getRoot().getName()));
+        }
 
-            names.add(groupName);
+        for (Map.Entry<String, String> entry : groupsDAO.getGroupCompleteNamesFromId(groupIds).entrySet()) {
+            List<String> names = splitNames(entry.getValue());
+            result.put(entry.getKey(), names);
         }
 
-        return String.join(".", names);
+        return result;
     }
 
-    /**
-     * @param groupsIdPath map having group id as keys and group paths as values
-     * @return map having group id as keys and group names as values
-     */
-    public Map<String, List<String>> getNames(List<Map.Entry<String, String>> groupsIdPath) {
+    private List<String> splitNames(String completeGroupName) {
+        return Arrays.asList(completeGroupName.split("(?<!\\\\)\\."));
+    }
 
-        Set<String> allIdentifiers = new HashSet<>();
-        for (Map.Entry<String, String> entry : groupsIdPath) {
-            allIdentifiers.addAll(getIdentifiers(entry.getValue()));
+    public String getShortGroupName(String completeGroupName, Optional<String> groupPrefix) {
+        if (groupPrefix.isPresent()) {
+            return completeGroupName.substring(groupPrefix.get().length() + 1);
         }
+        return completeGroupName;
+    }
+
+    public GroupEntity getGroupFromNames(Optional<String> group) {
 
-        Map<String, String> groupSingleNamesMap = getGroupSingleNamesMap(allIdentifiers);
+        List<String> groupNames = extractGroupNames(group);
 
-        Map<String, List<String>> groupCompleteNamesMap = new HashMap<>();
-        for (Map.Entry<String, String> entry : groupsIdPath) {
-            List<String> groupCompleteName = getGroupCompleteName(groupSingleNamesMap, entry.getValue());
-            groupCompleteNamesMap.put(entry.getKey(), groupCompleteName);
+        if (groupNames.isEmpty()) {
+            return getRoot();
         }
+        return getGroupFromNamesAndIndex(groupNames, groupNames.size() - 1);
+    }
 
-        return groupCompleteNamesMap;
+    public GroupEntity getGroupFromNamesAndIndex(Optional<String> group, int index) {
+        List<String> groupNames = extractGroupNames(group);
+        return getGroupFromNamesAndIndex(groupNames, index);
     }
 
-    private Map<String, String> getGroupSingleNamesMap(Set<String> allIdentifiers) {
-        Map<String, String> groupNamesMap = new HashMap<>();
-        for (GroupEntity group : groupsDAO.findGroupsByIds(allIdentifiers)) {
-            groupNamesMap.put(group.getId(), group.getName());
+    private GroupEntity getGroupFromNamesAndIndex(List<String> groupNames, int index) {
+        String parentPath = ""; // starting from ROOT
+        GroupEntity group = null;
+        for (int i = 0; i < index + 1; i++) {
+            String groupName = groupNames.get(i);
+            group = groupsDAO.findGroupByParentAndName(parentPath, groupName)
+                    .orElseThrow(() -> new BadRequestException("Unable to find group " + groupName));
+            parentPath = group.getPath();
+        }
+        if (group == null) {
+            throw new IllegalStateException();
         }
+        return group;
+    }
 
-        return groupNamesMap;
+    private List<String> extractGroupNames(Optional<String> group) {
+        return extractGroupNames(group.orElse(null));
     }
 
-    private List<String> getGroupCompleteName(Map<String, String> groupNamesMap, String groupPath) {
+    public List<String> extractGroupNames(String groupStr) {
+
+        if (groupStr == null || groupStr.isEmpty()) {
+            return new ArrayList<>();
+        }
+
         List<String> names = new ArrayList<>();
-        if (groupPath.isEmpty()) {
-            names.add("Root");
-        } else {
-            List<String> identifiers = getIdentifiers(groupPath);
-            for (String groupId : identifiers) {
-                names.add(groupNamesMap.get(groupId));
+        String currentName = "";
+        for (int i = 0; i < groupStr.length(); i++) {
+            char c = groupStr.charAt(i);
+            // dot is the group separator and it must be escaped if used inside
+            // group names
+            if (c == '.' && groupStr.charAt(i - 1) != '\\') {
+                names.add(currentName.replace("\\.", "."));
+                currentName = "";
+            } else {
+                currentName += c;
             }
         }
+        names.add(currentName);
         return names;
     }
 
-    /**
-     * Returns the list of all identifiers including parent ones.
-     */
-    private List<String> getIdentifiers(String groupPath) {
-        List<String> identifiers = new ArrayList<>();
-        if (!groupPath.isEmpty()) {
-            for (String id : groupPath.split(Pattern.quote("."))) {
-                identifiers.add(id);
-            }
-        }
-        return identifiers;
+    private GroupEntity getRoot() {
+        return groupsDAO.findGroupById("ROOT")
+                .orElseThrow(() -> new IllegalStateException("Missing root group"));
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/SearchService.java b/gms/src/main/java/it/inaf/ia2/gms/service/SearchService.java
index 9f59907cc51db024182baba78611e6c58b902135..8ba7f3c95737f20403746e13fe26a0a68413d88d 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/SearchService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/SearchService.java
@@ -13,11 +13,11 @@ import it.inaf.ia2.gms.persistence.MembershipsDAO;
 import it.inaf.ia2.gms.persistence.PermissionsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
-import it.inaf.ia2.gms.rap.RapClient;
-import java.util.AbstractMap.SimpleEntry;
+import it.inaf.ia2.rap.client.RapClient;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -58,7 +58,7 @@ public class SearchService {
     }
 
     private List<SearchResponseItem> searchUsers(String query) {
-        return rapClient.searchUsers(query).stream()
+        return rapClient.getUsers(query).stream()
                 .map(u -> {
                     SearchResponseItem item = new SearchResponseItem();
                     item.setType(SearchResponseType.USER);
@@ -75,22 +75,16 @@ public class SearchService {
 
         // Select only the groups visible to the user
         List<PermissionEntity> permissions = permissionsDAO.findUserPermissions(userId);
-
-        List<Map.Entry<String, String>> groupsIdPath = new ArrayList<>();
-        for (GroupEntity group : allGroups) {
-            PermissionUtils.getGroupPermission(group, permissions).ifPresent(permission -> {
-                groupsIdPath.add(new SimpleEntry<>(group.getId(), group.getPath()));
-            });
-        }
+        Set<GroupEntity> visibleGroups = getVisibleGroups(allGroups, permissions);
 
         List<SearchResponseItem> items = new ArrayList<>();
-        Map<String, List<String>> groupNames = groupNameService.getNames(groupsIdPath);
-        for (Map.Entry<String, String> entry : groupsIdPath) {
-            String groupId = entry.getKey();
+        Map<String, List<String>> groupNames = groupNameService.getNames(visibleGroups);
+
+        for (GroupEntity group : visibleGroups) {
             SearchResponseItem item = new SearchResponseItem();
             item.setType(SearchResponseType.GROUP);
-            item.setId(groupId);
-            List<String> names = groupNames.get(groupId);
+            item.setId(group.getId());
+            List<String> names = groupNames.get(group.getId());
             item.setLabel(String.join(" / ", names));
             items.add(item);
         }
@@ -115,7 +109,7 @@ public class SearchService {
         sortByGroupCompleteName(groups);
         response.setGroups(groups);
 
-        List<UserPermission> permissions = getUserPermission(targetUserId, actorPermissions);
+        List<UserPermission> permissions = getUserPermission(groupsManager.getRoot(), targetUserId, actorPermissions);
         sortByGroupCompleteName(permissions);
         response.setPermissions(permissions);
 
@@ -129,15 +123,9 @@ public class SearchService {
         List<GroupEntity> allGroups = membershipsDAO.getUserMemberships(targetUserId);
 
         // Select only groups visible to the actor user
-        List<Map.Entry<String, String>> visibleGroupsIdPath = new ArrayList<>();
-        for (GroupEntity group : allGroups) {
-
-            PermissionUtils.getGroupPermission(group, actorPermissions).ifPresent(permission -> {
-                visibleGroupsIdPath.add(new SimpleEntry<>(group.getId(), group.getPath()));
-            });
-        }
+        Set<GroupEntity> visibleGroups = getVisibleGroups(allGroups, actorPermissions);
 
-        return groupNameService.getNames(visibleGroupsIdPath).entrySet().stream()
+        return groupNameService.getNames(visibleGroups).entrySet().stream()
                 .map(entry -> {
                     UserGroup ug = new UserGroup();
                     ug.setGroupId(entry.getKey());
@@ -147,24 +135,28 @@ public class SearchService {
                 .collect(Collectors.toList());
     }
 
-    public List<UserPermission> getUserPermission(String targetUserId, List<PermissionEntity> actorPermissions) {
+    private Set<GroupEntity> getVisibleGroups(List<GroupEntity> allGroups, List<PermissionEntity> permissions) {
+        return allGroups.stream()
+                .filter(g -> PermissionUtils.getGroupPermission(g, permissions).isPresent())
+                .collect(Collectors.toSet());
+    }
+
+    public List<UserPermission> getUserPermission(GroupEntity group, String targetUserId, List<PermissionEntity> actorPermissions) {
 
         List<UserPermission> permissions = new ArrayList<>();
 
         // Super-admin user is able to see also other user permissions
-        PermissionUtils.getGroupPermission(groupsManager.getRoot(), actorPermissions).ifPresent(permission -> {
+        PermissionUtils.getGroupPermission(group, actorPermissions).ifPresent(permission -> {
             if (permission.equals(Permission.ADMIN)) {
 
                 Map<String, PermissionEntity> targetUserPermissions
                         = permissionsDAO.findUserPermissions(targetUserId).stream()
                                 .collect(Collectors.toMap(PermissionEntity::getGroupId, p -> p));
 
-                List<Map.Entry<String, String>> groupsIdPath = new ArrayList<>();
-                for (PermissionEntity p : targetUserPermissions.values()) {
-                    groupsIdPath.add(new SimpleEntry<>(p.getGroupId(), p.getGroupPath()));
-                }
+                Set<String> groupIds = targetUserPermissions.values().stream()
+                        .map(p -> p.getGroupId()).collect(Collectors.toSet());
 
-                for (Map.Entry<String, List<String>> entry : groupNameService.getNames(groupsIdPath).entrySet()) {
+                for (Map.Entry<String, List<String>> entry : groupNameService.getNamesFromIds(groupIds).entrySet()) {
                     UserPermission up = new UserPermission();
                     up.setGroupId(entry.getKey());
                     up.setGroupCompleteName(entry.getValue());
diff --git a/gms/src/main/resources/auth.properties b/gms/src/main/resources/auth.properties
index 7bd1b03a1fa98b51489556599bb2898e8ebf3297..ff3fda0e610e1b8891c06aedc2d3d2ef3712f22e 100644
--- a/gms/src/main/resources/auth.properties
+++ b/gms/src/main/resources/auth.properties
@@ -1,5 +1,7 @@
 client_id=gms
 client_secret=gms-secret
+rap_uri=http://localhost/rap-ia2
+jwks_endpoint=/auth/oidc/jwks
 access_token_uri=http://localhost/rap-ia2/auth/oauth2/token
 user_authorization_uri=http://localhost/rap-ia2/auth/oauth2/authorize
 check_token_uri=http://localhost/rap-ia2/auth/oauth2/token
diff --git a/gms/src/main/resources/sql/init.sql b/gms/src/main/resources/sql/init.sql
index 05aec53ca465b3b7fe8241685973524e74e2daae..41360a8b799a960fd6a51a32a6a534effbe99eeb 100644
--- a/gms/src/main/resources/sql/init.sql
+++ b/gms/src/main/resources/sql/init.sql
@@ -63,3 +63,18 @@ CREATE TABLE invited_registration_request_group (
   FOREIGN KEY (request_id) REFERENCES invited_registration_request(id),
   FOREIGN KEY (group_id) REFERENCES gms_group(id)
 );
+
+CREATE VIEW group_complete_name AS
+SELECT id, string_agg(name, '.') AS complete_name
+FROM (
+    SELECT replace(name, '.', '\.') AS name, p.id
+    FROM gms_group g
+    JOIN (
+        SELECT UNNEST(string_to_array(path::varchar, '.')) AS rel_id, id
+        FROM gms_group
+    ) AS p ON g.id = p.rel_id
+    ORDER BY p.id, nlevel(g.path)
+) AS j GROUP BY id
+UNION
+SELECT id, name AS complete_name FROM gms_group WHERE id = 'ROOT'
+ORDER BY complete_name;
diff --git a/gms/src/test/java/it/inaf/ia2/gms/GmsTestUtils.java b/gms/src/test/java/it/inaf/ia2/gms/GmsTestUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed79a9b487d01f080584b3eca67379d05dba4e4c
--- /dev/null
+++ b/gms/src/test/java/it/inaf/ia2/gms/GmsTestUtils.java
@@ -0,0 +1,19 @@
+package it.inaf.ia2.gms;
+
+import java.security.Principal;
+import javax.servlet.http.HttpServletRequest;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class GmsTestUtils {
+
+    public static void mockPrincipal(HttpServletRequest mockedServletRequest) {
+        mockPrincipal(mockedServletRequest, "admin_id");
+    }
+
+    public static void mockPrincipal(HttpServletRequest mockedServletRequest, String userId) {
+        Principal principal = mock(Principal.class);
+        when(principal.getName()).thenReturn(userId);
+        when(mockedServletRequest.getUserPrincipal()).thenReturn(principal);
+    }
+}
diff --git a/gms/src/test/java/it/inaf/ia2/gms/authn/ClientDbFilterTest.java b/gms/src/test/java/it/inaf/ia2/gms/authn/ClientDbFilterTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..171d0508dcdaf2091c13a57cca56c91ddb2242f0
--- /dev/null
+++ b/gms/src/test/java/it/inaf/ia2/gms/authn/ClientDbFilterTest.java
@@ -0,0 +1,46 @@
+package it.inaf.ia2.gms.authn;
+
+import it.inaf.ia2.aa.AuthConfig;
+import it.inaf.ia2.rap.client.RapClient;
+import java.net.URI;
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import static org.mockito.ArgumentMatchers.eq;
+import org.mockito.Mock;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ClientDbFilterTest {
+
+    @Mock
+    private HttpServletRequest request;
+
+    @Mock
+    private AuthConfig authConfig;
+
+    @Mock
+    private RapClient rapClient;
+
+    private ClientDbFilter filter;
+
+    @Test
+    public void testJwksUriOverride() throws Exception {
+
+        when(authConfig.getRapBaseUri()).thenReturn("http://ia2.inaf.it");
+        when(authConfig.getJwksEndpoint()).thenReturn("/jwks?client_name=db0");
+        when(request.getSession()).thenReturn(mock(HttpSession.class));
+        when(request.getParameter(eq("client_db"))).thenReturn("other_db");
+
+        filter = new ClientDbFilter(authConfig, rapClient);
+        filter.doFilter(request, mock(HttpServletResponse.class), mock(FilterChain.class));
+
+        verify(rapClient).addJwksUri(eq(URI.create("http://ia2.inaf.it/jwks?client_name=other_db")));
+    }
+}
diff --git a/gms/src/test/java/it/inaf/ia2/gms/authn/SessionDataTest.java b/gms/src/test/java/it/inaf/ia2/gms/authn/SessionDataTest.java
index 00648021317db5c3704b58e56feb509e7072f644..75d44989f48e75cce0fd7d7c62b8079c91acd6e7 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/authn/SessionDataTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/authn/SessionDataTest.java
@@ -1,6 +1,7 @@
 package it.inaf.ia2.gms.authn;
 
 import it.inaf.ia2.aa.data.User;
+import it.inaf.ia2.rap.client.RapClient;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 import static org.junit.Assert.assertTrue;
@@ -18,6 +19,9 @@ public class SessionDataTest {
 
     @Mock
     private HttpServletRequest request;
+    
+    @Mock
+    private RapClient rapClient;
 
     @InjectMocks
     private SessionData sessionData;
diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsControllerTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsControllerTest.java
index f09287e45d31828aad7464f635384ecc9eca5fab..cdbdb8b42f3ce19f906ebcb99d35b604c01a58a2 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsControllerTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsControllerTest.java
@@ -1,7 +1,7 @@
 package it.inaf.ia2.gms.controller;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import it.inaf.ia2.gms.authn.SessionData;
+import it.inaf.ia2.gms.GmsTestUtils;
 import it.inaf.ia2.gms.manager.GroupsManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
 import it.inaf.ia2.gms.model.GroupNode;
@@ -14,6 +14,7 @@ import it.inaf.ia2.gms.service.GroupsService;
 import it.inaf.ia2.gms.service.GroupsTreeBuilder;
 import java.util.ArrayList;
 import java.util.List;
+import javax.servlet.http.HttpServletRequest;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.notNullValue;
 import org.junit.Before;
@@ -50,7 +51,7 @@ public class GroupsControllerTest {
     private GroupsService groupsService;
 
     @Mock
-    private SessionData session;
+    private HttpServletRequest servletRequest;
 
     @Mock
     private PermissionsManager permissionsManager;
@@ -71,6 +72,7 @@ public class GroupsControllerTest {
     @Before
     public void init() {
         mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+        GmsTestUtils.mockPrincipal(servletRequest);
     }
 
     @Test
@@ -104,8 +106,6 @@ public class GroupsControllerTest {
         PaginatedData<GroupNode> paginatedData = new PaginatedData<>(nodes, 1, 10);
         when(groupsTreeBuilder.listSubGroups(any(), any(), any())).thenReturn(paginatedData);
 
-        when(session.getUserId()).thenReturn("admin_id");
-
         mockMvc.perform(post("/group")
                 .content(mapper.writeValueAsString(request))
                 .contentType(MediaType.APPLICATION_JSON))
diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java
index 937fbe74b33dd9e0756099bcb62d47f14a00ffb1..07f1e8577751f8a8c61340f530a971457bb3b269 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java
@@ -1,6 +1,6 @@
 package it.inaf.ia2.gms.controller;
 
-import it.inaf.ia2.gms.authn.SessionData;
+import it.inaf.ia2.gms.GmsTestUtils;
 import it.inaf.ia2.gms.manager.GroupsManager;
 import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
@@ -13,6 +13,7 @@ import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.service.GroupsService;
 import it.inaf.ia2.gms.service.GroupsTreeBuilder;
 import java.util.ArrayList;
+import javax.servlet.http.HttpServletRequest;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import org.junit.Test;
@@ -28,7 +29,7 @@ import org.mockito.junit.MockitoJUnitRunner;
 public class GroupsTabResponseBuilderTest {
 
     @Mock
-    private SessionData session;
+    private HttpServletRequest servletRequest;
 
     @Mock
     private GroupsManager groupsManager;
@@ -51,7 +52,7 @@ public class GroupsTabResponseBuilderTest {
     @Test
     public void testGetGroupsTab() {
 
-        when(session.getUserId()).thenReturn("admin_id");
+        GmsTestUtils.mockPrincipal(servletRequest);
 
         GroupEntity root = new GroupEntity();
         root.setId("ROOT");
diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/JWTWebServiceControllerTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/JWTWebServiceControllerTest.java
index 625e72c14dc3f3feb6113d24a72e2a309fe791c4..1a086e3dcf0112b6437841e576ef81227f0e0393 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/controller/JWTWebServiceControllerTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/controller/JWTWebServiceControllerTest.java
@@ -4,15 +4,16 @@ import it.inaf.ia2.gms.manager.GroupsManager;
 import it.inaf.ia2.gms.manager.MembershipManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.RapUser;
-import it.inaf.ia2.gms.model.UserPermission;
+import it.inaf.ia2.gms.model.RapUserPermission;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.PermissionsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.MembershipEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
+import it.inaf.ia2.gms.service.GroupNameService;
 import it.inaf.ia2.gms.service.GroupsService;
 import it.inaf.ia2.gms.service.JoinService;
+import it.inaf.ia2.rap.data.RapUser;
 import java.security.Principal;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -81,6 +82,7 @@ public class JWTWebServiceControllerTest {
 
     @Before
     public void init() {
+        controller.groupNameService = new GroupNameService(groupsDAO);
         mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
         root = getRoot();
         lbt = getLbtGroup();
@@ -193,8 +195,8 @@ public class JWTWebServiceControllerTest {
         when(groupsDAO.findGroupByParentAndName("", "LBT")).thenReturn(Optional.of(lbt));
         when(groupsDAO.findGroupByParentAndName("lbt_id", "INAF")).thenReturn(Optional.of(inaf));
 
-        List<UserPermission> permissions = new ArrayList<>();
-        UserPermission up = new UserPermission();
+        List<RapUserPermission> permissions = new ArrayList<>();
+        RapUserPermission up = new RapUserPermission();
         up.setUser(getRapUser());
         up.setPermission(Permission.ADMIN);
         permissions.add(up);
diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/SearchControllerTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/SearchControllerTest.java
index f419eba57f56c1c58302ffd750434f5eaf4bd39b..bfe0b1a1bb7b81ff65c4010b430779c011d2d1f5 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/controller/SearchControllerTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/controller/SearchControllerTest.java
@@ -1,12 +1,13 @@
 package it.inaf.ia2.gms.controller;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import it.inaf.ia2.gms.authn.SessionData;
+import it.inaf.ia2.gms.GmsTestUtils;
 import it.inaf.ia2.gms.model.response.PaginatedData;
 import it.inaf.ia2.gms.model.response.SearchResponseItem;
 import it.inaf.ia2.gms.model.response.UserSearchResponse;
 import it.inaf.ia2.gms.service.SearchService;
 import java.util.ArrayList;
+import javax.servlet.http.HttpServletRequest;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -29,7 +30,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 public class SearchControllerTest {
 
     @Mock
-    private SessionData session;
+    private HttpServletRequest servletRequest;
 
     @Mock
     private SearchService searchService;
@@ -44,8 +45,7 @@ public class SearchControllerTest {
     @Before
     public void init() {
         mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
-
-        when(session.getUserId()).thenReturn("admin_id");
+        GmsTestUtils.mockPrincipal(servletRequest);
     }
 
     @Test
@@ -56,7 +56,7 @@ public class SearchControllerTest {
         when(searchService.search(any(), any(), anyInt(), anyInt())).thenReturn(response);
 
         mockMvc.perform(get("/search?query=searchText&page=1&pageSize=10")
-                .contentType(MediaType.APPLICATION_JSON_UTF8))
+                .contentType(MediaType.APPLICATION_JSON_VALUE))
                 .andExpect(status().isOk());
 
         verify(searchService, times(1)).search(eq("searchText"), eq("admin_id"), eq(1), eq(10));
@@ -68,7 +68,7 @@ public class SearchControllerTest {
         when(searchService.getUserSearchResult(any(), any())).thenReturn(new UserSearchResponse());
 
         mockMvc.perform(get("/search/user/user_id")
-                .contentType(MediaType.APPLICATION_JSON_UTF8))
+                .contentType(MediaType.APPLICATION_JSON_VALUE))
                 .andExpect(status().isOk());
 
         verify(searchService, times(1)).getUserSearchResult(eq("admin_id"), eq("user_id"));
diff --git a/gms/src/test/java/it/inaf/ia2/gms/manager/InvitedRegistrationManagerTest.java b/gms/src/test/java/it/inaf/ia2/gms/manager/InvitedRegistrationManagerTest.java
index 13cf87d37e771480d2d06898e3cbcf7e356f08fa..4ce897a21f44e4e3c047087336188913ba198e60 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/manager/InvitedRegistrationManagerTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/manager/InvitedRegistrationManagerTest.java
@@ -1,24 +1,25 @@
 package it.inaf.ia2.gms.manager;
 
-import it.inaf.ia2.gms.authn.SessionData;
-import it.inaf.ia2.gms.model.Identity;
-import it.inaf.ia2.gms.model.IdentityType;
+import it.inaf.ia2.gms.GmsTestUtils;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.RapUser;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.InvitedRegistrationDAO;
 import it.inaf.ia2.gms.persistence.LoggingDAO;
 import it.inaf.ia2.gms.persistence.MembershipsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.InvitedRegistration;
-import it.inaf.ia2.gms.rap.RapClient;
 import it.inaf.ia2.gms.service.PermissionsService;
+import it.inaf.ia2.rap.client.RapClient;
+import it.inaf.ia2.rap.data.Identity;
+import it.inaf.ia2.rap.data.IdentityType;
+import it.inaf.ia2.rap.data.RapUser;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpSession;
 import org.junit.Before;
 import org.junit.Test;
@@ -51,7 +52,7 @@ public class InvitedRegistrationManagerTest {
     @Mock
     private RapClient rapClient;
     @Mock
-    private SessionData sessionData;
+    private HttpServletRequest servletRequest;
     @Mock
     private LoggingDAO loggingDAO;
     @Mock
@@ -95,14 +96,14 @@ public class InvitedRegistrationManagerTest {
 
         when(httpSession.getAttribute(eq("invited-registration"))).thenReturn(regFromToken);
 
-        when(sessionData.getUserId()).thenReturn(USER_ID);
+        GmsTestUtils.mockPrincipal(servletRequest, USER_ID);
 
         RapUser user = new RapUser();
         user.setId(USER_ID);
         Identity identity = new Identity();
         identity.setType(IdentityType.EDU_GAIN);
         identity.setEmail(EMAIL);
-        user.setIdentities(Collections.singletonList(identity));
+        user.getIdentities().addAll(Collections.singletonList(identity));
 
         when(rapClient.getUser(eq(USER_ID))).thenReturn(user);
 
@@ -145,7 +146,7 @@ public class InvitedRegistrationManagerTest {
 
         when(httpSession.getAttribute(eq("invited-registration"))).thenReturn(regFromToken);
 
-        when(sessionData.getUserId()).thenReturn(USER_ID);
+        GmsTestUtils.mockPrincipal(servletRequest, USER_ID);
 
         RapUser user = new RapUser();
         user.setId(USER_ID);
diff --git a/gms/src/test/java/it/inaf/ia2/gms/manager/PermissionsManagerIntegrationTest.java b/gms/src/test/java/it/inaf/ia2/gms/manager/PermissionsManagerIntegrationTest.java
index 9a25bc6eaf540ce6f9465fdfe44374f014eb56cd..b0b73601f1e7719528b9e88980b50f1815aa539b 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/manager/PermissionsManagerIntegrationTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/manager/PermissionsManagerIntegrationTest.java
@@ -2,17 +2,18 @@ package it.inaf.ia2.gms.manager;
 
 import it.inaf.ia2.gms.DataSourceConfig;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.RapUser;
-import it.inaf.ia2.gms.model.UserPermission;
+import it.inaf.ia2.gms.model.RapUserPermission;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.LoggingDAO;
 import it.inaf.ia2.gms.persistence.PermissionsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
-import it.inaf.ia2.gms.rap.RapClient;
 import it.inaf.ia2.gms.service.PermissionsService;
+import it.inaf.ia2.rap.client.RapClient;
+import it.inaf.ia2.rap.data.RapUser;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import javax.servlet.http.HttpServletRequest;
 import javax.sql.DataSource;
 import static org.junit.Assert.assertEquals;
@@ -52,7 +53,7 @@ public class PermissionsManagerIntegrationTest {
         // Mock RAP client
         RapUser rapUser = new RapUser();
         rapUser.setId(USER_ID);
-        when(rapClient.getUsers(any())).thenReturn(Collections.singletonList(rapUser));
+        when(rapClient.getUsers(any(Set.class))).thenReturn(Collections.singletonList(rapUser));
 
         PermissionsService permissionsService = new PermissionsService(permissionsDAO, loggingDAO);
         PermissionsManager permissionsManager = new PermissionsManager(permissionsService, rapClient, loggingDAO);
@@ -61,7 +62,7 @@ public class PermissionsManagerIntegrationTest {
         // Create root
         GroupEntity root = new GroupEntity();
         root.setId("ROOT");
-        root.setName("Root");
+        root.setName("ROOT");
         root.setPath("");
         root = groupsDAO.createGroup(root);
 
@@ -72,7 +73,7 @@ public class PermissionsManagerIntegrationTest {
         superAdminPermission.setGroupPath(root.getPath());
         permissionsDAO.createOrUpdatePermission(superAdminPermission);
 
-        List<UserPermission> permissions = permissionsManager.getAllPermissions(root);
+        List<RapUserPermission> permissions = permissionsManager.getAllPermissions(root);
 
         assertEquals(1, permissions.size());
         assertEquals(Permission.ADMIN, permissions.get(0).getPermission());
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
index fd56e8a6302bab237d6d0ffb69930f77e8a59dd7..17fb805a987694fb50bcd2fda50c433a4d3b0839 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/GroupsDAOTest.java
@@ -5,9 +5,11 @@ import it.inaf.ia2.gms.HooksConfig;
 import it.inaf.ia2.gms.model.GroupBreadcrumb;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.service.hook.GroupsHook;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.UUID;
 import javax.sql.DataSource;
 import static org.junit.Assert.assertEquals;
@@ -114,6 +116,15 @@ public class GroupsDAOTest {
         assertTrue(optGroup.isPresent());
         assertEquals(lbtInaf.getId(), optGroup.get().getId());
 
+        // Complete names
+        Set<String> groupIds = new HashSet<>();
+        groupIds.add(groups.get(0).getId());
+        groupIds.add(lbt.getId());
+        Map<String, String> completeGroupNames = dao.getGroupCompleteNamesFromId(groupIds);
+        assertEquals(2, completeGroupNames.size());
+        assertEquals("LBT", completeGroupNames.get(lbt.getId()));
+        assertEquals("LBT.INAF", completeGroupNames.get(groups.get(0).getId()));
+
         // Children map
         Map<String, Boolean> childrenMap = dao.getHasChildrenMap(Sets.newSet(root.getId()));
         assertEquals(1, childrenMap.size());
@@ -151,4 +162,9 @@ public class GroupsDAOTest {
     private String getNewGroupId() {
         return UUID.randomUUID().toString().replaceAll("-", "");
     }
+
+    @Test
+    public void testGroupCompleteNamesEmptyInput() {
+        assertTrue(dao.getGroupCompleteNamesFromId(new HashSet<>()).isEmpty());
+    }
 }
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/NestedGroupsIntegrationTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/NestedGroupsIntegrationTest.java
index baa08c183dd0e426f696857d3d8320d28c79a5fe..73d7d9985bb3dc0541b0c7aecc51d4e96c13b304 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/persistence/NestedGroupsIntegrationTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/NestedGroupsIntegrationTest.java
@@ -10,9 +10,9 @@ import it.inaf.ia2.gms.model.Permission;
 import it.inaf.ia2.gms.model.request.GroupsRequest;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
-import it.inaf.ia2.gms.rap.RapClient;
 import it.inaf.ia2.gms.service.GroupsTreeBuilder;
 import it.inaf.ia2.gms.service.PermissionsService;
+import it.inaf.ia2.rap.client.RapClient;
 import java.util.List;
 import javax.sql.DataSource;
 import static org.junit.Assert.assertEquals;
diff --git a/gms/src/test/java/it/inaf/ia2/gms/rap/RapClientTest.java b/gms/src/test/java/it/inaf/ia2/gms/rap/RapClientTest.java
index bd317c09d73c7def90f287ede0cf2c05ab08c584..09becbfc53457dea0335e75665a11b0003ac6cb0 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/rap/RapClientTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/rap/RapClientTest.java
@@ -1,7 +1,6 @@
 package it.inaf.ia2.gms.rap;
 
 import it.inaf.ia2.gms.authn.SessionData;
-import it.inaf.ia2.gms.model.RapUser;
 import java.nio.charset.StandardCharsets;
 import java.util.HashMap;
 import java.util.Map;
@@ -34,120 +33,120 @@ import org.springframework.web.client.HttpServerErrorException;
 import org.springframework.web.client.HttpServerErrorException.InternalServerError;
 import org.springframework.web.client.RestTemplate;
 
-@RunWith(MockitoJUnitRunner.class)
+//@RunWith(MockitoJUnitRunner.class)
 public class RapClientTest {
 
-    @Mock
-    private HttpServletRequest request;
-
-    @Mock
-    private SessionData sessionData;
-
-    @Mock
-    private RestTemplate restTemplate;
-
-    @Mock
-    private RestTemplate refreshTokenRestTemplate;
-
-    private RapClient rapClient;
-
-    @Before
-    public void init() {
-        rapClient = new RapClient(restTemplate);
-        ReflectionTestUtils.setField(rapClient, "request", request);
-        ReflectionTestUtils.setField(rapClient, "refreshTokenRestTemplate", refreshTokenRestTemplate);
-        ReflectionTestUtils.setField(rapClient, "scope", "openid");
-    }
-
-    @Test
-    public void testUnauthorizedNoRefreshJsonMsg() {
-
-        String jsonError = "{\"error\":\"Unauthorized: foo\"}";
-
-        HttpClientErrorException exception = Unauthorized
-                .create(HttpStatus.UNAUTHORIZED, "401", HttpHeaders.EMPTY, jsonError.getBytes(), StandardCharsets.UTF_8);
-
-        when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(new ParameterizedTypeReference<RapUser>() {
-        }))).thenThrow(exception);
-
-        try {
-            rapClient.getUser("123");
-        } catch (HttpClientErrorException ex) {
-            assertEquals("401 Unauthorized: foo", ex.getMessage());
-        }
-    }
-
-    @Test
-    public void testUnauthorizedNoRefreshNotJsonMsg() {
-
-        String errorMessage = "THIS IS NOT A JSON";
-
-        HttpClientErrorException exception = Unauthorized
-                .create(HttpStatus.UNAUTHORIZED, "401", HttpHeaders.EMPTY, errorMessage.getBytes(), StandardCharsets.UTF_8);
-
-        when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(new ParameterizedTypeReference<RapUser>() {
-        }))).thenThrow(exception);
-
-        try {
-            rapClient.getUser("123");
-        } catch (HttpClientErrorException ex) {
-            assertNotNull(ex.getMessage());
-        }
-    }
-
-    @Test
-    public void testServerErrorJsonMsg() {
-
-        String jsonError = "{\"error\":\"Fatal error\"}";
-
-        HttpServerErrorException exception = InternalServerError
-                .create(HttpStatus.INTERNAL_SERVER_ERROR, "500", HttpHeaders.EMPTY, jsonError.getBytes(), StandardCharsets.UTF_8);
-
-        when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(new ParameterizedTypeReference<RapUser>() {
-        }))).thenThrow(exception);
-
-        try {
-            rapClient.getUser("123");
-        } catch (HttpServerErrorException ex) {
-            assertEquals("500 Fatal error", ex.getMessage());
-        }
-    }
-
-    @Test
-    public void testRefreshToken() {
-
-        when(request.getSession(eq(false))).thenReturn(mock(HttpSession.class));
-        when(sessionData.getExpiresIn()).thenReturn(-100l);
-
-        ReflectionTestUtils.setField(rapClient, "sessionData", sessionData);
-        ReflectionTestUtils.setField(rapClient, "clientId", "clientId");
-        ReflectionTestUtils.setField(rapClient, "clientSecret", "clientSecret");
-        ReflectionTestUtils.setField(rapClient, "accessTokenUri", "https://sso.ia2.inaf.it");
-
-        String jsonError = "{\"error\":\"Unauthorized: token expired\"}";
-
-        HttpClientErrorException exception = Unauthorized
-                .create(HttpStatus.UNAUTHORIZED, "401", HttpHeaders.EMPTY, jsonError.getBytes(), StandardCharsets.UTF_8);
-
-        when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(new ParameterizedTypeReference<RapUser>() {
-        }))).thenThrow(exception)
-                .thenReturn(ResponseEntity.ok(new RapUser()));
-
-        ResponseEntity refreshTokenResponse = mock(ResponseEntity.class);
-        Map<String, Object> mockedBody = new HashMap<>();
-        mockedBody.put("access_token", "<access_token>");
-        mockedBody.put("refresh_token", "<refresh_token>");
-        mockedBody.put("expires_in", 3600);
-        when(refreshTokenResponse.getBody()).thenReturn(mockedBody);
-
-        when(refreshTokenRestTemplate.postForEntity(anyString(), any(HttpEntity.class), any()))
-                .thenReturn(refreshTokenResponse);
-
-        RapUser user = rapClient.getUser("123");
-        assertNotNull(user);
-
-        // verifies that token is refreshed
-        verify(sessionData, times(1)).setAccessToken(eq("<access_token>"));
-        verify(sessionData, times(1)).setExpiresIn(eq(3600l));
-    }
+//    @Mock
+//    private HttpServletRequest request;
+//
+//    @Mock
+//    private SessionData sessionData;
+//
+//    @Mock
+//    private RestTemplate restTemplate;
+//
+//    @Mock
+//    private RestTemplate refreshTokenRestTemplate;
+//
+//    private RapClient rapClient;
+//
+//    @Before
+//    public void init() {
+//        rapClient = new RapClient(restTemplate);
+//        ReflectionTestUtils.setField(rapClient, "request", request);
+//        ReflectionTestUtils.setField(rapClient, "refreshTokenRestTemplate", refreshTokenRestTemplate);
+//        ReflectionTestUtils.setField(rapClient, "scope", "openid");
+//    }
+//
+//    @Test
+//    public void testUnauthorizedNoRefreshJsonMsg() {
+//
+//        String jsonError = "{\"error\":\"Unauthorized: foo\"}";
+//
+//        HttpClientErrorException exception = Unauthorized
+//                .create(HttpStatus.UNAUTHORIZED, "401", HttpHeaders.EMPTY, jsonError.getBytes(), StandardCharsets.UTF_8);
+//
+//        when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(new ParameterizedTypeReference<RapUser>() {
+//        }))).thenThrow(exception);
+//
+//        try {
+//            rapClient.getUser("123");
+//        } catch (HttpClientErrorException ex) {
+//            assertEquals("401 Unauthorized: foo", ex.getMessage());
+//        }
+//    }
+//
+//    @Test
+//    public void testUnauthorizedNoRefreshNotJsonMsg() {
+//
+//        String errorMessage = "THIS IS NOT A JSON";
+//
+//        HttpClientErrorException exception = Unauthorized
+//                .create(HttpStatus.UNAUTHORIZED, "401", HttpHeaders.EMPTY, errorMessage.getBytes(), StandardCharsets.UTF_8);
+//
+//        when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(new ParameterizedTypeReference<RapUser>() {
+//        }))).thenThrow(exception);
+//
+//        try {
+//            rapClient.getUser("123");
+//        } catch (HttpClientErrorException ex) {
+//            assertNotNull(ex.getMessage());
+//        }
+//    }
+//
+//    @Test
+//    public void testServerErrorJsonMsg() {
+//
+//        String jsonError = "{\"error\":\"Fatal error\"}";
+//
+//        HttpServerErrorException exception = InternalServerError
+//                .create(HttpStatus.INTERNAL_SERVER_ERROR, "500", HttpHeaders.EMPTY, jsonError.getBytes(), StandardCharsets.UTF_8);
+//
+//        when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(new ParameterizedTypeReference<RapUser>() {
+//        }))).thenThrow(exception);
+//
+//        try {
+//            rapClient.getUser("123");
+//        } catch (HttpServerErrorException ex) {
+//            assertEquals("500 Fatal error", ex.getMessage());
+//        }
+//    }
+//
+//    @Test
+//    public void testRefreshToken() {
+//
+//        when(request.getSession(eq(false))).thenReturn(mock(HttpSession.class));
+//        when(sessionData.getExpiresIn()).thenReturn(-100l);
+//
+//        ReflectionTestUtils.setField(rapClient, "sessionData", sessionData);
+//        ReflectionTestUtils.setField(rapClient, "clientId", "clientId");
+//        ReflectionTestUtils.setField(rapClient, "clientSecret", "clientSecret");
+//        ReflectionTestUtils.setField(rapClient, "accessTokenUri", "https://sso.ia2.inaf.it");
+//
+//        String jsonError = "{\"error\":\"Unauthorized: token expired\"}";
+//
+//        HttpClientErrorException exception = Unauthorized
+//                .create(HttpStatus.UNAUTHORIZED, "401", HttpHeaders.EMPTY, jsonError.getBytes(), StandardCharsets.UTF_8);
+//
+//        when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(HttpEntity.class), eq(new ParameterizedTypeReference<RapUser>() {
+//        }))).thenThrow(exception)
+//                .thenReturn(ResponseEntity.ok(new RapUser()));
+//
+//        ResponseEntity refreshTokenResponse = mock(ResponseEntity.class);
+//        Map<String, Object> mockedBody = new HashMap<>();
+//        mockedBody.put("access_token", "<access_token>");
+//        mockedBody.put("refresh_token", "<refresh_token>");
+//        mockedBody.put("expires_in", 3600);
+//        when(refreshTokenResponse.getBody()).thenReturn(mockedBody);
+//
+//        when(refreshTokenRestTemplate.postForEntity(anyString(), any(HttpEntity.class), any()))
+//                .thenReturn(refreshTokenResponse);
+//
+//        RapUser user = rapClient.getUser("123");
+//        assertNotNull(user);
+//
+//        // verifies that token is refreshed
+//        verify(sessionData, times(1)).setAccessToken(eq("<access_token>"));
+//        verify(sessionData, times(1)).setExpiresIn(eq(3600l));
+//    }
 }
diff --git a/gms/src/test/java/it/inaf/ia2/gms/service/GroupNameServiceTest.java b/gms/src/test/java/it/inaf/ia2/gms/service/GroupNameServiceTest.java
index 1ec9f9aee50f263f31ff2ac0cfaad63f9c76aa18..6b4b94643c51e5b0b9ec4b40049004b6bb85c0e7 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/service/GroupNameServiceTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/service/GroupNameServiceTest.java
@@ -4,12 +4,17 @@ import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import java.util.AbstractMap;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
 import static org.junit.Assert.assertEquals;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import static org.mockito.Mockito.when;
@@ -27,56 +32,48 @@ public class GroupNameServiceTest {
     @Test
     public void getNamesTest() {
 
-        mockGroupsDAO();
+        GroupEntity group = new GroupEntity();
+        group.setName("Child\\.withDot");
+        group.setId("def");
+        group.setPath("abc.def");
 
-        List<Map.Entry<String, String>> groupsIdPath = new ArrayList<>();
-        groupsIdPath.add(new AbstractMap.SimpleEntry<>("def", "abc.def"));
-
-        Map<String, List<String>> names = groupNameService.getNames(groupsIdPath);
-        assertEquals(1, names.size());
-        assertEquals(2, names.get("def").size());
-        assertEquals("Group 1", names.get("def").get(0));
-        assertEquals("Group 2", names.get("def").get(1));
-    }
+        Set<GroupEntity> groups = new HashSet<>();
+        groups.add(group);
 
-    public void mockGroupsDAO() {
+        Map<String, String> daoResponse = new HashMap<>();
+        daoResponse.put("def", "Parent_group.Child\\.withDot");
 
-        List<GroupEntity> groups = new ArrayList<>();
+        when(groupsDAO.getGroupCompleteNamesFromId(any())).thenReturn(daoResponse);
 
-        GroupEntity group1 = new GroupEntity();
-        group1.setId("abc");
-        group1.setName("Group 1");
-        group1.setPath("abc");
-        groups.add(group1);
-
-        GroupEntity group2 = new GroupEntity();
-        group2.setId("def");
-        group2.setName("Group 2");
-        group2.setPath("abc.def");
-        groups.add(group2);
-
-        when(groupsDAO.findGroupsByIds(any())).thenReturn(groups);
+        Map<String, List<String>> names = groupNameService.getNames(groups);
+        assertEquals(1, names.size());
+        assertEquals(2, names.get("def").size());
+        assertEquals("Parent_group", names.get("def").get(0));
+        assertEquals("Child\\.withDot", names.get("def").get(1));
     }
 
     @Test
     public void getRootTest() {
 
-        List<GroupEntity> groups = new ArrayList<>();
+        Set<String> groupIds = new HashSet<>();
+        groupIds.add("ROOT");
+
+        when(groupsDAO.getGroupCompleteNamesFromId(any())).thenReturn(new HashMap<>());
 
         GroupEntity root = new GroupEntity();
         root.setId("ROOT");
-        root.setName("Root");
+        root.setName("ROOT");
         root.setPath("");
-        groups.add(root);
 
-        when(groupsDAO.findGroupsByIds(any())).thenReturn(groups);
+        when(groupsDAO.findGroupById(eq("ROOT")))
+                .thenReturn(Optional.of(root));
 
         List<Map.Entry<String, String>> groupsIdPath = new ArrayList<>();
         groupsIdPath.add(new AbstractMap.SimpleEntry<>("ROOT", ""));
 
-        Map<String, List<String>> names = groupNameService.getNames(groupsIdPath);
+        Map<String, List<String>> names = groupNameService.getNamesFromIds(groupIds);
         assertEquals(1, names.size());
         assertEquals(1, names.get("ROOT").size());
-        assertEquals("Root", names.get("ROOT").get(0));
+        assertEquals("ROOT", names.get("ROOT").get(0));
     }
 }
diff --git a/gms/src/test/java/it/inaf/ia2/gms/service/SearchServiceTest.java b/gms/src/test/java/it/inaf/ia2/gms/service/SearchServiceTest.java
index 098d24c62d959296f238b88c12626b38b5c93930..de6961a5760d8efaaa8ad737ac76181e0c35be4f 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/service/SearchServiceTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/service/SearchServiceTest.java
@@ -1,10 +1,7 @@
 package it.inaf.ia2.gms.service;
 
 import it.inaf.ia2.gms.manager.GroupsManager;
-import it.inaf.ia2.gms.model.Identity;
-import it.inaf.ia2.gms.model.IdentityType;
 import it.inaf.ia2.gms.model.Permission;
-import it.inaf.ia2.gms.model.RapUser;
 import it.inaf.ia2.gms.model.response.PaginatedData;
 import it.inaf.ia2.gms.model.response.SearchResponseItem;
 import it.inaf.ia2.gms.model.response.SearchResponseType;
@@ -14,13 +11,16 @@ import it.inaf.ia2.gms.persistence.MembershipsDAO;
 import it.inaf.ia2.gms.persistence.PermissionsDAO;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import it.inaf.ia2.gms.persistence.model.PermissionEntity;
-import it.inaf.ia2.gms.rap.RapClient;
+import it.inaf.ia2.rap.client.RapClient;
+import it.inaf.ia2.rap.data.Identity;
+import it.inaf.ia2.rap.data.IdentityType;
+import it.inaf.ia2.rap.data.RapUser;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Set;
 import static org.junit.Assert.assertEquals;
 import org.junit.Before;
 import org.junit.Test;
@@ -64,18 +64,18 @@ public class SearchServiceTest {
 
         when(groupNameService.getNames(any())).then(invocation -> {
             Map<String, List<String>> result = new HashMap<>();
-            List<Map.Entry<String, String>> arg = invocation.getArgument(0);
-            for (Entry<String, String> entry : arg) {
+            Set<GroupEntity> arg = invocation.getArgument(0);
+            for (GroupEntity group : arg) {
                 List<String> names = new ArrayList<>();
-                switch (entry.getKey()) {
+                switch (group.getId()) {
                     case "ROOT":
-                        names.add("Root");
+                        names.add("ROOT");
                         break;
                     case "group1_id":
                         names.add("Group 1");
                         break;
                 }
-                result.put(entry.getKey(), names);
+                result.put(group.getId(), names);
             }
             return result;
         });
@@ -93,7 +93,7 @@ public class SearchServiceTest {
         identity.setTypedId("user@inaf.it");
         user.setIdentities(Collections.singletonList(identity));
 
-        when(rapClient.searchUsers(any())).thenReturn(Collections.singletonList(user));
+        when(rapClient.getUsers(any(String.class))).thenReturn(Collections.singletonList(user));
 
         GroupEntity group1 = new GroupEntity();
         group1.setId("group1_id");
@@ -136,6 +136,11 @@ public class SearchServiceTest {
     @Test
     public void testGetUserSearchResult() {
 
+        Map<String, List<String>> nameResult = new HashMap<>();
+        nameResult.put("group1_id", Collections.singletonList("Group 1"));
+
+        when(groupNameService.getNamesFromIds(any())).thenReturn(nameResult);
+
         GroupEntity group1 = new GroupEntity();
         group1.setId("group1_id");
         group1.setName("Group 1");
@@ -164,7 +169,7 @@ public class SearchServiceTest {
 
         GroupEntity root = new GroupEntity();
         root.setId("ROOT");
-        root.setName("Root");
+        root.setName("ROOT");
         root.setPath("");
         when(groupsManager.getRoot()).thenReturn(root);