Cookie and Session (II): How session works in express-session | by Alysa Chan | Medium
Nội Dung Chính
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 = 3000var 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 hereapp.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
isfalse
, new session without any modification will not be stored. That’s whyreq.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.
- 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套件使用