Authentication Middleware with JWT in Node.js

In any website or web application, ensuring that users are who they claim to be and protecting sensitive data from unauthorized access is crucial . One popular method for implementing authentication in Node.js is using JSON Web Tokens (JWT) and custom middleware. In this article, lets explore how to create a middleware that authenticates users using JWT.

What is JWT?

JWT (JSON Web Token) is an open standard for securely transmitting information between parties as a JSON object. This information is digitally signed, ensuring its integrity and authenticity. JWT is commonly used for authentication and authorization purposes.

A JWT is composed of three parts:

  • Header: Contains metadata about the token, such as the type of token (JWT) and the signing algorithm used (e.g., HMAC SHA256).
  • Payload: Contains the claims, which are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private.
    • Registered claims: Predefined claims like iss (issuer), exp (expiration time), sub (subject), and aud (audience).
    • Public claims: Custom claims defined, such as user roles.
    • Private claims: Claims agreed upon between parties using the JWT.
  • Signature: Used to verify the token’s integrity and authenticity. It is created by encoding the header and payload using Base64Url, concatenating them with a dot, and signing the resulting string with a secret key using the specified algorithm.

A JWT typically looks like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Setting Up the Project

Before we dive into the middleware, let’s set up a basic Node.js project with Express.

  • Initialize a new Node.js project:
mkdir auth-middleware
cd auth-middleware
npm init -y
  • Install necessary packages:
npm install express jsonwebtoken body-parser
  • Create a basic Express server:
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');

const app = express();
app.use(bodyParser.json());

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

Creating the Middleware

Next lets create the middleware that will intercept and handle authentication in our application

  • Create the middleware:
// middleware/auth.js
const jwt = require('jsonwebtoken');

const authenticateToken = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (token == null) return res.sendStatus(401); // If no token, unauthorized

    jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
        if (err) return res.sendStatus(403); // If invalid token, forbidden
        req.user = user;
        next(); // Proceed to the next middleware or route handler
    });
};

module.exports = authenticateToken;
  • Protecting routes using the middleware:
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const authenticateToken = require('./middleware/auth');

const app = express();
app.use(bodyParser.json());

const PORT = process.env.PORT || 3000;

// Dummy user for demonstration
const user = { id: 1, username: 'testuser' };

// Route to login and generate a token
app.post('/login', (req, res) => {
    const username = req.body.username;

    if (username !== user.username) {
        return res.status(403).send('User not found');
    }

    const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET);
    res.json({ accessToken });
});

// A protected route
app.get('/protected', authenticateToken, (req, res) => {
    res.send('This is a protected route');
});

app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

Generating and Verifying JWTs

When a user logs in, for us to verify the user, we need to generate the JWT and verify that user is having a valid token

  • Generate a JWT:

In the /login route, we create a token using ‘jwt.sign()

const accessToken = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET);
  • Verify a JWT:

In the middleware, we use ‘jwt.verify()‘ to check the token’s validity

jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
});

Environment Variables

In order to keep our security keys for access token generation hidden from public , lets store our key in a .env file in the server and load that in our application when needed

Create a ‘.env‘ file in the project root:

ACCESS_TOKEN_SECRET=secret_key

Load these variables in the application using ‘dotenv‘:

npm install dotenv
// server.js
require('dotenv').config();

Conclusion

By implementing custom middleware with JWT, we can secure our Node.js applications with a robust authentication mechanism. This approach ensures that only authenticated users can access protected routes, enhancing the overall security of our application.

Understanding How WebSockets Work: A Deep Dive

As a follow up to my previous project example of using Socket.io , I wanted to do a deep dove in to WebSockets and the core behind WebSockets. In today’s web-driven world, real-time communication is becoming increasingly essential. Whether it’s for live chat applications, real-time notifications, or collaborative tools, traditional HTTP protocols fall short due to their request-response nature. WebSockets, however, offer a solution by enabling full-duplex communication channels over a single TCP connection. This article explores how WebSockets work and the principles behind them, providing you with a comprehensive understanding of this powerful technology.

So What Are WebSockets?

WebSockets are a protocol providing full-duplex communication channels over a single, long-lived TCP connection. Unlike HTTP, which follows a request-response model, WebSockets allow for bidirectional communication between the client and server, enabling real-time data exchange with minimal latency.

The WebSocket Handshake

