Cookie and Session (II): How session works in express-session | by Alysa Chan | Medium

Cookie and Session (II): How session works in express-session

My previous article, Cookie and Session (I) — The basic concept, introduces the basic concept of session, session ID, cookie and how they work for authentication. For beginners, I believe the most difficult part of session and cookie is not about how to issue a session ID, but the whole picture of how session works, such as how should we store and set session ID.

To have a whole picture of how session works. This article discusses about two questions, by taking express-session as an example:

  • How session is created?
  • How session is stored?

Why do we need session?

Before we start, we have to ask why do we need session. The reason is that HTTP protocol is stateless, which means it cannot keep track of user. To solve this problem, the server can create a session for tracking the user, including the data in the requests and responses between the client and server.

Let’s start with the first question: how to create a session?

Question 1: How session is created?

Session is created in server when the client send a request to it for the first time. There are different ways to create a session, depends on the language you use. This article focuses on express-session .

For express-session , we create a session in a middleware.

var app = express()
var session = require('express-session')

app.use(session({
secret: 'keyboardcat',
resave: false,
saveUninitialized: true,
cookie: { secure: true }
}))

// Access the session as req.session
app.get('/', function(req, res, next) {
console.log(req.session)
res.send('Hello World!')
})

To make sure we can the incoming request has a session, we should put the session middleware before all of the routes. The middleware will:

  • Check if this request has cookie with valid session ID
  • If yes, search for the matching session ID in session store and retrieve the data
  • If no, create a new session and add it to the request object. Also create a session ID cookie based on the cookie options you set in the middleware

More details about this process is mentioned in this stackoverflow discussion.

Session options

There are options for manipulating the session. Details of each common options such as httpOnly , secret, secure can be found on the official documentation.

Take resave option as an example,express-session will store the session automatically if the session has been altered, because we set resave: false in the session middleware.

Session.save(callback)

…This method is automatically called at the end of the HTTP response if the session data has been altered (though this behavior can be altered with various options in the middleware constructor). Because of this, typically this method does not need to be called.

If the session is not altered, the session ID will not be set in the cookie.

Session ID

Besides the session object, a session ID is necessary for searching a particular session that stored in the session store. The process works like:

client → GET request (with session ID in cookie) → server → search for this user’s session with his session ID

When a client visits the website for the first time, he does not have a session ID in his cookie. Thus, after he sends his first GET request to the server, the server has to set a session ID in his cookie.

So how do we set session ID in cookie?

Session ID will be set in cookie by a session middleware. Be careful of theresave option. As mentioned above, with resave: false option, the session ID will not be set in cookie if the session has not been altered.

Also, be aware that if you use local server, you should not follow the official example that sets cookie: { secure: true } . This options will not allow setting cookie in HTTP website. For example, you can just delete cookie: { secure: true } :

app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: false,
}))

After accessing to localhost 3000 (I use port 3000 for example), the session ID is set. We can check it in the browser by:

Right click > Inspect > Application > Storage (on the left side) > Cookies

By default the key is connect.sid . We can modify the name option for customising the name of the key:

app.use(session({
secret: 'keyboard cat',
name: 'test', // Customise the name to 'test'
resave: false,
saveUninitialized: false,
}))

Now the session ID is saved in the user’s cookie.

But how session ID is generated?

The session ID is generated by uid-safe library and stored in session store by default. You can assign another function for generating session ID in genid option.

However, the session ID stored in server looks different from the id we store in our user’s cookie.

Let’s take a look at the session ID in server’s memory store:

app.get('/', (req, res) => {
req.session.foo = 'foo'
console.log(req)
res.send('Hello World!')
})

The session ID is oyd0L2r... :

Compared to the value in my browser’s cookie:

s%3A is added at the beginning and .V2... is added at the end. The reason is that express-session uses cookie-signature library and your secret option to generate the value and sign the cookie. The mechanism is shown in the source code.

Feel free to skip this part if you are not interested in reading source code.

After reading this article (in Chinese) about how session ID is generated, I have a brief idea on this question. There are mainly two steps for creating a session ID:

  • Generate a UID by using uid-safe
  • Create a signature with the UID and your secret by using cookie-signature

express-session / index.js

function session(options) {
// get the session id generate function
var generateId = opts.genid || generateSessionId,
}

Generate a UID

function generateSessionId(sess) {
return uid(24);
}

Create a signature with that UID and your secret

/**
* Set cookie on response.
*
* @private
*/

function setcookie(res, name, val, secret, options) {
var signed = 's:' + signature.sign(val, secret);
var data = cookie.serialize(name, signed, options);

debug('set-cookie %s', data);

var prev = res.getHeader('Set-Cookie') || []
var header = Array.isArray(prev) ? prev.concat(data) : [prev, data];

res.setHeader('Set-Cookie', header)
}

