Nodejs und WordPress mit gemeinsamer Authentifizierung

Das Problem

Bei snowhow verwenden wir WordPress als Content-Management-System. Die Benutzer-Accounts für die snowhow User werden auch in WordPress verwaltet. Warum? Das System ist sehr weit verbreitet, die User-Verwaltung sollte also ausgereift sein (ge-hashte Passwörter, Passwort per E-Mail zurücksetzen).

Außerhalb von WordPress als CMS setzen wir auf Nodejs und MongoDB. Die aufgezeichneten Freeride-Tracks und Skitouren kommen als GeoJSON direkt in die MongoDB und können dort via räumlicher Suche gefunden werden, herrlich unkompliziert. Aber wie kann der Nodejs-Server (express) auf die Authentifizierung von WordPress zugreifen?

  • Das node-Modul node-wordpress-auth funktionierte mit einer älteren Version von WordPress, war aber immer schon ein dirty hack.
  • Die Passport Authentifizierung via OAuth2 für WordPress wäre prinzipiell möglich (ein OAuth2-Provider-Plugin für WordPress vorausgesetzt), die Nuss wäre damit wohl mit dem Vorschlaghammer geknackt.
  • Als Lösung wählten wir den gemeinsamen Zugriff auf Session-Daten von PHP und Nodejs (hier kommt uns zugute, dass der Nodejs-Server durch den apache, der WordPress betreibt, ge-proxy-t wird). Wie das geht?

Die Lösung: Gemeinsamer Zugriff auf Session Daten

Klingt einfacher, als es dann war. Hier die Kurzversion:

  1. Die PHP Sessions in den memcached Speicher verlegen.
  2. Ein WordPress Plugin schreiben, das die User-ID in die Session speichert.
  3. Nodejs Module installieren: memcached cookies php-unserialize.
  4. Den Javascript Code anpassen, damit die Session-Daten ausgelesen und dekodiert werden können.

Im Detail sind dazu folgende Schritte notwendig:
ad 1.) Zuerst wird der memcached-Server installiert. Unter Ubuntu Linux ein leichtes mit

apt-get install memcached php5-memcached

Der Server startet (in der Standardeinstellung unter Ubuntu) automatisch auf Port 11211 und der Zugriff ist nur vom localhost möglich. Als nächstes gilt es die PHP Konfiguration anzupassen. Im Fall von Ubuntu legt man zum Beispiel die Datei /etc/php5/apache2/conf.d/30-memcached-sessions.ini mit folgendem Inhalt an:

session.save_handler = memcached
session.save_path='127.0.0.1:11211'

Jetzt nicht vergessen den apache neu zu starten, dann sollten die Sessions im memcached Server liegen.

ad 2.) WordPress verwaltet die Session in einem eigenen Cache, nicht in der PHP Session. Ein kleines Plugin bringt die Login Informationen von WordPress in die PHP Session (die Idee dazu stammt von dieser Seite):

add_action('init', 'startSession', 1);
add_action('wp_logout', 'endSession');
add_action('wp_login', 'setSessionData', 10, 2);

function startSession() {
  session_name("snowhowSession");
  if(!session_id()) {
    session_start();
    $_SESSION['snowhowinit'] = true;
  }
}

function setSessionData($user_login, $user) {
  $_SESSION['wp_user_id'] = $user->ID;
  $_SESSION['wp_user_email'] = $user->user_email;
}

function endSession() {
  session_destroy();
}

ad 3.) Das sollte ein Leichtes sein:

npm install memcached cookies php-unserialize

Wird der Javascript Code in Form eines Node-Pakets verwaltet (was vermutlich eine gute Idee ist), trägt man die Abhängigkeiten am besten gleich in die package.json Datei ein und kann sie am Produktionssystem mit npm install installieren.

ad 4.) Zum Schluss wird noch eine Javascript-Funktion definiert, die die Login Informationen, sofern vorhanden, zurück gibt:

var Cookies = require('cookies');
var PHPUnserialize = require('php-unserialize');
var Memcached = require('memcached');
var mem = new Memcached('127.0.0.1:11211');

function getWPUser(req, res, cb) {
  var cookies = new Cookies(req, res);
  var sessionCookie = "memc.sess.key."+cookies.get("snowhowSession");
  mem.get(sessionCookie, function(err,data) {
    var user;
    if (err) cb({ error: err });
    if (data) {
      try {
        user = PHPUnserialize.unserializeSession(data);
      } catch(e) {
        user = {};
      }
    }
    cb(user);
  });
}

Hier ist noch anzumerken, dass das php-unserialize Modul nicht korrekt mit PHP Objekten (new stdClass) umgehen kann (zumindest nicht bei meiner Installation).

Fazit

Einige Klimmzüge sind notwendig, um die zwei Serverdienste auf der Backendseite zu verbinden. Das Session-Cookie (hier mit dem Namen snowhowSession) ist der gemeinsame Nenner der beiden Dienste. Das System funktioniert natürlich nur, wenn der Nodejs-Server durch den apache ge-proxy-t wird.