The WebSocket communication starts with a handshake, initiated by the client. The client sends an HTTP request to the server to upgrade the connection from HTTP to WebSocket. Here’s what the initial handshake looks like

Client Request

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

Server Response

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
  • Upgrade Header: Indicates that the client wishes to upgrade the connection to a WebSocket.
  • Connection Header: Indicates that the connection should be upgraded.
  • Sec-WebSocket-Key: A base64-encoded random value generated by the client, used for security purposes.
  • Sec-WebSocket-Accept: A response from the server, which is a hashed value generated from the Sec-WebSocket-Key and ensures the handshake integrity.

If the server supports WebSockets, it responds with a ‘101 Switching Protocols‘ status code, and the connection is upgraded.

Establishing the Connection

Once the handshake is complete, the connection is established, and both the client and server can send messages to each other freely. Unlike HTTP, where the client must initiate every request, WebSockets allow for server-initiated messages, enabling real-time data push.

WebSocket Frames

Data sent over WebSocket is transmitted in frames. There are different types of frames, such as text frames, binary frames, ping frames, and pong frames. Each frame consists of a header and a payload

  • FIN: Indicates if this is the final fragment in a message.
  • Opcode: Defines the type of frame (e.g., text, binary, close, ping, pong).
  • Mask: Indicates if the payload data is masked (client-to-server frames must be masked).
  • Payload Length: Length of the payload data.
  • Payload Data: The actual message data being transmitted.

Example: Real-Time Chat Application

To illustrate how WebSockets work in practice, let’s build a simple real-time chat application using Node.js and the WebSocket library

Server Code

const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (socket) => {
    console.log('A user connected');

    socket.on('message', (message) => {
        console.log('Received:', message);
        // Broadcast the message to all connected clients
        server.clients.forEach((client) => {
            if (client !== socket && client.readyState === WebSocket.OPEN) {
                client.send(message);
            }
        });
    });

    socket.on('close', () => {
        console.log('A user disconnected');
    });
});

console.log('WebSocket server is running on ws://localhost:8080');

Client Code

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Chat</title>
</head>
<body>
    <input type="text" id="message" placeholder="Enter your message">
    <button onclick="sendMessage()">Send</button>
    <div id="chat"></div>

    <script>
        const socket = new WebSocket('ws://localhost:8080');

        socket.onopen = () => {
            console.log('Connected to the server');
        };

        socket.onmessage = (event) => {
            const chat = document.getElementById('chat');
            const message = document.createElement('div');
            message.textContent = event.data;
            chat.appendChild(message);
        };

        socket.onclose = () => {
            console.log('Disconnected from the server');
        };

        function sendMessage() {
            const input = document.getElementById('message');
            const message = input.value;
            socket.send(message);
            input.value = '';
        }
    </script>
</body>
</html>

Why WebSockets

  • Low Latency: Provides real-time updates with minimal delay.
  • Efficient: Reduces overhead by maintaining a single, persistent connection.
  • Bidirectional Communication: Allows for server-initiated messages.
  • Scalability: Supports large-scale, real-time applications like chats, games, and collaborative tools.

Conclusion

WebSockets are a powerful tool for enabling real-time, bidirectional communication in web applications. By maintaining a persistent connection between the client and server, they allow for instant updates and efficient data exchange, which is crucial for modern interactive applications. Understanding the principles behind WebSockets and how they work will enable you to build more dynamic and responsive web applications, enhancing user experience and engagement.

The Power of WebSockets and Socket.io in Real-time Applications

In the age of real-time applications, our ability to provide instant updates to users is a significant advantage. Traditional HTTP protocols are not designed for persistent, real-time communication, which is where WebSockets come into play. WebSockets allow for full-duplex communication channels over a single TCP connection, making them ideal for applications requiring constant data exchange without the overhead of repeatedly opening and closing connections.

Socket.io, a JavaScript library, builds on top of WebSockets and offers additional capabilities like automatic reconnection, multiplexing, and broadcasting, making real-time application development more accessible and robust.

A Collaborative Google Sheets-Like Application

I recently worked on an exciting project where we developed a Google Sheets-like application for a logistic industry company, allowing multiple users to collaborate in real-time. The core feature was that any update to a cell would be instantly visible to all users. Moreover, when a user started editing a cell, it would be highlighted and locked for other users until the editing was complete.

Key Features Implemented:

  • Real-time Cell Updates: As one user updates a cell, all other users see the changes instantly.
  • Cell Locking Mechanism: The cell being edited is highlighted and locked for other users, preventing simultaneous edits.
  • Color Change Notification: The cell being edited changes color, indicating another user is working on it.

