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:
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.
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.
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.
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.
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.
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.
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).
With server-side authentication, you don’t need to re-authenticate the user on every request, which can improve your application’s performance.
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.
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.
Because it requires more resources and infrastructure, server-side authentication can be more expensive than client-side authentication.
Since all the session data is stored on the server, there is no offline access available, which can be a disadvantage in some scenarios.
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
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"); });
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.
Let’s review the scenarios where server-side authentication using tokens is generally preferable.
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.
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.
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.
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.
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.
JSON Web Tokens allow for scaling out server resources because the server doesn’t need to maintain any state data.
The token is self-contained and doesn’t require accessing the server for validation, so JWT can be used across different domains.
In JWT authentication, the token size can be large. This can impact performance negatively, especially if the token is sent with every request.
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.
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.
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"); });
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.
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.
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.
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.
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.
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 |
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!
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 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
One Reply to "Node.js server-side authentication: Tokens vs. JWT"
thanks for info.