Authentification WebSocket avec les sessions Express 3.x et Socket.IO

Suite à mon premier article complet sur le sujet (« Authentification et WebSocket, avec Node.JS, Express, et Socket.IO »), beaucoup m’ont signalé que les exemples ne fonctionnent plus avec Express 3.x. C’est pas tout ça mais Express est quand-même officiellement en version 3 depuis un petit moment, donc il était grand temps de mettre à jour le code !

Je vous encourage à consulter les commentaires de l’article original (merci à Laurent et Will pour leurs discussions sur ce thème) pour de plus amples informations sur les petites surprises qu’on peut rencontrer 😉

J’en profite pour vous décrire l’intégralité de la manœuvre pour que vous voyez un cas de migration Express 2→3, avec des petites astuces en passant :) Le code est dispo sur github, et chaque étape est accompagnée d’un lien vers le commit correspondant.

Comment tout casser

Il suffit de mettre à jour les dépendances 😉 Petite astuce pour connaître le numéro de version d’un package sur npm : la commande npm show package version.

  • Express 3.0 (anciennement 2.4)
  • Connect 2.7 (anciennement 1.6)
  • Socket.io 0.9.3 (anciennement 0.7.x) et d’ailleurs la version 1.0 n’est plus très loin
  • EJS 0.8.3 (anciennement 0.4.0)

Le petit refresh n’est pas du luxe.

Petite astuce pour mettre à jour les dépendances de son package.json avec les dernières versions : npm install package@latest --save (au lieu d’aller chercher manuellement les numéros de version et éditer a la mano le fichier).

Une fois ceci fait, on a quelques warnings, et l’application ne fonctionne plus.

Ce qui a changé et qui fait tout péter

Express 3 génère maintenant un simple callback

Le message est plutôt clair :

Warning: express.createServer() is deprecated, express
applications no longer inherit from http.Server,
please use:

  var express = require("express");
  var app = express();

Grosso modo ça consiste à distinguer « app » et « server »

var app = express(); // Request handler
var server = http.createServer(app); // Standard way of creating HTTP server

Il s’agit à mon sens d’un changement plutôt sain et bienvenu : Express s’efface face au serveur, il n’est plus qu’un générateur de callbacks.

Mais Socket.io veut toujours un Server, pas un callback

Là encore, le message est plutôt clair :

Socket.IO’s `listen()` method expects an `http.Server` instance
as its first parameter. Are you migrating from Express 2.x to 3.x?
If so, check out the « Socket.IO compatibility » section at:
https://github.com/visionmedia/express/wiki/Migrating-from-2.x-to-3.x

Et une fois encore, c’est extrêmement simple. Jusqu’ici la migration est plutôt simple :)

On a perdu notre layout !

Express 3 a supprimé la notion de « layout ». Choix discutable à mon sens, c’est assez pénible à faire gérer par les moteurs de template simple. D’ailleurs il n’existe aucun moyen dans EJS pour le faire, il faut utiliser soit les includes (un peu old-school) soit ajouter cette notion soi-même. Le module ejs-locals le fait bien donc on va utiliser ça.

Un npm install --save ejs-locals et quelques menues modifications (déclarer le moteur, et ajouter les instructions ‘layout’ dans nos vues) plus tard, on a retrouvé notre superbe design (notre font, oui).

« parseCookie »: undefined is not a function

La partie triviale de la migration est terminée, maintenant on va entrer dans le vif du sujet. À ce stade l’application se lance sans avertissements, mais quand on se log le serveur crash avec ce petit message sympa. Quelques modifications dans l’API à traiter :)

En fait maintenant les cookies de session Express sont signés, et la méthode a été renommée pour refléter ce changement : on doit maintenant utiliser parseSignedCookie. La méthode parseCookie de connect a disparu ; si on souhaite lire un cookie non signé le plus simple reste d’utiliser le module cookie.

J’en profite pour supprimer la dépendance directe à connect : on utilise directement la version requise par Express, comme ça on est sûr d’utiliser la bonne version de l’API. On peut faire ça très simplement car require('package/node_modules/subpackage') marche comme on s’y attend :)

On va en profiter pour faire un point sur le fonctionnement des sessions et les différents niveaux de sécurité :

  • On va dire à Express comment lire les cookies. En stipulant un « secret », le parseur sera capable de lire les cookies signées par cette phrase. Le principe est que le contenu du cookie sera accompagné d’un hash calculé à partir du contenu original et d’une chaîne privée. Toute modification du cookie est rendue impossible si on ne connaît pas la chaîne secrète. Attention, ça ne protège pas du vol de cookie, mais de la forge de faux cookies.
  • On va ensuite dire à Express comment on veut stocker les sessions (ici en mémoire, mais en prod ce serait évidemment dans Redis ou Memcached par exemple). Là encore, on peut lui fournir un « secret » : il a le même rôle et permet de signer la valeur du cookie de session.

Il y a donc plusieurs méthodes pour gérer les cookies :

// app.use(express.cookieParser())
res.cookie('user', 'john') // unsigned cookie
// read with "req.cookies.user"
 
// app.use(express.cookieParser())
res.cookie('user', 'john', {signed: true}) // error: no secret provided !
 
// app.use(express.cookieParser('my secret'))
res.cookie('user', 'john', {signed: true}) // signed cookie
// read with "req.signedCookies.user"

On sait maintenant qu’on devra parser un cookie signé avec le secret qu’on a mis dans la configuration du middleware « session ».

Il suffit donc de parser les cookies puis vérifier (et nettoyer) le cookie « connect.sid » pour récupérer l’ID de session et fonctionner comme avant.

// Grab "cookie" and "connect" from express
var connect = require('express/node_modules/connect')
var cookie = require('express/node_modules/cookie')
 
// Read cookies
var cookies = cookie.parse(handshake.headers.cookies)
 
// Unsign sessionId cookie
var sessionID = connect.utils.parseSignedCookie(cookies['connect.sid'], 'session secret')
Dernière note sur les cookies signés

Les cookies sont « signés », pas « cryptés ». Par exemple avec comme secret « some private string » et un ID de session « Ssbeyo4z1LVRk2cZu2WVFYlT » on trouvera dans les cookies

connect.sid=s%3ASsbeyo4z1LVRk2cZu2WVFYlT.RMQypoysb8wWlEuZUDQ9c1FjtB1aKG7YwhL%2FEtH8U9I

Vous voyez qu’on y retrouve notre ID de session en clair.

Express ajoute « s: » en préfixe des cookies signés, et Connect signe les cookies en ajoutant « .hash » en suffixe de la valeur fournie. Si on souhaite économiser quelques millisecondes au détriment de la sécurité on peut donc simplifier le parsing du cookie signé :

var s = cookies['connect.sid']
var sessionID = s.slice(2, s.lastIndexOf('.'))

Je le déconseille fortement car on n’est plus prémuni de la forge de cookie de session, mais il est toujours intéressant de bien comprendre la différence entre « signé » et « crypté » 😉

Conclusion

Au final, notre migration a consisté en ces quelques points plutôt simples :

  • Distinction entre « app » et « server ».
  • Réimplémentation de la notion de layout.
  • Cookie de session signé.

Complexité assez peu élevée au final (29 add, 18 del), mais c’était l’occasion de rentrer un peu dans le détail 😉

Laisser un commentaire