Sessions vs Tokens: How to authenticate in Node.js

Last updated on 01 November 2022

#authentication#nodejs

There are a lot of opinions around whether you should use JWT for sessions and someone who reaches out for it for almost every project, I wanted to understand both sides.

In this article, I have tried to document the rationales behind sessions and tokens and some best practices to implement user sessions in your Node.js application.

We will talk about:

  • 🔒 What is authentication
  • 🔑 Session-based authentication
  • 🔐 Token-based authentication
  • ⚖️ Pros and Cons of tokens
  • 💭 Which one you should choose?
  • ✅ Best practices
  • 🏁 Conclusion

A quick refresher on authentication

Authentication is a process of verifying that someone is what they are claiming to be. For example, someone knocks on your door and says to be your neighbor. You peek through the peephole and open the door only upon confirmation.

There are various ways of performing authentication:

  1. Basic authentication (using username and password)
  2. Session-based authentication
  3. Token-based authentication
  4. Single Sign-on (SSO)
  5. OAuth 2.0 for seamless and delegated authentication
  6. Multi-factor authentication
  7. Passwordless (eg. sending login link to your email address)

Based on the scope, convenience, and security guarantees, there are multiple ways to authenticate. The end goal remains the same.

Multi-factor authentication ensures tighter security. While Single Sign-on (SSO) authenticates users across a pool of applications at once. Username and password authentication is simple and straightforward in scope and convenience.

What is session-based authentication?

Whenever a user logs in, the server creates a session and returns the session ID to the client. The client stores that session ID in the cookies.

The session on the server side contains user identification information as well as some meta information like expiration, time of creation, email address, etc.

Upon successful login, the client sends that session Id cookie with every request. The server, on each request, validates the session and allows the client to access protected resources and perform authorized actions.

The session gets stored in the database, cache, or local memory of your application server. As you might have noticed, fetching entries from a database or even a cache is overhead.

Here's an example code snippet of the Express.js server authenticating the user:

Session-based implementation

1const express = require('express');
2const session = require('express-session');
3
4const app = express();
5
6app.use(session({
7 name: 'sessionIdCookie',
8 secret: 'thisshouldbeasecret',
9 resave: false,
10 saveUninitialized: true,
11 cookie: {
12 httpOnly: true,
13 maxAge: 3600000, // 1hr
14 secure: true, // cookie is only accessible over HTTP, requires HTTPS
15 }
16}));
17
18app.get('/', (req, res) => {
19 // something happens...
20
21 req.session.user = {
22 // user details to be stored in the session on server-side
23 }
24 res.status(200).send('Success!');
25})
26
27
28app.listen(4000, () => console.log(`Server listening on port 4000`));

The middleware picks the session ID from the cookie. It then fetches the user record from the database to authenticate and authorize the user.

What is token-based authentication?

It relies on an agreed-upon encoded signature that two services use to communicate online. When a user logs in, they authenticate themselves and upon success, they receive a token.

Your application provides a specially crafted token to each client. It allows the client to access certain protected resources and perform authorized actions. The token is usually valid for a short pre-defined duration.

Since tokens hold the user data, no data store lookup is required. The application server only verifies the token and lets the user through. This is the Unique Selling Point (USP) of tokens.

Let's look at authentication using a JWT token:

Token-based authentication

1const bcrypt = require('bcryptjs');
2const express = require('express');
3const jwt = require('jsonwebtoken');
4
5const secret = 'thisshouldbeasecret';
6
7const app = express();
8
9function verifyAuthToken(req, res, next) {
10 const { authToken } = req.cookies;
11
12 // verify the token
13 jwt.verify(authToken, secret, function (err, decoded) {
14 if (err) {
15 return res.status(401).send({ message: 'Authentication failed! Please try again :(' });
16 }
17
18 // save to request object for later use
19 req.userId = decoded.id;
20 next();
21 });
22}
23
24app.post('/login', async (req, res) => {
25 const { email, password } = req.body;
26
27 // login is performed
28 const user = await fetchUserFromDatabase({ email });
29 if (user && bcrypt.compareSync(password, user.password)) {
30 // token is created and shared with the client
31 const token = jwt.sign({ id: user._id }, secret, {
32 expiresIn: 86400 // expires in 24 hours
33 });
34
35 // return the information including token as JSON
36 res.status(200).send({ message: 'Successfully logged-in!', token });
37 }
38});
39
40app.post('/protected', verifyAuthToken, (req, res) => {
41 res.status(200).send('You are in!');
42})
43
44
45app.listen(4000, () => console.log(`Server listening on port 4000`));

