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
index 7be236d16fc22b3616996a4c82ea7ee48f00efaf..481e4c34aa394d12e9b9a1c8d2eee7ac30b59d5a 100644
--- 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
@@ -1,5 +1,6 @@
 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;
@@ -13,6 +14,7 @@ import it.inaf.ia2.gms.client.call.RemovePermissionCall;
 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 {
 
@@ -71,4 +73,8 @@ public class GmsClient {
     public List<UserPermission> getUserPermissions(String userId) {
         return new GetUserPermissionsCall(httpClientWrapper).getUserPermissions(userId);
     }
+
+    public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) {
+        new AddInvitedRegistrationCall(httpClientWrapper).addInvitedRegistration(token, email, groupsPermissions);
+    }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
new file mode 100644
index 0000000000000000000000000000000000000000..0aefd7168c141f47a806b24aece170d997eaadee
--- /dev/null
+++ b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
@@ -0,0 +1,56 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.model.Permission;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class AddInvitedRegistrationCall extends BaseGmsCall {
+
+    public AddInvitedRegistrationCall(HttpClientWrapper clientWrapper) {
+        super(clientWrapper);
+    }
+
+    public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) {
+
+        String tokenHash = getTokenHash(token);
+
+        String endpoint = "invited-registration";
+
+        String bodyParams = "token_hash=" + tokenHash
+                + "&email=" + email + "&groups="
+                + String.join("\n", groupsPermissions.entrySet()
+                        .stream().map(e -> e.getKey() + " " + e.getValue())
+                        .collect(Collectors.toList()));
+
+        HttpRequest groupsRequest = newHttpRequest(endpoint)
+                .header("Accept", "text/plain")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(HttpRequest.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();
+    }
+
+    private String getTokenHash(String token) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(hash);
+        } catch (NoSuchAlgorithmException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+}
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 0f561c65ca11bb57579fd9feb50a644d74b6b31a..63b992e8af1d40c587818cdc5cd39bdc2b57b580 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
@@ -64,7 +64,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
      */
     @Override
     public void configure(WebSecurity web) throws Exception {
-        web.ignoring().antMatchers("/ws/basic/**", "/ws/jwt/**", "/error", "/logout");
+        web.ignoring().antMatchers("/ws/jwt/**", "/error", "/logout", "/invited-registration");
     }
 
     /**
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 fc3f0a4bb06da987cff3d6f85acf18cc14c89448..ced3a3d13c5d356ea486d8fa465f19feeb901c14 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
@@ -1,10 +1,15 @@
 package it.inaf.ia2.gms.controller;
 
 import it.inaf.ia2.gms.authn.SessionData;
+import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
 import it.inaf.ia2.gms.model.request.GroupsRequest;
 import it.inaf.ia2.gms.model.response.GroupsTabResponse;
 import it.inaf.ia2.gms.model.response.HomePageResponse;
+import it.inaf.ia2.gms.persistence.model.InvitedRegistration;
 import java.io.IOException;
+import java.util.Optional;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import javax.validation.Valid;
@@ -25,6 +30,9 @@ public class HomePageController {
     @Autowired
     private GroupsTabResponseBuilder groupsTabResponseBuilder;
 
+    @Autowired
+    private InvitedRegistrationManager invitedRegistrationManager;
+
     @ResponseBody
     @GetMapping(value = "/home", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
     public ResponseEntity<HomePageResponse> getMainPage(@Valid GroupsRequest request) {
@@ -42,7 +50,15 @@ public class HomePageController {
     }
 
     @GetMapping(value = "/", produces = MediaType.TEXT_HTML_VALUE)
-    public String index() {
+    public String index(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+
+        Optional<InvitedRegistration> optReg = invitedRegistrationManager.completeInvitedRegistrationIfNecessary();
+        if (optReg.isPresent()) {
+            request.setAttribute("invited-registration", optReg.get());
+            return "/registration-completed";
+            //request.getRequestDispatcher("/registration-completed").forward(request, response);
+        }
+
         return "index.html";
     }
 
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java
new file mode 100644
index 0000000000000000000000000000000000000000..0a4554cad6fe0b406ef329446d0779239705e8c8
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java
@@ -0,0 +1,74 @@
+package it.inaf.ia2.gms.controller;
+
+import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
+import it.inaf.ia2.gms.persistence.model.InvitedRegistration;
+import it.inaf.ia2.gms.service.GroupNameService;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Scanner;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+
+@Controller
+public class InvitedRegistrationController {
+
+    @Autowired
+    private InvitedRegistrationManager invitedRegistrationManager;
+
+    @Autowired
+    private GroupNameService groupNameService;
+
+    @GetMapping(value = "/invited-registration", produces = MediaType.TEXT_HTML_VALUE)
+    public void index(@RequestParam("token") String token, HttpServletRequest request, HttpServletResponse response) throws IOException {
+        InvitedRegistration invitedRegistration = invitedRegistrationManager.getInvitedRegistrationFromToken(token);
+
+        String html = getFileContent("invited-registration.html")
+                .replace("#EMAIL#", invitedRegistration.getEmail())
+                .replace("#GROUPS#", getGroupsList(invitedRegistration))
+                .replace("#HOME#", request.getContextPath());
+
+        response.getOutputStream().print(html);
+    }
+
+    @GetMapping(value = "/registration-completed", produces = MediaType.TEXT_HTML_VALUE)
+    public void completed(HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+        response.setContentType("text/html;charset=UTF-8");
+
+        InvitedRegistration invitedRegistration = (InvitedRegistration) request.getAttribute("invited-registration");
+        if (invitedRegistration == null) {
+            // redirect to home
+            String baseUrl = ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
+            response.sendRedirect(baseUrl);
+        } else {
+            String html = getFileContent("registration-completed.html")
+                    .replace("#GROUPS#", getGroupsList(invitedRegistration))
+                    .replace("#HOME#", request.getContextPath());
+
+            response.getOutputStream().print(html);
+        }
+    }
+
+    private String getFileContent(String templateFileName) throws IOException {
+        try (InputStream in = InvitedRegistrationController.class.getClassLoader().getResourceAsStream("templates/" + templateFileName)) {
+            Scanner s = new Scanner(in).useDelimiter("\\A");
+            return s.hasNext() ? s.next() : "";
+        }
+    }
+
+    private String getGroupsList(InvitedRegistration invitedRegistration) {
+        String groups = "<ul>";
+        for (String groupName : groupNameService.getGroupsNamesFromIdentifiers(
+                invitedRegistration.getGroupsPermissions().keySet())) {
+            groups += "<li>" + groupName + "</li>";
+        }
+        groups += "</ul>";
+        return groups;
+    }
+}
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 4a7a2d6481af053085fe2914cadb16c42712c5d2..ea828d36a8d3c306861c01ff81859b3eaeb203a7 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
@@ -3,6 +3,7 @@ package it.inaf.ia2.gms.controller;
 import it.inaf.ia2.gms.authn.RapPrincipal;
 import it.inaf.ia2.gms.exception.BadRequestException;
 import it.inaf.ia2.gms.manager.GroupsManager;
+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;
@@ -11,6 +12,7 @@ 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.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.gms.service.PermissionUtils;
@@ -20,11 +22,9 @@ import java.io.PrintWriter;
 import java.security.Principal;
 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 javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -56,6 +56,9 @@ public class JWTWebServiceController {
 
     @Autowired
     private GroupsService groupsService;
+    
+    @Autowired
+    private GroupNameService groupNameService;
 
     @Autowired
     private MembershipManager membershipManager;
@@ -69,6 +72,9 @@ public class JWTWebServiceController {
     @Autowired
     private SearchService searchService;
 
+    @Autowired
+    private InvitedRegistrationManager invitedRegistrationManager;
+
     /**
      * This endpoint is compliant with the IVOA GMS standard.
      */
@@ -77,7 +83,7 @@ public class JWTWebServiceController {
 
         List<GroupEntity> memberships = membershipManager.getCurrentUserMemberships();
 
-        List<String> names = getGroupsNames(memberships);
+        List<String> names = groupNameService.getGroupsNames(memberships);
 
         try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
 
@@ -145,7 +151,7 @@ public class JWTWebServiceController {
         }
 
         try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
-            for (String groupName : getGroupsNames(visibleSubgroups)) {
+            for (String groupName : groupNameService.getGroupsNames(visibleSubgroups)) {
                 pw.println(getShortGroupName(groupName, group));
             }
         }
@@ -195,7 +201,7 @@ public class JWTWebServiceController {
         List<GroupEntity> groups = membershipManager.getUserGroups(parent, userId);
 
         try (PrintWriter pw = new PrintWriter(response.getOutputStream())) {
-            for (String groupName : getGroupsNames(groups)) {
+            for (String groupName : groupNameService.getGroupsNames(groups)) {
                 pw.println(getShortGroupName(groupName, group));
             }
         }
@@ -265,6 +271,27 @@ public class JWTWebServiceController {
         response.setStatus(HttpServletResponse.SC_NO_CONTENT);
     }
 
+    @PostMapping(value = "/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) {
+
+        Map<GroupEntity, Permission> groupsPermissions = new HashMap<>();
+
+        for (String param : groupNamesAndPermissionsParam.split("\n")) {
+            if (!param.isEmpty()) {
+                int lastSpaceIndex = param.lastIndexOf(" ");
+                String groupName = param.substring(0, lastSpaceIndex);
+                Permission permission = Permission.valueOf(param.substring(lastSpaceIndex + 1));
+                GroupEntity groupEntity = getGroupFromNames(extractGroupNames(groupName));
+                groupsPermissions.put(groupEntity, permission);
+            }
+        }
+
+        invitedRegistrationManager.addInvitedRegistration(tokenHash, email, groupsPermissions);
+
+        response.setStatus(HttpServletResponse.SC_CREATED);
+    }
+
     private GroupEntity getGroupFromNames(List<String> groupNames) {
         if (groupNames.isEmpty()) {
             return getRoot();
@@ -292,27 +319,6 @@ public class JWTWebServiceController {
                 .orElseThrow(() -> new IllegalStateException("Missing root group"));
     }
 
-    /**
-     * Returns the list of the group complete names, given a list of GroupEntity
-     * objects. TODO: probably this logic is duplicated inside GroupNameService.
-     * Check this.
-     */
-    private 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());
-        }
-
-        List<String> names = new ArrayList<>();
-        for (GroupEntity group : groups) {
-            names.add(getGroupCompleteName(group, idNameMap));
-        }
-        return names;
-    }
 
     private List<String> extractGroupNames(Optional<String> group) {
         return extractGroupNames(group.orElse(null));
@@ -341,44 +347,6 @@ public class JWTWebServiceController {
         return names;
     }
 
-    private Set<String> getAllIdentifiers(List<GroupEntity> groups) {
-
-        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;
-    }
-
-    private String getGroupCompleteName(GroupEntity group, Map<String, String> idNameMap) {
-
-        if ("ROOT".equals(group.getId())) {
-            return group.getName();
-        }
-
-        List<String> names = new ArrayList<>();
-
-        for (String groupId : group.getPath().split("\\.")) {
-
-            String groupName = idNameMap.get(groupId);
-
-            // 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("\\.", "\\\\.");
-
-            names.add(groupName);
-        }
-
-        return String.join(".", names);
-    }
-
     private String getShortGroupName(String completeGroupName, Optional<String> groupPrefix) {
         if (groupPrefix.isPresent()) {
             return completeGroupName.substring(groupPrefix.get().length() + 1);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java b/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..f04bc1f55742bc6550ca29658373f1714199b2be
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/exception/NotFoundException.java
@@ -0,0 +1,12 @@
+package it.inaf.ia2.gms.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.NOT_FOUND)
+public class NotFoundException extends RuntimeException {
+
+    public NotFoundException(String message) {
+        super(message);
+    }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..a8547e302e897c5a460ecbab124cbbafd9ee2484
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
@@ -0,0 +1,119 @@
+package it.inaf.ia2.gms.manager;
+
+import it.inaf.ia2.gms.exception.NotFoundException;
+import it.inaf.ia2.gms.exception.UnauthorizedException;
+import it.inaf.ia2.gms.model.Permission;
+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.persistence.model.MembershipEntity;
+import it.inaf.ia2.gms.service.PermissionsService;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import javax.servlet.http.HttpSession;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class InvitedRegistrationManager extends UserAwareComponent {
+
+    private static final String INVITED_REGISTRATION = "invited-registration";
+
+    @Autowired
+    private GroupsDAO groupsDAO;
+
+    @Autowired
+    private MembershipsDAO membershipsDAO;
+
+    @Autowired
+    private PermissionsService permissionsService;
+
+    @Autowired
+    private PermissionsManager permissionsManager;
+
+    @Autowired
+    private InvitedRegistrationDAO invitedRegistrationDAO;
+
+    @Autowired
+    private LoggingDAO loggingDAO;
+
+    @Autowired
+    private HttpSession httpSession;
+
+    public void addInvitedRegistration(String tokenHash, String email, Map<GroupEntity, Permission> groupsPermissions) {
+
+        Map<String, Permission> groupIdsPermissions = new HashMap<>();
+        for (Map.Entry<GroupEntity, Permission> entry : groupsPermissions.entrySet()) {
+            GroupEntity group = entry.getKey();
+            if (permissionsManager.getCurrentUserPermission(group) != Permission.ADMIN) {
+                throw new UnauthorizedException("You don't have the permission to perform invited registrations");
+            }
+            groupIdsPermissions.put(group.getId(), entry.getValue());
+        }
+
+        InvitedRegistration invitedRegistration = new InvitedRegistration()
+                .setId(UUID.randomUUID().toString().replaceAll("-", ""))
+                .setEmail(email)
+                .setTokenHash(tokenHash)
+                .setGroupsPermissions(groupIdsPermissions);
+
+        invitedRegistrationDAO.addInvitedRegistration(invitedRegistration);
+    }
+
+    public InvitedRegistration getInvitedRegistrationFromToken(String token) {
+
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] hash = digest.digest(token.getBytes(StandardCharsets.UTF_8));
+            String tokenHash = Base64.getEncoder().encodeToString(hash);
+
+            InvitedRegistration invitedRegistration = invitedRegistrationDAO.getInvitedRegistrationFromToken(tokenHash)
+                    .orElseThrow(() -> new NotFoundException("No invited registrations found for this token"));
+
+            httpSession.setAttribute(INVITED_REGISTRATION, invitedRegistration);
+
+            loggingDAO.logAction("Started invited registration for email " + invitedRegistration.getEmail());
+
+            return invitedRegistration;
+        } catch (NoSuchAlgorithmException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    public Optional<InvitedRegistration> completeInvitedRegistrationIfNecessary() {
+
+        InvitedRegistration invitedRegistration = (InvitedRegistration) httpSession.getAttribute(INVITED_REGISTRATION);
+
+        if (invitedRegistration != null) {
+
+            for (Map.Entry<String, Permission> entry : invitedRegistration.getGroupsPermissions().entrySet()) {
+                String groupId = entry.getKey();
+                String userId = getCurrentUserId();
+
+                GroupEntity groupEntity = groupsDAO.findGroupById(groupId).get();
+
+                MembershipEntity membershipEntity = new MembershipEntity();
+                membershipEntity.setUserId(userId);
+                membershipEntity.setGroupId(groupId);
+                membershipsDAO.addMember(membershipEntity);
+
+                permissionsService.addPermission(groupEntity, userId, entry.getValue());
+            }
+
+            invitedRegistrationDAO.setRegistrationDone(invitedRegistration);
+
+            httpSession.removeAttribute(INVITED_REGISTRATION);
+        }
+
+        return Optional.ofNullable(invitedRegistration);
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java
index e7047fec36e815dc69be34e75677d2a3a76b478e..382401584554a9b6ff756947e3cdafeb07862d14 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java
@@ -1,9 +1,11 @@
 package it.inaf.ia2.gms.persistence;
 
+import it.inaf.ia2.gms.model.Permission;
 import it.inaf.ia2.gms.persistence.model.InvitedRegistration;
 import java.sql.PreparedStatement;
-import java.util.ArrayList;
-import java.util.List;
+import java.sql.Types;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Optional;
 import javax.sql.DataSource;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -34,13 +36,15 @@ public class InvitedRegistrationDAO {
             return ps;
         });
 
-        for (String groupId : invitedRegistration.getGroupIds()) {
-            String sqlReqGroup = "INSERT INTO invited_registration_request_group (request_id, group_id) VALUES (?, ?)";
+        for (Map.Entry<String, Permission> entry : invitedRegistration.getGroupsPermissions().entrySet()) {
+
+            String sqlReqGroup = "INSERT INTO invited_registration_request_group (request_id, group_id, permission) VALUES (?, ?, ?)";
 
             jdbcTemplate.update(conn -> {
                 PreparedStatement ps = conn.prepareStatement(sqlReqGroup);
                 ps.setString(1, invitedRegistration.getId());
-                ps.setString(2, groupId);
+                ps.setString(2, entry.getKey());
+                ps.setObject(3, entry.getValue().toString(), Types.OTHER);
                 return ps;
             });
         }
@@ -48,7 +52,7 @@ public class InvitedRegistrationDAO {
 
     public Optional<InvitedRegistration> getInvitedRegistrationFromToken(String tokenHash) {
 
-        String sqlReq = "SELECT id, email FROM invited_registration_request WHERE token_hash = ? AND !done";
+        String sqlReq = "SELECT id, email FROM invited_registration_request WHERE token_hash = ? AND done IS NOT true";
 
         InvitedRegistration registration = jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sqlReq);
@@ -66,33 +70,35 @@ public class InvitedRegistrationDAO {
 
         if (registration != null) {
 
-            String sqlReqGroup = "SELECT group_id FROM invited_registration_request_group WHERE request_id = ?";
+            String sqlReqGroup = "SELECT group_id, permission FROM invited_registration_request_group WHERE request_id = ?";
 
-            List<String> groupIds = jdbcTemplate.query(conn -> {
+            Map<String, Permission> groupsPermissions = jdbcTemplate.query(conn -> {
                 PreparedStatement ps = conn.prepareStatement(sqlReqGroup);
                 ps.setString(1, registration.getId());
                 return ps;
             }, resultSet -> {
-                List<String> groups = new ArrayList<>();
+                Map<String, Permission> map = new HashMap<>();
                 while (resultSet.next()) {
-                    groups.add(resultSet.getString("group_id"));
+                    String groupId = resultSet.getString("group_id");
+                    Permission permission = Permission.valueOf(resultSet.getString("permission"));
+                    map.put(groupId, permission);
                 }
-                return groups;
+                return map;
             });
 
-            registration.setGroupIds(groupIds);
+            registration.setGroupsPermissions(groupsPermissions);
         }
 
         return Optional.ofNullable(registration);
     }
 
-    public void setRegistrationDone(String tokenHash) {
+    public void setRegistrationDone(InvitedRegistration invitedRegistration) {
 
-        String sql = "UPDATE invited_registration_request SET done = true WHERE token_hash = ?";
+        String sql = "UPDATE invited_registration_request SET done = true WHERE id = ?";
 
         jdbcTemplate.update(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
-            ps.setString(1, tokenHash);
+            ps.setString(1, invitedRegistration.getId());
             return ps;
         });
     }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
index aedfbf8bf04c2136ab0b1e2da43d5babf4991b13..6f0b4c659b92eb5ce6915893e500704ca5173c19 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
@@ -99,7 +99,8 @@ public class MembershipsDAO {
 
     public MembershipEntity addMember(MembershipEntity membership) {
 
-        String sql = "INSERT INTO gms_membership (group_id, user_id) VALUES (?, ?)";
+        String sql = "INSERT INTO gms_membership (group_id, user_id) VALUES (?, ?)\n"
+                + "ON CONFLICT (group_id, user_id) DO NOTHING";
 
         jdbcTemplate.update(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java
index da709f7b539e75e4c7bcae44c1fc91a2dde9a94d..f0e476941479e95272e50090c3a11632c61f9dd6 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java
@@ -1,6 +1,7 @@
 package it.inaf.ia2.gms.persistence.model;
 
-import java.util.List;
+import it.inaf.ia2.gms.model.Permission;
+import java.util.Map;
 
 public class InvitedRegistration {
 
@@ -8,45 +9,50 @@ public class InvitedRegistration {
     private String tokenHash;
     private String email;
     private boolean done;
-    private List<String> groupIds;
+    private Map<String, Permission> groupsPermissions;
 
     public String getId() {
         return id;
     }
 
-    public void setId(String id) {
+    public InvitedRegistration setId(String id) {
         this.id = id;
+        return this;
     }
 
     public String getTokenHash() {
         return tokenHash;
     }
 
-    public void setTokenHash(String tokenHash) {
+    public InvitedRegistration setTokenHash(String tokenHash) {
         this.tokenHash = tokenHash;
+        return this;
     }
 
     public String getEmail() {
         return email;
     }
 
-    public void setEmail(String email) {
+    public InvitedRegistration setEmail(String email) {
         this.email = email;
+        return this;
     }
 
     public boolean isDone() {
         return done;
     }
 
-    public void setDone(boolean done) {
+    public InvitedRegistration setDone(boolean done) {
         this.done = done;
+        return this;
     }
 
-    public List<String> getGroupIds() {
-        return groupIds;
+    public Map<String, Permission> getGroupsPermissions() {
+        return groupsPermissions;
     }
 
-    public void setGroupIds(List<String> groupIds) {
-        this.groupIds = groupIds;
+    public InvitedRegistration setGroupsPermissions(Map<String, Permission> groupsPermissions) {
+        this.groupsPermissions = groupsPermissions;
+        return this;
     }
 }
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 20c04e9f72997631d1bceb5019b618d1d45cfae3..0cb329b6d85acd5e9f44f52af65d90ff7e76178e 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
@@ -22,6 +22,69 @@ public class GroupNameService {
     @Autowired
     private GroupsDAO groupsDAO;
 
+    public List<String> getGroupsNamesFromIdentifiers(Set<String> groupIdentifiers) {
+        return getGroupsNames(groupsDAO.findGroupsByIds(groupIdentifiers));
+    }
+
+    /**
+     * Returns the list of the group complete names, given a list of GroupEntity
+     * objects.
+     */
+    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());
+        }
+
+        List<String> names = new ArrayList<>();
+        for (GroupEntity group : groups) {
+            names.add(getGroupCompleteName(group, idNameMap));
+        }
+        return names;
+    }
+
+    private Set<String> getAllIdentifiers(List<GroupEntity> groups) {
+
+        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;
+    }
+
+    private String getGroupCompleteName(GroupEntity group, Map<String, String> idNameMap) {
+
+        if ("ROOT".equals(group.getId())) {
+            return group.getName();
+        }
+
+        List<String> names = new ArrayList<>();
+
+        for (String groupId : group.getPath().split("\\.")) {
+
+            String groupName = idNameMap.get(groupId);
+
+            // 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("\\.", "\\\\.");
+
+            names.add(groupName);
+        }
+
+        return String.join(".", names);
+    }
+
     /**
      * @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
diff --git a/gms/src/main/resources/sql/init.sql b/gms/src/main/resources/sql/init.sql
index b9c0a942032d62593a2087401ec47d094f627eca..2849c2fae0c470df1863c1e6180e55269d52b003 100644
--- a/gms/src/main/resources/sql/init.sql
+++ b/gms/src/main/resources/sql/init.sql
@@ -56,6 +56,8 @@ CREATE TABLE invited_registration_request (
 CREATE TABLE invited_registration_request_group (
   request_id varchar NOT NULL,
   group_id varchar NOT NULL,
-  PRIMARY KEY (request_id, group_id),
-  FOREIGN KEY (request_id) REFERENCES invited_registration_request(id)
+  permission permission_type NOT NULL,
+  PRIMARY KEY (request_id, group_id, permission),
+  FOREIGN KEY (request_id) REFERENCES invited_registration_request(id),
+  FOREIGN KEY (group_id) REFERENCES gms_group(id)
 );
diff --git a/gms/src/main/resources/templates/invited-registration.html b/gms/src/main/resources/templates/invited-registration.html
new file mode 100644
index 0000000000000000000000000000000000000000..32e9a46e03c0de8912294149293dae0f7d41ed12
--- /dev/null
+++ b/gms/src/main/resources/templates/invited-registration.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Invited registration</title>
+        <meta charset="UTF-8" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
+    </head>
+    <body>
+        <div class="container mt-4">
+            <h1 class="mb-3">Invited registration</h1>
+            <p>Hi <strong>#EMAIL#</strong>, at the end of this procedure you will be added to the following groups:</p>
+            #GROUPS#
+            <p>Just perform a login!</p>
+            <p><a class="btn btn-primary" href="#HOME#">Login</a></p>
+        </div>
+    </body>
+</html>
diff --git a/gms/src/main/resources/templates/registration-completed.html b/gms/src/main/resources/templates/registration-completed.html
new file mode 100644
index 0000000000000000000000000000000000000000..5ded06733c9456ecae511981068e2cadab602325
--- /dev/null
+++ b/gms/src/main/resources/templates/registration-completed.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Invited registration completed</title>
+        <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous" />
+    </head>
+    <body>
+        <div class="container mt-4">
+            <h1 class="mb-3">Invited registration completed!</h1>
+            <p>You are now member of the following groups:</p>
+            #GROUPS#
+            <p><a class="btn btn-primary" href="#HOME#">Ok</a></p>
+        </div>
+    </body>
+</html>