diff --git a/classes/CallbackHandler.php b/classes/CallbackHandler.php
index de88c9e88153c36c0a043f7fc93d6954a3387a8c..85d33e978b8c3d128ab6fe642a5751dedee85f9a 100644
--- a/classes/CallbackHandler.php
+++ b/classes/CallbackHandler.php
@@ -48,13 +48,13 @@ class CallbackHandler {
 
     public static function manageLoginRedirect($user) {
 
-        global $BASE_PATH, $session;
+        global $BASE_PATH, $session, $log;
 
-        if (isset($session->callback) && $session->callback !== null) {
+        if ($session->getCallbackURL() !== null) {
             // External login using token
             $token = Util::createNewToken();
-            DAO::get()->insertTokenData($token, $user->id);
-            header('Location: ' . $session->callback . '?token=' . $token);
+            DAO::get()->createLoginToken($token, $user->id);
+            header('Location: ' . $session->getCallbackURL() . '?token=' . $token);
         } else {
             // Login in session
             $session->user = $user;
diff --git a/classes/DAO.php b/classes/DAO.php
index 8a9d0789f7eb5fa76b2d669f0b6bcdb2ae96bfaf..713eff90539f3f46ba8add7d430c68ffb797cb68 100644
--- a/classes/DAO.php
+++ b/classes/DAO.php
@@ -60,11 +60,13 @@ abstract class DAO {
 
     public abstract function createJoinRequest($token, $applicantUserId, $targetUserId);
 
-    public $config;
+    public abstract function findJoinRequest($token);
 
-    public function __construct($config) {
-        $this->config = $config;
-    }
+    public abstract function deleteUser($userId);
+
+    public abstract function joinUsers($userId1, $userId2);
+    
+    public abstract function deleteJoinRequest($token);
 
     public static function get() {
         global $DATABASE;
diff --git a/classes/GrouperClient.php b/classes/GrouperClient.php
new file mode 100644
index 0000000000000000000000000000000000000000..2bef66b38c7c04ee42eec2b85b52f6809dd33e8e
--- /dev/null
+++ b/classes/GrouperClient.php
@@ -0,0 +1,110 @@
+<?php
+
+/* ----------------------------------------------------------------------------
+ *               INAF - National Institute for Astrophysics
+ *               IRA  - Radioastronomical Institute - Bologna
+ *               OATS - Astronomical Observatory - Trieste
+ * ----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2016 Istituto Nazionale di Astrofisica
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License Version 3 as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+namespace RAP;
+
+class GrouperClient {
+
+    private $client;
+
+    function __construct($config) {
+
+        $this->client = new \SoapClient($config['wsdlURL'], array(
+            'login' => $config['user'],
+            'password' => $config['password'],
+            'trace' => 1,
+            // See: https://bugs.php.net/bug.php?id=36226
+            'features' => SOAP_SINGLE_ELEMENT_ARRAYS
+                )
+        );
+    }
+
+    private function getBaseRequestParams() {
+        return array(
+            'clientVersion' => 'v2_3_000'
+        );
+    }
+
+    private function startsWith($haystack, $needle) {
+        return strpos($haystack, "$needle", 0) === 0;
+    }
+
+    private function isSuccess($response) {
+        $success = isset($response->return->resultMetadata) && $response->return->resultMetadata->resultCode === 'SUCCESS';
+        if (!$success) {
+            throw new \Exception("Web Service Failure. Response=" . json_encode($response));
+        }
+        return $success;
+    }
+
+    public function getSubjectGroups($subjectId) {
+
+        $params = $this->getBaseRequestParams();
+        $params['subjectLookups'] = array(
+            'subjectId' => $subjectId,
+            'subjectSourceId' => 'RAP'
+        );
+
+        $response = $this->client->getGroups($params);
+
+        if ($this->isSuccess($response)) {
+            if (count($response->return->results) === 1) {
+                $groups = [];
+                foreach ($response->return->results[0]->wsGroups as $group) {
+                    if (!$this->startsWith($group->name, 'etc:')) {
+                        array_push($groups, $group->name);
+                    }
+                }
+                return $groups;
+            } else {
+                throw new \Exception("Wrong results number. Response=" . json_encode($response));
+            }
+        }
+    }
+
+    public function addMemberships($subjectId, $groups) {
+
+        foreach ($groups as $group) {
+            $params = $this->getBaseRequestParams();
+            $params['subjectId'] = $subjectId;
+            $params['subjectSourceId'] = 'RAP';
+            $params['groupName'] = $group;
+
+            $this->client->addMemberLite($params);
+        }
+    }
+
+    public function removeMemberships($subjectId, $groups) {
+
+        foreach ($groups as $group) {
+            $params = $this->getBaseRequestParams();
+            $params['subjectId'] = $subjectId;
+            $params['subjectSourceId'] = 'RAP';
+            $params['groupName'] = $group;
+
+            $this->client->deleteMemberLite($params);
+        }
+    }
+
+}
diff --git a/classes/TokenHandler.php b/classes/MailSender.php
similarity index 70%
rename from classes/TokenHandler.php
rename to classes/MailSender.php
index 4c7998ade71c855226a52c1a943644c5f50a347f..cede88a89c79eac54a6a0460884206e0bf49d2f0 100644
--- a/classes/TokenHandler.php
+++ b/classes/MailSender.php
@@ -24,20 +24,10 @@
 
 namespace RAP;
 
-class TokenHandler {
+class MailSender {
 
-    public static function createNewToken($data) {
-        $token = bin2hex(openssl_random_pseudo_bytes(16)); // http://stackoverflow.com/a/18890309/771431
-        DAO::get()->insertTokenData($token, $data);
-        return $token;
-    }
-
-    public static function deleteToken($token) {
-        DAO::get()->deleteToken($token);
-    }
-
-    public static function getUserData($token) {
-        return DAO::get()->findTokenData($token);
+    public static function sendJoinEmail(User $recipientUser, User $applicantUser) {
+        
     }
 
 }
diff --git a/classes/MySQLDAO.php b/classes/MySQLDAO.php
index 05ba51a75a2afb6d38ec3656612934d49c4bb650..ab2cddb174984eb907bf2a7aa2cfeac62cbd4614 100644
--- a/classes/MySQLDAO.php
+++ b/classes/MySQLDAO.php
@@ -29,8 +29,12 @@ use PDO;
 class MySQLDAO extends DAO {
 
     public function getDBHandler() {
-        $connectionString = "mysql:host=" . $this->config['hostname'] . ";dbname=" . $this->config['dbname'];
-        return new PDO($connectionString, $this->config['username'], $this->config['password']);
+        global $DATABASE;
+        $connectionString = "mysql:host=" . $DATABASE['hostname'] . ";dbname=" . $DATABASE['dbname'];
+        $dbh = new PDO($connectionString, $DATABASE['username'], $DATABASE['password']);
+        // For transaction errors (see https://stackoverflow.com/a/9659366/771431)
+        $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+        return $dbh;
     }
 
     public function createLoginToken($token, $userId) {
@@ -130,6 +134,10 @@ class MySQLDAO extends DAO {
 
     public function findUserById($userId) {
 
+        if (!filter_var($userId, FILTER_VALIDATE_INT)) {
+            return null;
+        }
+
         $dbh = $this->getDBHandler();
 
         $stmt = $dbh->prepare("SELECT `id`, `type`, `typed_id`, `email`, `local_db_id`, `name`, `surname`, `institution`, `username`, `eppn`"
@@ -269,4 +277,75 @@ class MySQLDAO extends DAO {
         $stmt->execute();
     }
 
+    public function findJoinRequest($token) {
+        $dbh = $this->getDBHandler();
+
+        $stmt = $dbh->prepare("SELECT `applicant_user_id`, `target_user_id` FROM `join_request` WHERE `token` = :token");
+        $stmt->bindParam(':token', $token);
+        $stmt->execute();
+
+        $result = $stmt->fetchAll();
+
+        switch (count($result)) {
+            case 0:
+                return null;
+            case 1:
+                $row = $result[0];
+                return [$row['applicant_user_id'], $row['target_user_id']];
+            default:
+                throw new Exception("Found multiple join request with the same token");
+        }
+    }
+
+    public function deleteUser($userId) {
+        $dbh = $this->getDBHandler();
+
+        $stmt3 = $dbh->prepare("DELETE FROM TABLE `user` WHERE `id` = :id2");
+        $stmt3->bindParam(':id2', $userId);
+        $stmt3->execute();
+    }
+
+    public function joinUsers($userId1, $userId2) {
+        $dbh = $this->getDBHandler();
+
+        try {
+            $dbh->beginTransaction();
+
+            // Moving identities from user2 to user1
+            $stmt1 = $dbh->prepare("UPDATE `identity` SET `user_id` = :id1 WHERE `user_id` = :id2");
+            $stmt1->bindParam(':id1', $userId1);
+            $stmt1->bindParam(':id2', $userId2);
+            $stmt1->execute();
+
+            // Moving additional email addresses from user2 to user1
+            $stmt2 = $dbh->prepare("UPDATE `additional_email` SET `user_id` = :id1 WHERE `user_id` = :id2");
+            $stmt2->bindParam(':id1', $userId1);
+            $stmt2->bindParam(':id2', $userId2);
+            $stmt2->execute();
+
+            // Deleting user2 join requests
+            $stmt3 = $dbh->prepare("DELETE FROM `join_request` WHERE `target_user_id` = :id2");
+            $stmt3->bindParam(':id2', $userId2);
+            $stmt3->execute();
+
+            // Deleting user2
+            $stmt4 = $dbh->prepare("DELETE FROM `user` WHERE `id` = :id2");
+            $stmt4->bindParam(':id2', $userId2);
+            $stmt4->execute();
+
+            $dbh->commit();
+        } catch (Exception $ex) {
+            $dbh->rollBack();
+            throw $ex;
+        }
+    }
+
+    public function deleteJoinRequest($token) {
+        $dbh = $this->getDBHandler();
+
+        $stmt = $dbh->prepare("DELETE FROM `join_request` WHERE `token` = :token");
+        $stmt->bindParam(':token', $token);
+        $stmt->execute();
+    }
+
 }
diff --git a/classes/User.php b/classes/User.php
index 220c6f088c4a28f7e851ff708f712f072e37d2c3..1d7a6cb9b9250637224eb8c4d3b4a357b38d7cef 100644
--- a/classes/User.php
+++ b/classes/User.php
@@ -43,4 +43,7 @@ class User {
         array_push($this->additionalEmailAddresses, $email);
     }
 
+    public function getPrimaryEmail() {
+        return $this->identities[0]->email;
+    }
 }
diff --git a/classes/UserHandler.php b/classes/UserHandler.php
index bb3bb3eceb9efa616702bcfb4682df5e08a233ba..27223bb3fbc760e1d8484e052e0897696d940eab 100644
--- a/classes/UserHandler.php
+++ b/classes/UserHandler.php
@@ -52,4 +52,19 @@ class UserHandler {
         return DAO::get()->findUserByIdentity($type, $identifier, $dbIdentifier);
     }
 
+    public static function joinUsers($userId1, $userId2) {
+
+        global $GROUPER;
+
+        if (isset($GROUPER)) {
+            $gc = new GrouperClient($GROUPER);
+
+            $groupsToMove = $gc->getSubjectGroups('RAP:' . $userId2);
+            $gc->addMemberships('RAP:' . $userId1, $groupsToMove);
+            $gc->removeMemberships('RAP:' . $userId2, $groupsToMove);
+        }
+
+        DAO::get()->joinUsers($userId1, $userId2);
+    }
+
 }
diff --git a/classes/UserSearchResult.php b/classes/UserSearchResult.php
index daada02ea3d19caadf4e04cd3b6c512c74454640..25de1ad65ea0494889ed1603c57fc5c9c27cbfd6 100644
--- a/classes/UserSearchResult.php
+++ b/classes/UserSearchResult.php
@@ -26,12 +26,12 @@ namespace RAP;
 
 class UserSearchResult {
 
-    private $userId;
+    private $user;
     public $userDisplayText;
 
     public static function buildFromUser(User $user) {
         $usr = new UserSearchResult();
-        $usr->userId = $user->id;
+        $usr->user = $user;
 
         $nameAndSurname = null;
         $email = null;
@@ -71,8 +71,8 @@ class UserSearchResult {
         return $usr;
     }
 
-    public function getUserId() {
-        return $this->userId;
+    public function getUser() {
+        return $this->user;
     }
 
 }
diff --git a/classes/X509Data.php b/classes/X509Data.php
new file mode 100644
index 0000000000000000000000000000000000000000..c907452c094a4f783e2c6470cbd4880f04a23f44
--- /dev/null
+++ b/classes/X509Data.php
@@ -0,0 +1,148 @@
+<?php
+
+/* ----------------------------------------------------------------------------
+ *               INAF - National Institute for Astrophysics
+ *               IRA  - Radioastronomical Institute - Bologna
+ *               OATS - Astronomical Observatory - Trieste
+ * ----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2016 Istituto Nazionale di Astrofisica
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License Version 3 as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+namespace RAP;
+
+class X509Data {
+
+    public $email;
+    public $fullName;
+    public $institution;
+    public $serialNumber;
+
+    /**
+     * Retrieve full name from CN, removing the e-mail if necessary
+     */
+    private function parseCN($cn) {
+        $cnSplit = explode(" ", $cn);
+
+        $count = count($cnSplit);
+        $lastSegment = $cnSplit[$count - 1];
+
+        if (strpos($lastSegment, "@") === false) {
+            if ($count < 2) {
+                // We need name + surname
+                throw new \Exception("Unparsable CN");
+            }
+            $this->fullName = $cn;
+        } else {
+            // Last segment is an email
+            if ($count < 3) {
+                // We need name + surname + email
+                throw new \Exception("Unparsable CN");
+            }
+
+            // Rebuilding full name removing the email part
+            $cnLength = strlen($cn);
+            $emailLength = strlen($lastSegment);
+            // -1 is for removing also the space between surname and email
+            $this->fullName = substr($cn, 0, $cnLength - $emailLength - 1);
+        }
+    }
+
+    private function parseUsingOpenSSL($sslClientCert) {
+
+        $parsedX509 = openssl_x509_parse($sslClientCert);
+
+        // try extracting email
+        if (isset($parsedX509["extensions"]["subjectAltName"])) {
+            $AyAlt = explode(":", $parsedX509["extensions"]["subjectAltName"]);
+            if ($AyAlt[0] === "email") {
+                $this->email = $AyAlt[1];
+            }
+        }
+
+        $this->serialNumber = $parsedX509["serialNumber"];
+
+        $cn = $parsedX509["subject"]["CN"];
+        $this->parseCN($cn);
+
+        $this->institution = $parsedX509["subject"]["O"];
+    }
+
+    private function extractDNFields($dn) {
+        $dnSplit = explode(",", $dn);
+
+        foreach ($dnSplit as $dnField) {
+            $pos = strpos($dnField, "=");
+            $key = substr($dnField, 0, $pos - 1);
+            $value = substr($dnField, $pos + 1, strlen($dnField) - 1);
+            switch ($key) {
+                case 'CN':
+                    $this->parseCN($value);
+                    break;
+                case 'O':
+                    $this->institution = $value;
+            }
+        }
+    }
+
+    public static function parse($server) {
+
+        $parsedData = new X509Data();
+
+        if (isset($server['SSL_CLIENT_CERT'])) {
+            $parsedData->parseUsingOpenSSL(['SSL_CLIENT_CERT']);
+            // If all mandatory information has been parsed return the object
+            if ($parsedData->email !== null && $parsedData->fullName !== null && $parsedData->serialNumber !== null) {
+                return $parsedData;
+            }
+        }
+
+        if ($parsedData->fullName === null) {
+            if (isset($server['SSL_CLIENT_S_DN_CN'])) {
+                $parsedData->parseCN($server['SSL_CLIENT_S_DN_CN']);
+                if ($parsedData->fullName === null) {
+                    throw new \Exception("Unable to extract CN from DN");
+                }
+            } else {
+                throw new \Exception("Unable to obtain DN from certificate");
+            }
+        }
+
+        if ($parsedData->email === null) {
+            if (isset($_SERVER['SSL_CLIENT_SAN_Email_0'])) {
+                $parsedData->email = $_SERVER['SSL_CLIENT_SAN_Email_0'];
+            } else {
+                throw new \Exception("Unable to retrieve e-mail address from certificate");
+            }
+        }
+
+        if ($parsedData->serialNumber === null) {
+            if (isset($_SERVER['SSL_CLIENT_M_SERIAL'])) {
+                // In this server variable the serial number is stored into an HEX format,
+                // while openssl_x509_parse function provides it in DEC format.
+                // Here a hex->dec conversion is performed, in order to store the
+                // serial number in a consistent format:
+                $hexSerial = $_SERVER['SSL_CLIENT_M_SERIAL'];
+                // TODO
+            } else {
+                throw new Exception("Unable to retrieve serial number from certificate");
+            }
+        }
+
+        return $parsedData;
+    }
+
+}
diff --git a/config.php b/config.php
index 53d270460b050d208cb0bd061f6c7305b8ef6826..b29bb713eb6510684880fcd12af4297160b3ea8b 100644
--- a/config.php
+++ b/config.php
@@ -72,3 +72,10 @@ $AUTHENTICATION_METHODS = array(
         )
     )
 );
+
+$GROUPER = array(
+    //'serviceBaseURL' => 'https://sso.ia2.inaf.it/grouper-ws/servicesRest',
+    'wsdlURL' => 'http://localhost:8087/grouper-ws/services/GrouperService_v2_3?wsdl',
+    'user' => 'GrouperSystem',
+    'password' => '***REMOVED***'
+);
diff --git a/css/style.css b/css/style.css
index f44bc67f44b4bf00b74b952b30650633980f908d..f76e0fa5c0b4dcda79337c71b3593d90e619a635 100644
--- a/css/style.css
+++ b/css/style.css
@@ -103,7 +103,7 @@ body {
     margin-bottom: 8px
 }
 
-.callback-title {
+.page-title {
     margin-top: 0;
     font-weight: bold;
     color: #24388e;
@@ -121,3 +121,24 @@ body {
 .panel-default > .panel-heading {
     background-image: linear-gradient(to bottom,#f5f5f5 0,#ccc 100%);
 }
+
+.table-like {
+    display: table;
+    width: 100%;
+}
+
+.table-like .table-row {
+    display: table-row;
+}
+
+.table-row .table-cell {
+    display: table-cell;
+    vertical-align: middle;
+}
+
+.join-icon-cell {
+    min-width: 100px;
+    text-align: center;
+    font-size: 30px;
+}
+
diff --git a/include/front-controller.php b/include/front-controller.php
index 28c26903c7951c8e86a48fd96d0a407d14ed5f90..7ad325abcf2c337dc533476f9ed01cac95a1815a 100644
--- a/include/front-controller.php
+++ b/include/front-controller.php
@@ -37,3 +37,67 @@ Flight::route('/facebook', function() {
     startSession();
     Flight::redirect('/oauth2/facebook_login.php');
 });
+
+Flight::route('/eduGAIN', function() {
+    startSession();
+    Flight::redirect('/saml2/aai.php');
+});
+
+Flight::route('/x509', function() {
+    startSession();
+    Flight::redirect('/x509/certlogin.php');
+});
+
+Flight::route('GET /confirm-join', function() {
+    $token = Flight::request()->query['token'];
+
+    if ($token === null) {
+        http_response_code(422);
+        die("Token not found");
+    }
+
+    $dao = RAP\DAO::get();
+
+    $userIds = $dao->findJoinRequest($token);
+    if ($userIds === null) {
+        http_response_code(422);
+        die("Invalid token");
+    }
+
+    $applicantUser = $dao->findUserById($userIds[0]);
+    $targetUser = $dao->findUserById($userIds[1]);
+
+    Flight::render('confirm-join.php', array('title' => 'RAP',
+        'token' => $token,
+        'applicantUser' => $applicantUser,
+        'targetUser' => $targetUser));
+});
+
+Flight::route('POST /confirm-join', function() {
+
+    $token = Flight::request()->data['token'];
+
+    if ($token === null) {
+        http_response_code(422);
+        die("Token not found");
+    }
+
+    $dao = RAP\DAO::get();
+
+    $userIds = $dao->findJoinRequest($token);
+    if ($userIds === null) {
+        http_response_code(422);
+        die("Invalid token");
+    }
+
+    RAP\UserHandler::joinUsers($userIds[0], $userIds[1]);
+    $dao->deleteJoinRequest($token);
+
+    // Force user to relogin to see changes to him/her identities
+    session_start();
+    session_destroy();
+
+    global $BASE_PATH;
+    Flight::render('join-success.php', array('title' => 'Success - RAP Join Request',
+        'basePath' => $BASE_PATH));
+});
diff --git a/include/gui-backend.php b/include/gui-backend.php
index 6087bb3277fd8abaf3529aa0d07c55c3b658e518..2506bbbb735495d2fe3c157cf97f72900900863d 100644
--- a/include/gui-backend.php
+++ b/include/gui-backend.php
@@ -38,10 +38,12 @@ Flight::route('POST /join', function() {
     global $session;
 
     $selectedUserIndex = Flight::request()->data['selectedUserIndex'];
-    $targetUserId = $session->userSearchResults[$selectedUserIndex]->getUserId();
+    $selectedSearchResult = $session->userSearchResults[$selectedUserIndex];
+    $targetUserId = $selectedSearchResult->getUser()->id;
 
     $token = RAP\Util::createNewToken();
     RAP\DAO::get()->createJoinRequest($token, $session->user->id, $targetUserId);
+    RAP\MailSender::sendJoinEmail($selectedSearchResult->getUser(), $session->user);
 
-    echo "";
+    echo $selectedSearchResult->userDisplayText;
 });
diff --git a/include/init.php b/include/init.php
index cfb824c825299a2ea4d47908c6fb0517ffa4a0c7..f74e1e30936f67149c51f66441eae2dc70e761c5 100644
--- a/include/init.php
+++ b/include/init.php
@@ -34,9 +34,12 @@ spl_autoload_register(function ($class_name) {
     }
 });
 
+// Loading dependecy classes
 include ROOT . '/vendor/autoload.php';
+// Loading configuration
 include ROOT . '/config.php';
 
+// Setup logging
 $log = new Monolog\Logger('mainLogger');
 $log->pushHandler(new Monolog\Handler\StreamHandler($LOG_PATH, $LOG_LEVEL));
 
diff --git a/include/rest-web-service.php b/include/rest-web-service.php
index e913e8a70c73feb2bfd76d9f780e3e7e4102439f..2eca4f1a0dc0194785daa851b242d21475030ab3 100644
--- a/include/rest-web-service.php
+++ b/include/rest-web-service.php
@@ -9,14 +9,14 @@ $WS_PREFIX = '/ws';
 Flight::route('GET ' . $WS_PREFIX . '/user-info', function() {
 
     $token = Flight::request()->query['token'];
-    $userData = RAP\DAO::get()->findTokenData($token);
+    $userData = RAP\DAO::get()->findLoginToken($token);
 
     if (is_null($userData)) {
         http_response_code(404);
         die("Token not found");
     }
 
-    RAP\DAO::get()->deleteToken($token);
+    RAP\DAO::get()->deleteLoginToken($token);
 
     header('Content-Type: text/plain');
     echo $userData;
@@ -40,3 +40,6 @@ Flight::route('GET ' . $WS_PREFIX . '/user', function() {
     $users = RAP\DAO::get()->searchUser($searchText);
     echo json_encode($users);
 });
+
+Flight::route('GET ' . $WS_PREFIX . '/test', function() {
+});
diff --git a/include/user-data.php b/include/user-data.php
new file mode 100644
index 0000000000000000000000000000000000000000..755cfcd9acfa5a483f55604cdd0af9041263a6fa
--- /dev/null
+++ b/include/user-data.php
@@ -0,0 +1,28 @@
+<?php foreach ($user->identities as $identity) { ?>
+    <dl class="dl-horizontal">
+        <dt>Type</dt>
+        <dd><?php echo $identity->type; ?></dd>
+        <dt>E-mail</dt>
+        <dd><?php echo $identity->email; ?></dd>
+        <?php if ($identity->eppn !== null) { ?>
+            <dt>EduPersonPrincipalName</dt>
+            <dd><?php echo $identity->eppn; ?></dd>
+        <?php } ?>
+        <?php if ($identity->username !== null) { ?>
+            <dt>Username</dt>
+            <dd><?php echo $identity->username; ?></dd>
+        <?php } ?>
+        <?php if ($identity->name !== null) { ?>
+            <dt>Name</dt>
+            <dd><?php echo $identity->name; ?></dd>
+        <?php } ?>
+        <?php if ($identity->surname !== null) { ?>
+            <dt>Surname</dt>
+            <dd><?php echo $identity->surname; ?></dd>
+        <?php } ?>
+        <?php if ($identity->institution !== null) { ?>
+            <dt>Institution</dt>
+            <dd><?php echo $identity->institution; ?></dd>
+        <?php } ?>
+    </dl>
+<?php } ?>
diff --git a/js/script.js b/js/script.js
index 2ee30793e57f581b539ac38f219a31fc289db004..bc1a24912f0b0cf0963f036dafa5204d193bf5ac 100644
--- a/js/script.js
+++ b/js/script.js
@@ -20,11 +20,16 @@
     }
 
     function sendJoinRequest() {
+
         $userSelector = $('#user-selector-group select');
         var selectedUserIndex = $userSelector.val();
+
         if (selectedUserIndex !== null) {
             $.post('join', {selectedUserIndex: selectedUserIndex}, function (response) {
-                console.log(response);
+                $('#search-user-modal').modal('hide');
+                $infoAlert = $('#info-message-alert');
+                $infoAlert.find('.info-message').text("An e-mail has been sent to " + response + " primary e-mail address.");
+                $infoAlert.removeClass('hide');
             });
         }
     }
@@ -43,6 +48,10 @@
 
         // Add click event handler to join request button
         $(document).on('click', '#send-join-request-btn', sendJoinRequest);
+
+        $(document).on('click', '#info-message-alert .close', function () {
+            $('#info-message-alert').addClass('hide');
+        });
     });
 
 })(jQuery);
\ No newline at end of file
diff --git a/saml2/aai.php b/saml2/aai.php
new file mode 100644
index 0000000000000000000000000000000000000000..ff23b16b8e14287341a5619072017bf7d6ecadda
--- /dev/null
+++ b/saml2/aai.php
@@ -0,0 +1,52 @@
+<?php
+
+/* ----------------------------------------------------------------------------
+ *               INAF - National Institute for Astrophysics
+ *               IRA  - Radioastronomical Institute - Bologna
+ *               OATS - Astronomical Observatory - Trieste
+ * ----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2016 Istituto Nazionale di Astrofisica
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License Version 3 as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+include '../include/init.php';
+startSession();
+
+if (isset($_SERVER['Shib-Session-ID'])) {
+
+    $eppn = $_SERVER['eppn'];
+
+    $user = RAP\UserHandler::findUserByIdentity(RAP\Identity::EDU_GAIN, $eppn, null);
+
+    if ($user === null) {
+        $user = new RAP\User();
+
+        $identity = new RAP\Identity(RAP\Identity::EDU_GAIN);
+        $identity->email = $_SERVER['mail'];
+        $identity->name = $_SERVER['givenName'];
+        $identity->surname = $_SERVER['sn'];
+        $identity->typedId = $eppn;
+        //$_SERVER['Shib-Identity-Provider']
+
+        $user->addIdentity($identity);
+
+        RAP\UserHandler::saveUser($user);
+    }
+
+    RAP\CallbackHandler::manageLoginRedirect($user);
+} else {
+    throw new Exception("Shib-Session-ID not found!");
+}
diff --git a/views/confirm-join.php b/views/confirm-join.php
new file mode 100644
index 0000000000000000000000000000000000000000..60ef13782a4f7d2a4dbec215895f46f8ee2a15e3
--- /dev/null
+++ b/views/confirm-join.php
@@ -0,0 +1,47 @@
+<?php
+include 'include/header.php';
+?>
+
+<h1 class="text-center page-title">Confirm join request</h1>
+
+<br/>
+<div class="table-like">
+    <div class="table-row">
+        <div class="table-cell">
+            <div class="panel panel-default">
+                <div class="panel-body">
+                    <?php
+                    $user = $applicantUser;
+                    include 'include/user-data.php';
+                    ?>        
+                </div>
+            </div>
+        </div>
+        <div class="table-cell join-icon-cell text-success">
+            <span class="glyphicon glyphicon-transfer"></span>
+        </div>
+        <div class="table-cell">
+            <div class="panel panel-default">
+                <div class="panel-body">
+                    <?php
+                    $user = $targetUser;
+                    include 'include/user-data.php';
+                    ?>        
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+
+<div class="row">
+    <div class="col-xs-12 text-center">
+        <p>Pressing the following button the identities listed below will be joined.</p>
+        <form action="confirm-join" method="POST">
+            <input type="hidden" name="token" value="<?php echo $token; ?>" />
+            <input type="submit" class="btn btn-success btn-lg" value="Join users" />
+        </form>
+    </div>
+</div>
+
+<?php
+include 'include/footer.php';
diff --git a/views/index.php b/views/index.php
index 5a27057e4d0140edb905b53fd92886f2dd0b1ff9..e7eb9f06a9f7057681625e83b693213b223c40b5 100644
--- a/views/index.php
+++ b/views/index.php
@@ -5,7 +5,7 @@ include 'include/header.php';
 <?php if ($session->user === null) { ?>
     <div class="row">
         <div class="col-xs-12">
-            <h1 class="text-center callback-title"><?php echo $session->getCallbackTitle(); ?></h1>
+            <h1 class="text-center page-title"><?php echo $session->getCallbackTitle(); ?></h1>
         </div>
     </div>
     <div class="row">
@@ -71,6 +71,15 @@ include 'include/header.php';
         </div>
     </div>
 <?php } else { ?>
+    <div class="row">
+        <div class="col-xs-12">
+            <div class="alert alert-success hide" id="info-message-alert">
+                <button type="button" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+                <span class="glyphicon glyphicon-info-sign"></span>
+                <span class="info-message"></span>
+            </div>
+        </div>
+    </div>
     <div class="row">
         <div class="col-sm-5 col-xs-12">
             <div class="panel panel-default">
@@ -78,34 +87,10 @@ include 'include/header.php';
                     <h3 class="panel-title">Your identities</h3>
                 </div>
                 <div class="panel-body">
-                    <?php foreach ($session->user->identities as $identity) { ?>
-                        <dl class="dl-horizontal">
-                            <dt>Type</dt>
-                            <dd><?php echo $identity->type; ?></dd>
-                            <dt>E-mail</dt>
-                            <dd><?php echo $identity->email; ?></dd>
-                            <?php if ($identity->eppn !== null) { ?>
-                                <dt>EduPersonPrincipalName</dt>
-                                <dd><?php echo $identity->eppn; ?></dd>
-                            <?php } ?>
-                            <?php if ($identity->username !== null) { ?>
-                                <dt>Username</dt>
-                                <dd><?php echo $identity->username; ?></dd>
-                            <?php } ?>
-                            <?php if ($identity->name !== null) { ?>
-                                <dt>Name</dt>
-                                <dd><?php echo $identity->name; ?></dd>
-                            <?php } ?>
-                            <?php if ($identity->surname !== null) { ?>
-                                <dt>Surname</dt>
-                                <dd><?php echo $identity->surname; ?></dd>
-                            <?php } ?>
-                            <?php if ($identity->institution !== null) { ?>
-                                <dt>Institution</dt>
-                                <dd><?php echo $identity->institution; ?></dd>
-                            <?php } ?>
-                        </dl>
-                    <?php } ?>
+                    <?php
+                    $user = $session->user;
+                    include 'include/user-data.php';
+                    ?>
                 </div>
             </div>
         </div>
diff --git a/views/join-success.php b/views/join-success.php
new file mode 100644
index 0000000000000000000000000000000000000000..0587bd0cf79f5df8a4d8d6f405584d17b75d7460
--- /dev/null
+++ b/views/join-success.php
@@ -0,0 +1,15 @@
+<?php
+include 'include/header.php';
+?>
+
+<h1 class="text-center page-title">Success</h1>
+
+<br/>
+<div class="text-center">
+    <p>Your identities have been joined!</p>
+    <br/>
+    <p><a href="<?php echo $basePath; ?>">Return to index</a></p>
+</div>
+
+<?php
+include 'include/footer.php';
diff --git a/x509/certlogin.php b/x509/certlogin.php
new file mode 100644
index 0000000000000000000000000000000000000000..1694ecff4558c966cc434ac5a37b215f7af3602f
--- /dev/null
+++ b/x509/certlogin.php
@@ -0,0 +1,49 @@
+<?php
+
+/* ----------------------------------------------------------------------------
+ *               INAF - National Institute for Astrophysics
+ *               IRA  - Radioastronomical Institute - Bologna
+ *               OATS - Astronomical Observatory - Trieste
+ * ----------------------------------------------------------------------------
+ *
+ * Copyright (C) 2016 Istituto Nazionale di Astrofisica
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License Version 3 as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 51
+ * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+
+include '../include/init.php';
+startSession();
+
+if (isset($_SERVER['SSL_CLIENT_VERIFY']) && isset($_SERVER['SSL_CLIENT_V_REMAIN']) &&
+        $_SERVER['SSL_CLIENT_VERIFY'] === 'SUCCESS' && $_SERVER['SSL_CLIENT_V_REMAIN'] > 0) {
+
+    $email = null;
+
+    if (isset($_SERVER['SSL_CLIENT_CERT'])) {
+
+        $MYx509 = openssl_x509_parse($_SERVER['SSL_CLIENT_CERT']);
+
+        if (isset($MYx509["extensions"]["subjectAltName"])) {
+            $AyAlt = explode(":", $MYx509["extensions"]["subjectAltName"]);
+            if ($AyAlt[0] === "email") {
+                $email = $AyAlt[1];
+            }
+        }
+    }
+
+    if (isset($_SERVER['SSL_CLIENT_S_DN_CN'])) {
+        $fullName = $_SERVER['SSL_CLIENT_S_DN_CN'];
+    }
+}