To achieve these features, we utilized WebSockets and Socket.io for real-time communication between the server and clients.

Usage of WebSockets and Socket.io

As WebSockets provide a persistent connection between the client and server, enabling real-time data exchange with minimal latency. This was crucial for an applications like ours where delays in updates could lead to conflicts or confusion among users.

Socket.io simplifies WebSocket implementation by handling the complexities of real-time communication, such as reconnection, event handling, and cross-browser compatibility. It also supports additional features like rooms and namespaces, which can be used to segment communication to specific groups of users.

Simplified Backend Code Implementation

Below is a simplified version of our backend code using Node.js and Socket.io

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server);

let sheetsData = {}; // A placeholder for our sheet data storage, once completed this will be sent to our document db.

io.on('connection', (socket) => {
    console.log('A user connected');

    // Handling cell update
    socket.on('cellUpdate', (data) => {
        const { sheetId, cellId, newValue } = data;

        // Update the cell in our data store
        if (!sheetsData[sheetId]) {
            sheetsData[sheetId] = {};
        }
        sheetsData[sheetId][cellId] = newValue;

        // Notify all other users about the cell update
        socket.broadcast.to(sheetId).emit('cellUpdated', { cellId, newValue });
    });

    // Handling cell lock
    socket.on('lockCell', (data) => {
        const { sheetId, cellId } = data;
        
        // Notify all other users to lock the cell
        socket.broadcast.to(sheetId).emit('cellLocked', { cellId });
    });

    // Handling cell unlock
    socket.on('unlockCell', (data) => {
        const { sheetId, cellId } = data;
        
        // Notify all other users to unlock the cell
        socket.broadcast.to(sheetId).emit('cellUnlocked', { cellId });
    });

    // Joining a specific sheet room
    socket.on('joinSheet', (sheetId) => {
        socket.join(sheetId);
    });

    socket.on('disconnect', () => {
        console.log('A user disconnected');
    });
});

server.listen(3000, () => {
    console.log('Server is running on port 3000');
});

Conclusion

WebSockets and Socket.io have transformed the way we develop real-time applications. By enabling low-latency, persistent communication between clients and servers, these technologies allow us to build collaborative, interactive applications efficiently. The project described above demonstrates the power and flexibility of WebSockets and Socket.io in creating a seamless user experience for real-time collaboration.

By leveraging these tools, we were able to provide a robust solution that ensured all users remained synchronized, thus enhancing productivity and collaboration. Whether you’re developing a collaborative tool, a chat application, or any other real-time service, WebSockets and Socket.io are a good option and is worth mastering.

The frontend implementation for this will be given in a separate article with the full ReactJS code.

About Node.js Worker Threads and Multithreading Misconceptions

Introduction

Node.js is renowned for its single-threaded, non-blocking architecture, which is powered by the event loop and ‘libuv‘. However, this has led to a common misconception that Node.js cannot utilize multiple threads for concurrent operations. This article demystifies Node.js’s threading capabilities by introducing Worker Threads and explaining how they can be used for long-running tasks, such as database operations.

The Role of libuv in Node.js

Node.js uses ‘libuv‘, a multi-platform support library with a focus on asynchronous I/O. ‘libuv‘ manages a thread pool, which can handle file system operations, DNS resolution, and other non-JavaScript operations that require asynchronous execution. While JavaScript code runs on a single thread, ‘libuv‘ ensures that blocking operations are offloaded to these worker threads.

Node.js Worker Threads

Introduced in Node.js 10.5.0, Worker Threads provide a way to run JavaScript code in parallel threads. This is particularly useful for CPU-intensive tasks and operations that would otherwise block the event loop.

Example: Using Worker Threads for a Long-Running Database Operation

Let’s explore a practical example of using Worker Threads to handle a long-running database operation without blocking the main thread.

  • Initialize the project and install dependencies:
mkdir node-worker-example
cd node-worker-example
npm init -y
npm install express mysql2 
  • Create the project structure:
  • Worker Thread Implementation:

Create a worker.js file to define the worker:

// worker.js
const { parentPort } = require('worker_threads');
const mysql = require('mysql2/promise');

const dbConfig = {
    host: 'localhost',
    user: 'db_user',
    password: 'db_pw',
    database: 'db'
};