node-cookie-signture / index.js

var crypto = require('crypto');

/**
* Sign the given `val` with `secret`.
*
* @param {String} val
* @param {String} secret
* @return {String}
* @api private
*/

exports.sign = function(val, secret){
if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string.");
if ('string' != typeof secret) throw new TypeError("Secret string must be provided.");
return val + '.' + crypto
.createHmac('sha256', secret)
.update(val)
.digest('base64')
.replace(/\\=+$/, '');
};

The result will be s: +UID + . + crypto.createHmac(...) . Colon is a special character, which equals to s%3A (percent % is an escape character). That’s why the value stored in cookie starts withs%3A .

Additionally, in the source code above, we need a secret for creating a signature. That’s the reason why we should not reveal our secret in production. We should hide it with using environment variable:

app.use(session({
secret: process.env.SECRET,
...
}))

Since the signature depends on the secret, be careful if you change your secret , all the cookies that were set previously will be invalid.

More details about secret can be found in the express-session official document.

Question 2: How session is stored?

We now know how session and session ID are created. But how do we store them, such that we can validate the session ID sent by the client? The common options are in-memory store or database.

By default, express-session creates a new memory store instance for storing session data in server.

A simple example:

const express = require('express')
const app = express()
const port = 3000

var session = require('express-session')

app.use(session
secret: 'keyboard cat',
resave: false,
saveUninitialized: false,
}))

// No store option is provided,
// so express-session will create a MemoryStore instance for storing session here

app.get('/', (req, res) => {
req.session.foo = 'some text here'
res.send('Hello World!')
console.log(req);
})

Console log message in server:

In the request object, there is a property called sessionStore , with a MemoryStore instance as a value. That’s where we store our session by default.

Session ID I set in cookie:

So what is session store?

Session store is a place where we store session data on server.

By default, express-session creates a new MemoryStore instance for storing session data in server.

Source code from express-session :

function session(options) {
// get the session store
var store = opts.store || new MemoryStore()
}

However, in production, it is not recommended to use the default memory store, as mentioned in the official documentation. We should use other modules, such as connect-redis , a Redis-based session store.

To store session, it is common to use a in-memory store or database. Brief comparison here:

Pros of using in-memory store

  • Fast read and write speed since in-memory store stores and retrieves data from RAM
  • Database may have disk I/O issue if the server keeps writing in and reading session data from the database, which results in database performance issue

Cons of using in-memory store

  • Risk of losing data. When we relaunch the app or the app is crashed, we may lose the data in in-memory store

This TED video clearly explains how memory works in computer.

connect-redis implemtation

To start with connect-redis , make sure you have installed Redis already.

After that, install all required packages in your project, as mentioned in the connect-redis documentation:

npm install redis@v3 connect-redis express-session

app.js

const express = require('express')
const app = express()
const port = 3000

const redis = require('redis')
var session = require('express-session')

let RedisStore = require('connect-redis')(session)
let redisClient = redis.createClient()

redisClient.on('error', (err) => console.log(`Fail to connect with redis. ${err}`));
redisClient.on('connect', () => console.log('Successful to connect with redis'));

app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'keyboard cat',
resave: false,
saveUninitialized: false,
}))

app.get('/', (req, res) => {
req.session.foo = 'some text here' // To imitate alter the session
res.send('Hello World!')
})

app.listen(port, () => {
console.log(`Example app listening at <http://localhost>:${port}`)
})

Few important notes here:

  • Because resave is false , new session without any modification will not be stored. That’s why req.session.foo = 'some text here' is required
  • The default expire date of data in session store is one day (86400 seconds). It can be customised by ttl option

Let’s run the program by typing node app.js in terminal.

  1. Access localhost:3000

2. Check if the session ID is set in cookie or not.

Inspect > Application > Storage (on the left side) > Cookies

3. Check if the session data is stored in Redis by accessing redis-cli in terminal

redis-cli

Check all available keys and get the session data

KEYS *

GET "sess:vUj5V4Du8OmK2jc067-SOtC9j-0UQPjU"

Result:

Summary

To understand the basic of how session works, we go through two questions in this article, by taking express-session as an example:

  • How session is create?

Session is created in a session middleware.

  • How session is stored?

Session is stored in MemoryStore, an instance created by the middleware by default. This implementation is not suggested for production. We can use external in-memory store such as Redis for storing session.

In the next article, we will focus on login and logout example using express-session and connect-redis.

Reference

Creating and managing sessions in ExpressJS

Session Management in Nodejs Using Redis as Session Store

Session Management in Node.js using ExpressJS and Express Session

深入 Session 與 Cookie:Express、PHP 與 Rails 的實作

[Node.js] cookie-session驗證原理以及express-session套件使用