Best Practices for Secure Session Management in Node

February 13th, 2020 | By Karan Gandhi | 7 min read

In a web application, data is transferred from a browser to a server over HTTP. In modern applications, we use the HTTPS protocol, which is HTTP over TLS/SSL (secure connection), to transfer data securely.

Looking at common use cases, we often encounter situations where we need to retain user state and information. However, HTTP is a stateless protocol. Sessions are used to store user information between HTTP requests.

We can use sessions to store users' settings like when not authenticated. Post authentication sessions are used to identify authenticated users. Sessions fulfill an important role between user authentication and authorization.

Exploring Sessions

Traditionally, sessions are identifiers sent from the server and stored on the client-side. On the next request, the client sends the session token to the server. Using the identifier, the server can associate a request with a user.

Session identifiers can be stored in cookies, localStorage, and sessionStorage. Session identifiers can be sent back to the server via cookies, URL params, hidden form fields or a custom header. Additionally, a server can accept session identifiers by multiple means. This is usually the case when a back-end is used for websites and mobile applications.

Session Identifiers

A session identifier is a token stored on the client-side. Data associated with a session identifier lies on the server.

Generally speaking, a session identifier:

  1. Must be random;

  2. Should be stored in a cookie.

The recommended session ID must have a length of 128 bits or 16 bytes. A good pseudorandom number generator (PNRG) is recommended to generate entropy, usually 50% of ID length.

Cookies are ideal because they are sent with every request and can be secured easily. LocalStorage doesn't have an expiry attribute so it persists. On the other hand, SessionStorage doesn't persist across multiple tabs/windows and is cleared when a tab is closed. Extra client code is required to be written to handle LocalStorage / SessionStorage. Additionally, both are an API so, theoretically, they are vulnerable to XSS.

Usually, the communication between client and server should be over HTTPS. Session identifiers should not be shared among the protocols. Sessions should be refreshed if the request is redirected. Also, if the redirect is to HTTPS, the cookie should set after the redirect. In case multiple cookies are set, the back-end should verify all cookies.

Securing Cookie Attributes

Cookies can be secured using the following attributes.

  • The Secure attribute instructs the browser to set cookies over HTTPS only. This attribute prevents MITM attacks since the transfer is over TLS.

  • The HttpOnly attribute blocks the ability to use the document.cookie object. This prevents XSS attacks from stealing the session identifier.

  • The SameSite attribute blocks the ability to send a cookie in a cross-origin request. This provides limited protection against CSRF attacks.

  • Setting Domain & Path attributes can limit the exposure of a cookie. By default, Domain should not be set and Path should be restricted.

  • Expire & Max-Age allow us to set the persistence of a cookie.

Typically, a session library should be able to generate a unique session, refresh an existing session and revoke sessions. We will be exploring the express-session library ahead.

Enforcing Best Practices Using express-session

In Node.js apps using Express, express-session is the de facto library for managing sessions. This library offers:

  • Cookie-based Session Management.

  • Multiple modules for managing session stores.

  • An API to generate, regenerate, destroy and update sessions.

  • Settings to secure cookies (Secure / HttpOnly / Expire /SameSite / Max Age / Expires /Domain / Path)

We can generate a session using the following command:

app.use(session({
secret: 'veryimportantsecret',
}))


The secret is used to sign the cookie using the cookie-signature library. Cookies are signed using Hmac-sha256 and converted to a base64 string. We can have multiple secrets as an array. The first secret will be used to sign the cookie. The rest will be used in verification.

app.use(session({
secret: ['veryimportantsecret','notsoimportantsecret','highlyprobablysecret'],
}))


To use a custom session ID generator, we can use the genid param. By default, uid-safe is used to generate session IDs with a byte length of 24. It's recommended to stick to default implementation unless there is a specific requirement to harden uuid.

app.use(session({
secret: 'veryimportantsecret',
genid: function(req) {
return genuuid() // use UUIDs for session IDs
}
}))


The default name of the cookie is connect.sid. We can change the name using the name param. It's advisable to change the name to avoid fingerprinting.

app.use(session({
secret: ['veryimportantsecret','notsoimportantsecret','highlyprobablysecret'],
name: "secretname"
}))


By default, the cookies are set to

{ path: '/', httpOnly: true, secure: false, maxAge: null }


To harden our session cookies, we can assign the following options:

app.use(session({
secret: ['veryimportantsecret','notsoimportantsecret','highlyprobablysecret'],
name: "secretname",
cookie: {
httpOnly: true,
secure: true,
sameSite: true,
maxAge: 600000 // Time is in miliseconds
}
}))


The caveats here are:

  • sameSite: true blocks CORS requests on cookies. This will affect the workflow on API calls and mobile applications.

  • secure requires HTTPS connections. Also, if the Node app is behind a proxy (like Nginx), we will have to set proxy to true, as shown below.

app.set('trust proxy', 1)


By default, the sessions are stored in MemoryStore. This is not recommended for production use. Instead, it's advisable to use alternative session stores for production. We have multiple options to store the data, like:

  • Databases like MySQL, MongoDB.

  • Memory stores like Redis.

  • ORM libraries like sequelize.

We will be using Redis as an example here.

npm install redis connect-redis 
const redis = require('redis');
const session = require('express-session');
let RedisStore = require('connect-redis')(session);
let redisClient = redis.createClient();

app.use(
session({
secret: ['veryimportantsecret','notsoimportantsecret','highlyprobablysecret'],
name: "secretname",
cookie: {
httpOnly: true,
secure: true,
sameSite: true,
maxAge: 600000 // Time is in miliseconds
},
store: new RedisStore({ client: redisClient ,ttl: 86400}),
resave: false
})
)


The ttl (time to live) param is used to create an expiration date. If the Expire attribute is set on the cookie, it will override the ttl. By default, ttl is one day.

We have also set resave to false. This param forces the session to be saved to the session store. This param should be set after checking the store docs.

The session object is associated with all routes and can be accessed on all requests.

router.get('/', function(req, res, next) {
req.session.value = "somevalue";
res.render('index', { title: 'Express' });
});


Sessions should be regenerated after logins and privilege escalations. This prevents session fixation attacks. To regenerate a session, we will use:

req.session.regenerate(function(err) {
// will have a new session here
})


Sessions should be expired when the user logs out or times out. To destroy a session, we can use:

req.session.destroy(function(err) {
// cannot access session here
})


Side Note: While this article focuses on back-end security, you should protect your front-end as well. See our tutorials on protecting React, Angular, Vue, React Native, Ionic, and NativeScript.

Jscrambler

The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.

View All Articles

Subscribe to Our Newsletter