DEV Community

Cover image for Node.js server-side authentication: Tokens vs. JWT
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Node.js server-side authentication: Tokens vs. JWT

Written by Destiny Erhabor✏️

In modern web development, authentication is a critical part of securing applications. The two most popular approaches include server-side authentication using tokens and client-side authentication using JSON Web Tokens (JWT).

Both methods have their own unique advantages and disadvantages, and deciding which one to use depends on the requirements of your specific application. In this article, we’ll compare both methods and highlight the benefits and drawbacks of each. Let’s get started!

Jump ahead:

Stateless vs. stateful authentication

In web applications, authentication is the process of verifying the identity of a user who wants to access a restricted resource. You can use several different kinds of authentication, like username and password authentication, social login, or biometric authentication.

Stateless authentication

In stateless authentication, the server doesn’t store any session information about the user. Instead, each request that the user makes to the server contains all the necessary information for authentication, typically in the form of a JWT. The server then validates the token and responds accordingly.

Stateless authentication is popular in modern web applications because it is scalable and can be used with the microservices architecture.

Stateful authentication

In stateful authentication, the server stores session information about the user in a database or an in-memory cache. When the user logs in, the server creates a session ID and stores it on the server-side. This session ID is then used to authenticate subsequent requests made by the user.

Stateful authentication is less scalable than stateless authentication because it requires the server to maintain state, which can become an issue with large user bases.

What are server-side tokens?

Using server-side tokens, which is also called session-based authentication, is an example of stateful authentication that involves storing user authentication data on the server. Upon successful authentication, the server generates a unique token for the user, which is then stored in the server's memory or database. The token is then sent back to the client, either as a cookie or in the response body.

Server-side authentication with tokens involves creating a unique session token for each user when they log in. The token is stored on the server-side and used to authenticate subsequent requests from the same user.

In contrast, client-side authentication using JWT involves issuing a signed token to the client upon successful login, which is then stored on the client-side and sent back to the server with each subsequent request.

Advantages of using server-side authentication

Easy to invalidate

Server-side authentication is easy to invalidate because we have complete control over the session data stored on the server. If we suspect any fraudulent activity, we can quickly terminate a user's session or revoke a token, thereby providing an additional layer of security.

No storage limitations

Unlike client-side authentication, where storage space is limited to cookies or local storage, we can store any amount of session data on the server with server-side authentication. This makes it easier to store large amounts of data, like user preferences or history.

Better for compliance

To meet compliance regulations, some regulatory bodies require that session data be stored on the server-side. This is aimed at enhancing data security, privacy, and control over sensitive information within a controlled and secure environment. In these cases, server-side authentication is a better option.

Some examples include the Payment Card Industry Data Security Standard (PCI DSS) and the Health Insurance Portability and Accountability Act (HIPAA).

No need for re-authentication

With server-side authentication, you don't need to re-authenticate the user on every request, which can improve your application's performance.

Disadvantages of server-side authentication

Scalability issues

As the number of users and sessions increases, storing all the session data on the server can cause scalability issues. It requires more memory and CPU resources, which can cause slower response times and decrease performance overall.

Complexity

Server-side authentication can be complex to implement and maintain, especially if you need to store the session data across multiple servers or instances. It requires more code, configuration, and infrastructure.

Cost

Because it requires more resources and infrastructure, server-side authentication can be more expensive than client-side authentication.

No offline access

Since all the session data is stored on the server, there is no offline access available, which can be a disadvantage in some scenarios.

Server-side authentication with a Node.js application

Let’s review a simple code implementation of the server-side authentication method. First, we need to install express-session and the Express package for Node.js. We can do so by running the code below:

npm install express express-session
Enter fullscreen mode Exit fullscreen mode

Next, we'll create a simple index.js file:

const express = require("express");
const session = require("express-session");
const app = express();
// Dummy user object to demonstrate
const users = [
  { id: 1, username: "john", password: "password" },
  { id: 2, username: "jane", password: "password" },
];
// Array to hold blacklisted user IDs
const blacklistedUsers = [];
// Middleware to parse JSON request bodies
app.use(express.json());
// Middleware to initialize the session
app.use(
  session({
    secret: "mysecretkey",
    resave: false,
    saveUninitialized: true,
  })
);

