Best Practices for Secure Session Management in Node
February 13th, 2020 | By Karan Gandhi | 7 min read
Today, we will explore the best practices for secure session management in Node. Why?
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 or SSL (secure connection), to transfer data securely.
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 when they are not authenticated. Post-authentication sessions are used to identify authenticated users. Sessions fulfill a relevant role in 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 parameters, 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. The data associated with a session identifier lies on the server.
Generally speaking, a session identifier is:
Must be random;
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 expiration attribute, so it persists.
On the other hand, SessionStorage doesn't persist across multiple tabs or windows and is cleared when a tab is closed. Extra client code is required to be written to handle LocalStorage or SessionStorage. Additionally, both are APIs, so theoretically, they are vulnerable to XSS.
Usually, 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 be set after the redirect. In cases where 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 and Path attributes can limit the exposure of a cookie. By default, Domain should not be set, and Path should be restricted.
Expire and 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 in an array. The first secret will be used to sign the cookie. The rest will be used for verification.
app.use(session({
secret: ['veryimportantsecret','notsoimportantsecret','highlyprobablysecret'],
}))
To use a custom session ID generator, we can use the grid parameter.
By default, uid-safe is used to generate session IDs with a byte length of 24. It's recommended to stick to the 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 parameter. 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 of API calls and mobile applications.
Secure connections require HTTPS. Also, if the Node app is behind a proxy (like Nginx), we must set the 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 and 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) parameter 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 parameter forces the session to be saved to the session store. This parameter should be set after checking the store documentation.
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 the following:
req.session.regenerate(function(err) {
// will have a new session here
})
Sessions should expire when the user logs out or times out. To destroy a session, we can use:
req.session.destroy(function(err) {
// cannot access session here
})
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 ArticlesMust read next
10 Tips For Optimizing Node.js Applications
10 useful optimization tips to take the most of your Node.js server application. From asynchronous functions to enabling streaming responses.
July 5, 2016 | By Jscrambler | 4 min read
How To Protect Node.js Apps With Jscrambler
In this post, we're going to walk through the steps to protect your Node.js application with Jscrambler, using our integrations with Grunt and Gulp.
September 16, 2020 | By Jscrambler | 6 min read