Interfacer les sessions PHP avec Node.js

Comment partager l'authentification de PHP avec Node.js ?

5 septembre 2017

Introduction

Cet article est un retour d'expérience sur la manière de partager la gestion des utilisateurs et notamment les sessions entre PHP et Node.js.

Nous avons réalisé une application Web popmind faisant collaborer Node.js et PHP.

Un utilisateur peut accéder aux WebSockets de Node.js uniquement si il a été préalablement authentifié avec PHP.
Node.js doit pouvoir également accéder aux informations de l'utilisateur gérées par PHP (Nom, email, etc ...).

popmind schema 1

Rappel sur les sessions

Une session permet notamment au serveur d'identifier l'utilisateur sans devoir lui redemander à chaque requête HTTP son identifiant et son mot de passe.

Création de la session

Lorsque l'utilisateur se connecte à l'aide de son identifiant / mot de passe :

1) le serveur génère un identifiant unique, représenté sous la forme d'une chaîne de caractères aléatoire
2) le serveur stocke cet identifiant unique en mémoire
3) le serveur communique à l'utilisateur l'identifiant unique
4) le navigateur internet de l'utilisateur stocke cet identifiant unique

Le serveur et le navigateur internet de l'utilisateur sont les seuls à avoir connaissance de cet identifiant unique.

create user session

Remarque : Le cookie contenant l'identifiant unique de session est automatiquement effacé au-delà d'une certaine durée ou lorsque l'utilisateur se déconnecte.

Vérification de la session

Le navigateur ajoute à chaque requête auprès du serveur l'identifiant unique de la session. À réception de la requête, le serveur vérifie que l'identifiant unique correspond bien à l'un de ceux en sa possession.

Suite à cette vérification, le serveur peut en conclure que :

verify user session

Partager des données entre PHP et Node.js

Les informations de sessions et d'utilisateurs doivent être partagées entre PHP et Node.js.

PHP et Node.js fonctionnent dans des processus indépendants.

Les systèmes d'exploitations (Linux, Windows, ...) proposent nativement plusieurs outils de communication entre processus :

Et il existe des solutions open-source de partage de données entre plusieurs machines :

Dans notre cas PHP et Node.js fonctionnent sur le même serveur, nous avons donc choisi la base de données embarquée SQLite pour partager des données

Remarque : le connecteur SQLite est nativement supporté par PHP et correctement maintenu sous Node.js.

Côté PHP

Les sessions sont nativement gérées par PHP (avec session_start), tandis que l'authentification, l'inscription, ... des utilisateurs sont gérés par Symfony avec FOSUserBundle.

La gestion des utilisateurs

FOSUserBundle stocke les informations dans une table SQL de la base de données.

Symfony doit être configuré pour indiquer à FOSUserBundle d'utiliser SQLite comme moyen de stockage.

Chaque utilisateur inscrit est identifié par un numéro unique (voir colonne id ci-dessous).

sql table user

La gestion des sessions

Spécificités

PHP sauvegarde par défaut les sessions dans un répertoire dédié.

PHP permet de modifier à l'aide de l'interface SessionHandlerInterface la manière et l'endroit où sont stockées les sessions.

Symfony a une classe appelé PdoSessionHandler implémentant SessionHandlerInterface et permettant de stocker les sessions dans une table SQL d'une base de donnée.

Cette classe stocke les informations de sessions dans la table appelée session et dans la colonne data les informations spécifiques associées à une session (voir colonne data ci-dessous).

sql table session

Les données stockées dans data permettent notamment de lier une session avec l'identité de l'utilisateur.

sql link user session

Mais les objets PHP contenant les données sur les sessions sont sérialisées dans la colonne data avec un format propriétaire.

Pour des raisons d'interropérabilité, l'identifiant de l'utilisateur est stocké dans une colonne SQL dédiée, cela nécessite de modifier PdoSessionHandler.

Modification de PdoSessionHandler

Recopier le fichier /vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php dans le projet, par exemple src/AppBundle/Service/PdoSessionHandler.php.

Les modifications principales :

public function __construct($pdoOrDsn = null, array $options = array(), SecurityContext $context)
{
    ...
    $this->context = $context;
}
public function write($sessionId, $data)
{
    $userId = $this->context->isGranted('IS_AUTHENTICATED_REMEMBERED') || $this->context->isGranted('IS_AUTHENTICATED_FULLY') ? $this->context->getToken()->getUser()->getId() : null;
    ...
    $updateStmt->bindValue(':user_id', $userId, \PDO::PARAM_INT);
    ...
}

Ci-dessous une partie du git diff entre la version originale et la version modifiée de PdoSessionHandler.

screen capture git diff PdoSessionHandler

La table session devient :

sql table session

Remarque : Les sessions sont automatiquement supprimées de la table.

Configuration du nouveau service

Ajout dans le fichier app/config/services.yml :

pdo.db_options:
    db_table: session
    db_id_col: id
    db_data_col: data
    db_time_col: time
    db_lifetime_col: lifetime
    db_user_id_col: user_id
    lock_mode: 0

pdo:
    class: PDO
    arguments:
        - "sqlite:%database_path%"
        - "%database_user%"
        - "%database_password%"
        - "%pdo.options%"

session.handler.pdo:
    class:     AppBundle\Service\PdoSessionHandler
    public:    false
    arguments: ["@pdo", "%pdo.db_options%", "@security.context" ]

Modification du fichier app/config/config.yml pour utiliser le service :

framework:
    session:
        handler_id: session.handler.pdo

Côté Node.js

Nous utilisons les modules npm :

La fonctionnalité middleware de socket.io est utilisé pour vérifier la validité de la session PHP au moment de la connexion.

ws.use(function(client, next) {
    // récupération des cookies
    let cookies = cookie.parse(client.request.headers.cookie); 

    // vérifier dans la base de données la validité de la session PHP et récupèrer l'identifiant de l'utilisateur associé
    db.get(`SELECT user.id AS id FROM session, fos_user_user AS user WHERE session.id="${cookies.PHPSESSID}" AND session.user_id = user.id`,    
        function(error, row) {
            // si erreur ou la session est inconnue, le navigateur n'est pas autorisé à communiquer en mode websocket
            if (error) { 
                next(new Error(error));
            } else if (row) {
                // stocke dans le websocket ouvert l'identifiant de l'utilisateur
                client.user_id = row.id;   
                next();
            } else {
                next(new Error('not authorized'));
            }
        }
    );
});