// Login endpoint
app.post("/login", (req, res) => {
  const { username, password } = req.body;
  // Find the user by username and password
  const user = users.find(
    (u) => u.username === username && u.password === password
  );
  if (user) {
    if (blacklistedUsers.includes(user.id)) {
      return res.status(403).send("User is blacklisted");
    }
    // Save the user ID in the session
    req.session.userId = user.id;
    // Send the user object back to the client
    res.json({ user });
  } else {
    // Send an error response if the user is not found
    res.status(401).json({ message: "Invalid username or password" });
  }
});
// Logout endpoint
app.post("/logout", (req, res) => {
  // Destroy the session to log the user out
  req.session.destroy();
  // Send a success response
  res.json({ message: "Logged out successfully" });
});
// Protected endpoint
app.get("/profile", (req, res) => {
  console.log(req.session.userId);
  // Check if the user is logged in by checking if the user ID is present in the session
  if (req.session.userId) {
    // Find the user by ID
    const user = users.find((u) => u.id === req.session.userId);
    // Send the user object back to the client
    res.json({ user });
  } else {
    // Send an error response if the user is not logged in
    res.status(401).json({ message: "Unauthorized" });
  }
});
  // Blacklist endpoint
  app.post("/blacklist", (req, res) => {
    const { userId } = req.body;
    blacklistedUsers.push(userId);
    res.send(`User ID ${userId} blacklisted`);
});
// Start the server
app.listen(3000, () => {
  console.log("Server started on port 3000");
});
Enter fullscreen mode Exit fullscreen mode

The code above implements server-side authentication using Express and the express-session middleware. It defines a simple login and logout endpoint and a protected profile endpoint that can only be accessed by authenticated users.

When a user logs in with a valid username and password, their user ID is saved in the session. This user ID retrieves the user object from the array of dummy users and sends it back to the client. When the user logs out, their session is destroyed.

The protected profile endpoint checks if the user is logged in by checking if their user ID is present in the session. If the user is not logged in, an error response is sent. Otherwise, the user object is retrieved from the array of dummy users and sent back to the client.

One drawback of this implementation is that the session and blacklisted user is stored server-side in memory, which can become a scalability issue as the number of users increases. Additionally, if the server crashes or restarts, all active sessions will be lost. To avoid these issues, you could use a distributed caching system like Redis to store sessions instead of storing them in memory.

Server-side authentication: Use cases

Let’s review the scenarios where server-side authentication using tokens is generally preferable.

Security is a top priority

Because the server has full control over the creation and management of session tokens, it's easier to implement advanced security measures, like IP blocking, rate limiting, and token revocation.

The application requires real-time updates

If your application requires real-time updates or notifications, server-side authentication can be more efficient because the server can push updates to the client based on the session ID.

Scalability is a concern

In server-side authentication, the session state is stored on the server-side, which can be scaled horizontally across multiple servers using tools like Redis or Memcached.

What is JWT authentication?

JWT authentication is a stateless, token-based authentication method. It involves generating a token containing the user's identity information, which is then sent to the client to be stored. The client then sends this token with every request to the server to authenticate the user. To ensure that the user is authorized to access the requested resource, the token is verified on the server.

To implement client-side authentication using JWT, you'll need to issue a signed token to the client upon successful login and store it on the client-side. You'll also need to include the token with each subsequent request to authenticate the user.

Advantages of JWT authentication

Stateless

Since the token contains all the necessary information to authenticate the user, the server doesn't need to maintain any session data or database queries. JWT is a stateless authentication method that can simplify server maintenance and reduce resource usage.

Scalability

JSON Web Tokens allow for scaling out server resources because the server doesn't need to maintain any state data.

Cross-domain

The token is self-contained and doesn’t require accessing the server for validation, so JWT can be used across different domains.

Disadvantages of JWT authentication

Token size

In JWT authentication, the token size can be large. This can impact performance negatively, especially if the token is sent with every request.

Security risks

If a token is compromised, an attacker can impersonate the user and gain access to protected resources. Additionally, if the token is not properly signed, an attacker can modify the data contained in the token.

Token expiration

If the token doesn’t expire, it can be used indefinitely. However, if the token expires too frequently, it can inconvenience the users, who would have to log in frequently. Balancing the token expiration time is a critical aspect to consider.

JWT authentication with a Node.js application

The code below shows an example implementation of JWT authentication using Node.js and the Express framework:

const express = require("express");
const jwt = require("jsonwebtoken");

const app = express();

// Dummy user object to demonstrate
const users = [
  { id: 1, username: "john", password: "password" },
  { id: 2, username: "jane", password: "password" },
];

// Secret key to sign and verify JWTs
const secretKey = "mysecretkey";

