Commit 97c4886b authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Added comments and documentation + security fix

parent 4a9d24b4
# RAP 2
# RAP IA2
## Installation
## Installation and configuration
Requirements:
* Apache httpd server (tested on Apache/2.4.6)
* PHP (5.4+), composer for dependecies
* MySQL/MariaDB (tested on MariaDB 5.5.52)
### PHP
Put RAP sources in `/var/www/html/rap-ia2`
For installing PHP dependencies run:
......@@ -8,7 +18,82 @@ For installing PHP dependencies run:
Install also the bcmath PHP package (used in X.509 parser).
To setup the database edit scripts in the sql folder and run them:
### MySQL
Create a dedicated database and user:
CREATE DATABASE rap;
CREATE USER rap@localhost IDENTIFIED BY 'XXXXXX';
GRANT ALL PRIVILEGES ON rap.* TO rap@localhost;
Enable the event scheduler:
* open MySQL configuration file (e.g. /etc/my.cnf)
* set `event_scheduler=1`
* restart MySQL
Then run the setup script:
mysql -u root -p < sql/setup-database.sql
### Apache (httpd)
* Configure a valid HTTPS certificate on the server
* Configure X.509 client certificate authentication:
<Directory /var/www/html/rap-ia2/auth/x509/>
Options Indexes FollowSymLinks
AllowOverride None
Order allow,deny
allow from all
SSLVerifyClient require
SSLVerifyDepth 10
SSLOptions +ExportCertData
</Directory>
* Shibboleth authentication:
<Directory /var/www/html/rap-ia2/auth/saml2/>
AuthType shibboleth
ShibRequestSetting requireSession 1
Require valid-user
</Directory>
* Protect log directory:
<Directory /var/www/html/rap-ia2/logs/>
Order deny,allow
Deny From All
</Directory>
* Protect RAP Web Service in Basic-Auth:
<Location "/rap-ia2/ws">
AuthType basic
AuthName RAP
AuthUserFile apachepasswd
Require valid-user
</Location>
* Then creates a password file for RAP Web Service Basic-Auth:
* `cd /etc/httpd/`
* `htpasswd -c apachepasswd rap`
* The last command creates an hashed password for an user "rap" and store it in a file named apachepasswd.
* Finally, restart the Apache server.
### Social networks
Before using social API it is necessary to register an application on each social network and obtain API keys and secrets:
* https://console.developers.google.com
* https://www.linkedin.com/developer/apps
* https://developers.facebook.com/apps
### Configuration file
Copy the `config-example.php` into `config.php` and edit it for matching your needs.
## Additional information and developer guide
mysql -u root -p < sql/create-db-and-user.sql
mysql -u root -p rap < sql/create-tables.sql
See the wiki: https://www.ict.inaf.it/gitlab/zorba/rap-ia2/wikis/home
......@@ -22,9 +22,12 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* This page uses the Facebook API for generating the redirect URL to use for Facebook login */
include '../../include/init.php';
startSession();
// Retrieve Facebook configuration
$Facebook = $AUTHENTICATION_METHODS['Facebook'];
$fb = new Facebook\Facebook([
......@@ -35,7 +38,8 @@ $fb = new Facebook\Facebook([
$helper = $fb->getRedirectLoginHelper();
$permissions = ['email']; // Optional permissions
$permissions = ['email']; // Optional permissions: we need user email
$loginUrl = $helper->getLoginUrl($Facebook['callback'], $permissions);
header("Location: $loginUrl");
......
......@@ -22,9 +22,12 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* Facebook callback page */
include '../../include/init.php';
startSession();
// Retrieve Facebook configuration
$Facebook = $AUTHENTICATION_METHODS['Facebook'];
$fb = new Facebook\Facebook([
......@@ -80,9 +83,11 @@ $fbUser = $response->getGraphUser();
$typedId = $fbUser["id"];
// Search if the user is already registered into RAP using the Facebook ID.
$user = $userHandler->findUserByIdentity(RAP\Identity::FACEBOOK, $typedId);
if ($user === null) {
// Create new user
$user = new RAP\User();
$identity = new RAP\Identity(RAP\Identity::FACEBOOK);
......
......@@ -22,9 +22,12 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* Google redirect and callback page */
include '../../include/init.php';
startSession();
// Retrieve Google configuration
$Google = $AUTHENTICATION_METHODS['Google'];
$client = new Google_Client(array(
......@@ -53,7 +56,7 @@ if (isset($_GET['code'])) {
if ($client->getAccessToken()) {
// Query web service
// Query web service for retrieving user information
$service = new Google_Service_People($client);
try {
......@@ -74,9 +77,11 @@ if ($client->getAccessToken()) {
$typedId = explode('/', $res->getResourceName())[1];
// Search if the user is already registered into RAP using the Google ID.
$user = $userHandler->findUserByIdentity(RAP\Identity::GOOGLE, $typedId);
if ($user === null) {
// Create new user
$user = new RAP\User();
$identity = new RAP\Identity(RAP\Identity::GOOGLE);
......
......@@ -22,9 +22,12 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* This page redirects to LinkedIn login page */
include '../../include/init.php';
startSession();
// Retrieve LinkedIn configuration
$LinkedIn = $AUTHENTICATION_METHODS['LinkedIn'];
$url = "https://www.linkedin.com/oauth/v2/authorization?response_type=code";
......
......@@ -22,9 +22,12 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* LinkedIn callback page. Curl is used, because LinkedIn doesn't provide official PHP API. */
include '../../include/init.php';
startSession();
// Retrieve LinkedIn configuration
$LinkedIn = $AUTHENTICATION_METHODS['LinkedIn'];
if (!isset($_REQUEST['code'])) {
......@@ -100,9 +103,11 @@ if ($info2['http_code'] === 200) {
$typedId = $data['id'];
// Search if the user is already registered into RAP using the LinkedIn ID.
$user = $userHandler->findUserByIdentity(RAP\Identity::LINKEDIN, $typedId);
if ($user === null) {
// Create new user
$user = new RAP\User();
$identity = new RAP\Identity(RAP\Identity::LINKEDIN);
......
......@@ -22,16 +22,29 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* This page MUST be protected by Shibboleth authentication
* On Apache httpd:
* AuthType shibboleth
* ShibRequestSetting requireSession 1
* Require valid-user
*/
include '../../include/init.php';
startSession();
if (isset($_SERVER['Shib-Session-ID'])) {
// Retrieving eduPersonPrincipalName (eppn)
$eppn = $_SERVER['eppn'];
// Search if the user is already registered into RAP using the eppn.
// The persistent id should be a more appropriate identifier, however at IA2
// we need to import all INAF user into RAP, even if they will never register,
// and in that case we know only their eppn.
$user = $userHandler->findUserByIdentity(RAP\Identity::EDU_GAIN, $eppn);
if ($user === null) {
// Creating a new user
$user = new RAP\User();
$identity = new RAP\Identity(RAP\Identity::EDU_GAIN);
......
......@@ -22,6 +22,12 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* This page must be protected by client certificate authentication
* On Apache httpd:
* SSLVerifyClient require
* SSLVerifyDepth 10
* SSLOptions +ExportCertData
*/
include '../../include/init.php';
startSession();
......@@ -44,10 +50,20 @@ function saveUserFromX509Data($x509Data) {
$userHandler->saveUser($user);
$session->x509DataToRegister = null;
$session->save();
return $user;
}
/**
* We want to extract name and surname from the X.509 certificate, however X.509
* puts name and surname together (inside the CN field).
* If name and surname are single words it is possible to retrieve them splitting
* on the space character, otherwise the user has to choose the correct combination.
* In that case partial X.509 data is temporarily stored into the user session and
* the page views/x509-name-surname.php is shown to the user before completing the
* registration, in order to allow him/her selecting the correct name and surname.
*/
if ($session->x509DataToRegister !== null && $session->x509DataToRegister->name !== null) {
$user = saveUserFromX509Data($session->x509DataToRegister);
......
......@@ -24,6 +24,9 @@
namespace RAP;
/**
* Manage callback URL validation and redirection
*/
class CallbackHandler {
private $dao;
......@@ -49,7 +52,11 @@ class CallbackHandler {
}
/**
* returns null if the callback URL is not listed in configuration file.
* Each callback has a title and a logo in order to avoid confusion in users
* and show in which application they are logging in using RAP.
* @param type $callbackURL
* @return type the callback title or null if the callback URL is not listed
* in configuration file or it doesn't have a title.
*/
public function getCallbackTitle($callbackURL) {
......@@ -62,6 +69,13 @@ class CallbackHandler {
return null;
}
/**
* Each callback has a title and a logo in order to avoid confusion in users
* and show in which application they are logging in using RAP.
* @param type $callbackURL
* @return type the callback logo or null if the callback URL is not listed
* in configuration file or it doesn't have a logo.
*/
public function getCallbackLogo($callbackURL) {
foreach ($this->callbacks as $callback) {
......
......@@ -24,46 +24,104 @@
namespace RAP;
/**
* Data Access Object interface for accessing the RAP database.
* Current implementations: RAP\MySQLDAO
*/
interface DAO {
/**
* @return type PDO object for accessing the database
*/
function getDBHandler();
/**
* Store a new login token into the database.
* @param type $token login token
* @param type $userId
*/
function createLoginToken($token, $userId);
/**
* Retrieve the user ID from the login token.
* @param type $token
* @return type user ID
*/
function findLoginToken($token);
/**
* Delete a login token from the database. This happens when the caller
* application has received the token and used it for retrieving user
* information from the token using the RAP REST web service.
* @param type $token login token
*/
function deleteLoginToken($token);
/**
* Return the new identity ID.
* Create a new identity.
* @param type $userId the user ID associated to that identity
* @return type the new identity ID
*/
function insertIdentity(Identity $identity, $userId);
/**
* Return the new user ID.
* Create a new user.
* @return the new user ID.
*/
function createUser();
/**
* @return RAP\User an user object, null if nothing was found.
*/
function findUserById($userId);
function setPrimaryIdentity($userId, $identityId);
/**
* Return a User object, null if nothing was found.
* Retrieve the user associated to a given identity using the typedId.
* @param type $type Identity type (EDU_GAIN, X509, GOOGLE, ...)
* @param type $identifier value used to search the identity in the database
* @param type $typedId typed unique value used to search the identity in the database
* @return RAP\User an user object, null if nothing was found.
*/
function findUserByIdentity($type, $identifier);
function findUserByIdentity($type, $typedId);
/**
* Retrieve a set of users matching a given search text.
* @param type $searchText name, surname or email
* @return list of RAP\User objects
*/
function searchUser($searchText);
/**
* Store into the database information about a new join request.
* @param type $token join token
* @param type $applicantUserId the user asking for the join
* @param type $targetUserId the user target of the join
*/
function createJoinRequest($token, $applicantUserId, $targetUserId);
/**
* Retrieve join request information.
* @param type $token join token
* @return an array of 2 elements having the applicant user id at the first
* position and the target user id at the second position; null if nothing
* was found.
* @throws Exception if multiple requests has been found for the same token.
*/
function findJoinRequest($token);
function deleteUser($userId);
/**
* Perform a join request.
* @param type $userId1 the user that will receive all identities
* @param type $userId2 the user that will lost the identities and will be
* deleted from the database
*/
function joinUsers($userId1, $userId2);
/**
* When a join action is performed the join request data (join token and user
* identifiers) needs to be removed from the database.
* @param type $token join token
*/
function deleteJoinRequest($token);
}
......@@ -24,6 +24,9 @@
namespace RAP;
/**
* Data model for identities.
*/
class Identity {
const EDU_GAIN = "eduGAIN";
......@@ -31,9 +34,8 @@ class Identity {
const GOOGLE = "Google";
const FACEBOOK = "Facebook";
const LINKEDIN = "LinkedIn";
const LOCAL = "Local";
private static $ALLOWED_TYPES = [Identity::EDU_GAIN, Identity::X509, Identity::GOOGLE, Identity::FACEBOOK, Identity::LINKEDIN, Identity::LOCAL];
private static $ALLOWED_TYPES = [Identity::EDU_GAIN, Identity::X509, Identity::GOOGLE, Identity::FACEBOOK, Identity::LINKEDIN];
/**
* Identity id in the database. Mandatory field.
......@@ -46,23 +48,27 @@ class Identity {
public $type;
/**
* Data related to specific account type (shibboleth persistent id, facebook id, etc, ...). Mandatory field.
* Data related to specific account type (shibboleth persistent id,
* facebook id, certificate serial number, etc, ...). Mandatory field.
*/
public $typedId;
/**
* Primary email related to this identity. Mandatory field.
* User can have additional email addresses. These are stored into User class.
* @todo Maybe an user can have additional email addresses (e.g.: Google
* API provides them). Should we store them somewhere?
*/
public $email;
/**
* First name
* First name.
* This should be mandatory, however for old IA2 users we have only email address.
*/
public $name;
/**
* Last name / Family name
* This should be mandatory, however for old IA2 users we have only email address.
*/
public $surname;
......@@ -73,6 +79,9 @@ class Identity {
/**
* For eduGAIN identities.
* This is currently the same as the typedId for eduGAIN identity types, because
* at IA2 we need this (see the wiki). Use the Shibboleth persistent id should
* be a more appropriate typedId for these cases.
*/
public $eppn;
......
......@@ -24,6 +24,10 @@
namespace RAP;
/**
* Manage mail sending.
* Currently used only for join email messages.
*/
class MailSender {
private $serverName;
......@@ -34,6 +38,13 @@ class MailSender {
$this->basePath = $basePath;
}
/**
* Send the email for confirming the join request.
* @param \RAP\User $recipientUser user target of the join requests: he/she
* will receive the email containing the confirmation link
* @param \RAP\User $applicantUser user that have requested the join
* @param string $token the join token
*/
public function sendJoinEmail(User $recipientUser, User $applicantUser, $token) {
$subject = "IA2 RAP: Join request";
......
......@@ -26,6 +26,9 @@ namespace RAP;
use PDO;
/**
* MySQL implementation of the DAO interface. See comments on the DAO interface.
*/
class MySQLDAO implements DAO {
private $config;
......@@ -289,14 +292,6 @@ class MySQLDAO implements DAO {
}
}
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();
......
......@@ -24,6 +24,10 @@
namespace RAP;
/**
* This class wraps various objects that need to be stored into the session in
* order to provide an object oriented transparent session management.
*/
class SessionData {
private $dao;
......@@ -34,14 +38,26 @@ class SessionData {
public $userSearchResults;
public $x509DataToRegister;
/**
* @todo: move DAO away from here
*/
public function __construct(DAO $dao) {
$this->dao = $dao;
}
/**
* Store the data into the $_SESSION PHP variable
*/
public function save() {
$_SESSION['SessionData'] = $this;
}
/**
* Retrieve the SessionData object from the $_SESSION PHP variable. Create a
* new one if it is necessary.
* @param \RAP\DAO $dao
* @return \RAP\SessionData the SessionData object
*/
public static function get(DAO $dao) {
if (!isset($_SESSION['SessionData'])) {
......@@ -70,6 +86,12 @@ class SessionData {
return $this->callbackLogo;
}
/**
* Perform a user search and store the results inside the session. This is
* used for achieving the user selection using the dropdown menu in the join
* request modal.
* @param string $searchText
*/
public function searchUser($searchText) {
$users = $this->dao->searchUser($searchText);
......@@ -85,6 +107,12 @@ class SessionData {
$this->save();
}
/**
* Update the user data model stored into the session after the primary
* identity has changed, in order to avoid reading again the user data from
* the database.
* @param int $identityId
*/
public function updatePrimaryIdentity($identityId) {
foreach ($this->user->identities as $identity) {
$identity->primary = ($identity->id === $identityId);
......
......@@ -24,9 +24,14 @@
namespace RAP;
/**
* Data model for the user. An user is a set of identities.
*/
class User {
// User ID
public $id;
// List of identities
public $identities;
public function __construct() {
......@@ -43,6 +48,7 @@ class User {
return $identity->email;
}
}
// A primary identity MUST be defined
throw new \Exception("No primary identity defined for user " . $this->id);
}
......
......@@ -24,6 +24,9 @@
namespace RAP;
/**
* Perform operations on users.
*/