async function longRunningDatabaseOperation() {
    const connection = await mysql.createConnection(dbConfig);
    try {
        const [rows] = await connection.execute('SELECT SLEEP(10); SELECT * FROM test_table');
        return rows;
    } catch (error) {
        throw error;
    } finally {
        await connection.end();
    }
}

parentPort.on('message', async (message) => {
    if (message === 'start') {
        try {
            const result = await longRunningDatabaseOperation();
            parentPort.postMessage({ status: 'success', data: result });
        } catch (error) {
            parentPort.postMessage({ status: 'error', error: error.message });
        }
    }
});
  • Main Application Setup:

Create an index.js file for the main application:

// index.js
const express = require('express');
const { Worker } = require('worker_threads');

const app = express();
const port = 3000;

app.get('/non-blocking', (req, res) => {
    res.json({ status: 'non blocking finished...' });
}
app.get('/start-operation', (req, res) => {
    const worker = new Worker('./worker.js');

    worker.on('message', (message) => {
        if (message.status === 'success') {
            res.json({ status: 'completed', data: message.data });
        } else {
            res.status(500).json({ status: 'error', error: message.error });
        }
    });

    worker.on('error', (error) => {
        res.status(500).json({ status: 'error', error: error.message });
    });

    worker.on('exit', (code) => {
        if (code !== 0) {
            console.error(`Worker stopped with exit code ${code}`);
        }
    });

    worker.postMessage('start');
    res.json({ status: 'processing' });
});

app.get('/', (req, res) => {
    res.send('Hello, world!');
});

app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}`);
});

This example demonstrates how to leverage Worker Threads for a long-running database operation, ensuring that other requests continue to be processed efficiently. Happy coding!

Caveats

While Worker Threads in Node.js offer significant advantages for handling CPU-intensive and long-running tasks, they also come with some drawbacks.

  • Increased Complexity
    • Concurrency Issues: Introducing multiple threads can lead to issues such as race conditions, deadlocks, and other concurrency-related bugs, making the code harder to debug and maintain.
    • Communication Overhead: Communicating between the main thread and worker threads requires serialization and deserialization of messages, which can introduce overhead and complexity.
  • Performance Considerations
    • Thread Creation Overhead: Creating and managing worker threads incurs some overhead, which might negate the performance benefits for small or simple tasks.
    • Resource Consumption: Each worker thread consumes additional memory and CPU resources. For tasks that are not CPU-bound or do not benefit significantly from parallel execution, this can lead to inefficiencies.
  • Debugging and Profiling
    • Complex Debugging: Debugging issues in a multithreaded environment is generally more complex than in a single-threaded one. Tools and techniques for debugging need to account for the parallel execution context.
    • Profiling Challenges: Performance profiling in a multithreaded application can be more challenging, as it requires analyzing multiple execution contexts simultaneously.
  • Compatibility and Ecosystem
    • Module Compatibility: Not all Node.js modules are thread-safe or designed to work in a multithreaded environment. This can limit the choice of modules or require additional effort to ensure compatibility.
    • Library Support: While many libraries are compatible with Worker Threads, some may not be, or they may require additional configuration to work correctly in a multithreaded context.
  • Development Overhead
    • Learning Curve: Developers need to understand the nuances of working with threads, including thread synchronization, message passing, and potential pitfalls of concurrent execution.
    • Increased Code Complexity: Managing worker threads and ensuring proper communication and synchronization can increase the overall complexity of the application codebase.
  • Use Case Suitability
    • Not Always Necessary: For many I/O-bound tasks, Node.js’s non-blocking, asynchronous nature provides sufficient performance without the need for Worker Threads. Using Worker Threads for such tasks may not provide significant benefits and can complicate the architecture unnecessarily.

Conclusion

Node.js’s Worker Threads offer a powerful way to handle CPU-intensive and long-running tasks without blocking the main thread. This capability, coupled with libuv‘s asynchronous I/O operations, debunks the myth that Node.js cannot handle multithreading. By using Worker Threads, developers can build more efficient and responsive applications.

While Worker Threads provide powerful capabilities for handling CPU-intensive and long-running tasks in Node.js, they also introduce complexity and potential performance issues. It is essential to evaluate whether the benefits of using Worker Threads outweigh the drawbacks for your specific use case. For many applications, Node.js’s asynchronous, event-driven model may be sufficient, and Worker Threads should be used judiciously to avoid unnecessary complexity and resource consumption.