// Login endpoint
app.post("/login", (req, res) => {
  const { username, password } = req.body;

  // Find the user by username and password
  const user = users.find(
    (u) => u.username === username && u.password === password
  );

  if (user) {
    // Create a JWT token with the user ID as the payload
    const token = jwt.sign({ userId: user.id }, secretKey);

    // Send the token back to the client
    res.json({ token });
  } else {
    // Send an error response if the user is not found
    res.status(401).json({ message: "Invalid username or password" });
  }
});

// Protected endpoint
app.get("/profile", (req, res) => {
  // Get the authorization header from the request
  const authHeader = req.headers.authorization;

  if (authHeader) {
    // Extract the JWT token from the authorization header
    const token = authHeader.split(" ")[1];

    try {
      // Verify the JWT token with the secret key
      const decodedToken = jwt.verify(token, secretKey);

      // Get the user ID from the decoded token
      const userId = decodedToken.userId;

      // Find the user by ID
      const user = users.find((u) => u.id === userId);

      // Send the user object back to the client
      res.json({ user });
    } catch (error) {
      // Send an error response if the token is invalid
      res.status(401).json({ message: "Invalid token" });
    }
  } else {
    // Send an error response if the authorization header is not present
    res.status(401).json({ message: "Unauthorized" });
  }
});

// Start the server
app.listen(3000, () => {
  console.log("Server started on port 3000");
});
Enter fullscreen mode Exit fullscreen mode

The code uses the jsonwebtoken library to generate and verify JSON Web Tokens. It provides two endpoints, /login and /profile.

The /login endpoint expects a POST request with the username and password of a user in the request body. It finds the user in the users array and creates a JWT token with the user ID as the payload. It then sends the token back to the client as a JSON object.

The /profile endpoint expects a GET request with an authorization header containing a valid JWT token. It extracts the token from the header and verifies it with the secret key. If the token is valid, it extracts the user ID from the payload and finds the user in the users array. It then sends the user object back to the client as a JSON object.

JWT: Use cases

Authentication and authorization

We can use JWTs to securely transmit authentication and authorization data between the client and server. By including a user's identity and permissions in a JWT, a server can verify that a user is authorized to access certain resources.

Single sign-on (SSO)

JWTs are a great choice to implement single-sign on (SSO), where a user logs into a single application and is then able to access other applications without having to log in again. The JWT can securely transmit the user's identity and authentication state between applications.

Mobile applications

Where traditional session-based authentication methods may not be feasible, you can use JWTs to authenticate and authorize users in mobile applications. The JWT can be stored on the device and used to authenticate the user with the server on subsequent requests.

Microservices

You can use JWTs to authenticate and authorize requests between microservices in a distributed system. Each microservice can use the JWT to verify that requests are coming from a trusted source and that the user is authorized to access the requested resource.

Server-side tokens vs. JWT: Summary

Tokens JWT
Stored server-side Stored client-side
Easily revoked Difficult to revoke
Require server-side storage Stateless
Suitable for real-time Better for mobile or SPA
Updates Multiple domains or microservices
Scalable with Redis or Memcached Requires distributed signing or validation
More secure Faster and easier to implement

Conclusion

In summary, JWT authentication is a stateless approach that uses digitally signed tokens for secure communication. It offers easy integration, cross-domain compatibility, and additional security features. However, it's important to keep an eye on the token size and revocation.

Server-side token authentication involves storing session information on the server. It offers easy session management, quick invalidation, and control over simultaneous logins. However, it requires server-side storage, which may pose scalability challenges.

Choosing between JWT and server-side token authentication depends on your use case, security needs, and scalability requirements. JWT is suitable for stateless scenarios and APIs, while server-side tokens work best for session-based authentication in web applications.

I hope you enjoyed this article, and be sure to leave a comment if you have any questions. Happy coding!


200’s only Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.

Top comments (3)

Collapse
 
lalami profile image
Salah Eddine Lalami

Thanks for Article ,
Here another Complete tutorial about auth using JWT with React : dev.to/idurar/how-to-secure-reactj...

Github Repo Real Project using JWT with node.js and react : github.com/idurar/idurar-erp-crm

Collapse
 
kostyatretyak profile image
Костя Третяк

Thanks, good article, easy to read. The only thing that is not quite clear to me is that the table shows that JWT is better suited for SPA. But why? Personally, I think that working with JWT on the client side is a little more difficult, this also applies to SPA.

Collapse
 
moinakh22885547 profile image
Moin Akhter

Simple and clear thanks buddy for this great article.