Pros and Cons of tokens

Some of the core benefits of using a token-based authentication mechanism are:

  1. It is stateless or self-contained. It helps you avoid the overhead of fetching session data from the data store.
  2. It is scalable due to its stateless nature.

With that said, there are several downsides to going with the token-based approach:

  1. The secret key on the server side has to be extremely safe & secure from prying eyes.
  2. Cookies have a maximum size limit of 4096 bytes. Thus, it does not allow storing large tokens. Switching to localStorage poses even more security concerns. Not to mention, the increase in latency with larger request headers.
  3. Not suitable for applications that allow longer sessions (eg. one week). Tokens (should) have a shorter lifespan and it can impact user experience.
  4. Revoking the token immediately is a challenge.

Should you not use tokens then?

There's no definite answer. It really depends on your use case.

Before we come to a conclusion, let's make the differences between the two absolutely clear.

With sessions, you store the actual user data in some kind of data store and only pass a session Id to the client. The client sends that session Id in every request and the server authenticates the user by reading from the data store using the session ID.

With JWT, you don't need to reach out to the data store. You can store the user data in the JWT token itself. Also, there's no agreed-upon way in the community to handle revocations “the right way”. Revoking the JWT token is implementation dependent and if handled poorly, can pose security vulnerabilities.

Due to the hard-to-revoke nature of JWTs, it is usually advised to not use them in user-facing applications where users can log out at will. Why? As we learned, revocation is not immediate. Thus, JWTs are said to be very apt for non-user-facing applications. For example, server-to-server communication can enjoy the token-based approach since instant revocation is not necessary.

Sessions, on the other hand, are said to be hard-to-scale due to two main reasons:

  1. You need to take the responsibility of storing them server-side. If the data store hosting the sessions is down, all your applications are effectively down. Choosing allow or deny as a fallback/default is tricky.
  2. With sessions, you need to read from the data store on every request. Definitely not a good deal when you're looking to squeeze out all the performance gains.

To get the best of both worlds, you can choose to have a JWT token for non-sensitive read-only API endpoints and have your sessions for sensitive operations.

This is the summarized version of what I've learned about the two approaches and I hope it helps you in taking an informed decision.

No matter which one you choose to go with, there are a few things to keep in mind. Let's take a look.

Best practices

Whether you decide to go with tokens or good old sessions, you should take conscious and intentional steps toward tightening the security of your web application. For starters, there are some low-hanging fruits you should definitely consider.

  1. Use HTTPS.
  2. Use httpOnly and secure policies for your cookies.
  3. Use sameSite policy for cookies if possible.
  4. Ask the logged-in user for a username and password before taking any “sensitive” action. Validating through OTP is also a good alternative. Just make sure to re-authenticate.
  5. If possible, only allow a single active session per user.
  6. Force logout user if inactive for a certain amount of time.
  7. Have shorter expiration times for your cookies.
  8. If using sessions, make sure your session ID is long, random, and opaque. Do not store any user-related details in the session ID.

Conclusion

In this article, we briefly looked at the session-based and token-based authentication strategies, their trade-offs, and things you must know before deciding the right approach for your application.

I hope it will help you decide how you should proceed with your authentication journey.

I'd love to know which strategy you ended up choosing in the comments. Feel free to reach out on Twitter.

Lastly, I'd highly recommend glancing over the wonderful flowchart in this article: Stop using JWT for sessions.

Liked the article?

If you enjoyed this article, you will like the other ones:

  1. Learn How to Use Group in Mongodb Aggregation Pipeline (With Exercise)
  2. Unit Testing Essentials for Express API: A Step-by-Step Guide
  3. Deploy Node.js to AWS: Build an Automated CI/CD Pipeline
  4. Introduction to TCP Connection Establishment for Software Developers
  5. 5 Things You Must Know About Building a Reliable Express.js Application
 
Liked the article? Share it on: Twitter
No spam. Unsubscribe at any time.