7

I want to share the server session between a NodeJs app and a PHP app using Redis. I took most of the code from this gist.

NodeJs code:

app.use(session({
    store: new RedisStore({prefix: 'session:php:'}),
    name: 'PHPSESSID',
    secret: 'node.js'
}));

app.use(function(req, res, next) {
    req.session.nodejs = 'node.js!';
    res.send(JSON.stringify(req.session, null, '  ') );

});

And it outputs:

{
    "cookie": {
        "originalMaxAge": null,
        "expires": null,
        "httpOnly": true,
        "path": "/"
    },
    "passport": {},
    "nodejs": "node.js!"
}

PHP code (I use redis-session-php and Predis) :

require('redis-session-php/redis-session.php');
RedisSession::start();

$_SESSION['php'] = 'php';

if (!isset($_SESSION["cookie"])) {
    $_SESSION["cookie"] = array();
}

var_dump($_SESSION);

And it outputs:

array(2) {
    ["php"] => string(3) "php"
    ["cookie"] => array(0) { }
}

The problem: I would expect both sessions to look the same, but they don't ( the app is run on the same domain ). Setting the values with set() from Predis\Client works (but the values won't be on the session variable). I found this code that I think would work, using set() and get(), but I feel that it would overcomplicate the code.

Do you know what I am doing wrong?

N Alex
  • 1,014
  • 2
  • 18
  • 29
  • something seems fishy with the the php redis module. I would expect to see some more configuration options for connecting to redis. can you show us the config for the PHP Redis (mainly what you are passing into the constructor – geggleto Jan 15 '15 at 15:50
  • I am not passing it anything, by default if no arguments are passed it assumes assumes `127.0.0.1` and `6379` as the default host and port with a connection timeout of 5 seconds. – N Alex Jan 15 '15 at 16:11
  • Wll in your node.js you are passing a secret key... i dont see the option in your code for php. could be that it isnt connecting to the same redis store? – geggleto Jan 15 '15 at 19:53

1 Answers1

12

I'm the author of the gist. The code worked until express-session started forcing signed cookies and started implementing them in a different way.

I have updated the gist to work with the latest version of express-session. A copy of the gist follows for convenience:

app.js:

var express = require('express'),
    app = express(),
    cookieParser = require('cookie-parser'),
    session = require('express-session'),
    RedisStore = require('connect-redis')(session);

app.use(express.static(__dirname + '/public'));
app.use(function(req, res, next) {
  if (~req.url.indexOf('favicon'))
    return res.send(404);
  next();
});
app.use(cookieParser());
app.use(session({
  store: new RedisStore({
    // this is the default prefix used by redis-session-php
    prefix: 'session:php:'
  }),
  // use the default PHP session cookie name
  name: 'PHPSESSID',
  secret: 'node.js rules',
  resave: false,
  saveUninitialized: false
}));
app.use(function(req, res, next) {
  req.session.nodejs = 'Hello from node.js!';
  res.send('<pre>' + JSON.stringify(req.session, null, '    ') + '</pre>');
});

app.listen(8080);

app.php:

<?php
// this must match the express-session `secret` in your Express app
define('EXPRESS_SECRET', 'node.js rules');

// ==== BEGIN express-session COMPATIBILITY ====
// this id mutator function helps ensure we look up
// the session using the right id
define('REDIS_SESSION_ID_MUTATOR', 'express_mutator');
function express_mutator($id) {
  if (substr($id, 0, 2) === "s:")
    $id = substr($id, 2);
  $dot_pos = strpos($id, ".");
  if ($dot_pos !== false) {
    $hmac_in = substr($id, $dot_pos + 1);
    $id = substr($id, 0, $dot_pos);
  }
  return $id;
}
// check for existing express-session cookie ...
$sess_name = session_name();
if (isset($_COOKIE[$sess_name])) {
  // here we have to manipulate the cookie data in order for
  // the lookup in redis to work correctly

  // since express-session forces signed cookies now, we have
  // to deal with that here ...
  if (substr($_COOKIE[$sess_name], 0, 2) === "s:")
    $_COOKIE[$sess_name] = substr($_COOKIE[$sess_name], 2);
  $dot_pos = strpos($_COOKIE[$sess_name], ".");
  if ($dot_pos !== false) {
    $hmac_in = substr($_COOKIE[$sess_name], $dot_pos + 1);
    $_COOKIE[$sess_name] = substr($_COOKIE[$sess_name], 0, $dot_pos);

    // https://github.com/tj/node-cookie-signature/blob/0aa4ec2fffa29753efe7661ef9fe7f8e5f0f4843/index.js#L20-L23
    $hmac_calc = str_replace("=", "", base64_encode(hash_hmac('sha256', $_COOKIE[$sess_name], EXPRESS_SECRET, true)));
    if ($hmac_calc !== $hmac_in) {
      // the cookie data has been tampered with, you can decide
      // how you want to handle this. for this example we will
      // just ignore the cookie and generate a new session ...
      unset($_COOKIE[$sess_name]);
    }
  }
} else {
  // let PHP generate us a new id
  session_regenerate_id();
  $sess_id = session_id();
  $hmac = str_replace("=", "", base64_encode(hash_hmac('sha256', $sess_id, EXPRESS_SECRET, true)));
  // format it according to the express-session signed cookie format
  session_id("s:$sess_id.$hmac");
}
// ==== END express-session COMPATIBILITY ====



require('redis-session-php/redis-session.php');
RedisSession::start();

$_SESSION["php"] = "Hello from PHP";
if (!isset($_SESSION["cookie"]))
  $_SESSION["cookie"] = array();

echo "<pre>";
echo json_encode($_COOKIE, JSON_PRETTY_PRINT);
echo json_encode($_SESSION, JSON_PRETTY_PRINT);
echo "</pre>";

?>
mscdex
  • 104,356
  • 15
  • 192
  • 153
  • This row `$_COOKIE[$sess_name] = substr($_COOKIE[$sess_name], 0, $dot_pos);` won't make the session cookie to equal `""` ? – N Alex Jan 16 '15 at 10:40
  • No. `express-session`'s cookie format is `s:<32-byte id>.<32-byte id hmac>`. So all that line is doing is setting the session cookie value to just the 32-byte id part. The code could probably be simplified a little bit since I included the `express_mutator` function, but the hmac value should still be verified. – mscdex Jan 16 '15 at 13:27
  • But if I switch between PHP and NodeJs, everytime a new session id will be generated, because one of them is setting the cookie as `s:<32-byte id>.<32-byte id hmac>` and the other as `<32-byte id>`, right? How can I fix this, using 2 separate cookies? – N Alex Jan 16 '15 at 14:32
  • No, there should only be one session cookie. I have tested the code personally, having each side create the session and the other side read the session. Both scenarios work just fine. `s:<32-byte id>.<32-byte id hmac>` is always set as the session cookie value in both PHP and node. `<32-byte id>` is what is stored as the key in redis. – mscdex Jan 16 '15 at 15:34
  • Thanks for sharing your code. I can't seem to get this working though. I can see the session data from each individual app, but they don't cross over. Shouldn't I be able to see the PHP session data in node and vice versa? What am I missing? – brawlins Jan 08 '16 at 14:36
  • @mscdex When I use this code the key in redis just says session:php: without any id. If I use just the bottom part I get an Id, but it's not compatible with Node, when I run my script in node it creates a new session. I think something is wrong with your express mutator, please help. – user2278120 Jan 29 '16 at 14:58