Socket.IO Authentication System With JWT
Tags: socketio, authentication, node, backend
Published: 2023-06-08 21:33:57

---
title: Socket.IO Authentication System With JWT
published: true
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xhmmc8amu6axp19erue8.jpg
date: 2023-06-08 20:19:43 UTC
tags: socketIO, authentication, nodejs, backend
canonical_url: https://hayatscodes.hashnode.dev/socketio-authentication-system-with-jwt
---

<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->

- [Overview](#overview)
- [The Authentication System](#the-authentication-system)
  * [Database Model](#database-model)
  * [Express Authentication Handlers](#express-authentication-handlers)
  * [Socket Server Setup](#socket-server-setup)
- [Client-Side Connection](#client-side-connection)
- [Conclusion](#conclusion)

<!-- TOC end -->

There are few resources to explore when it comes to authenticating `socket.io` connections. I recently finished a programming project that involved authenticating socket connections. During the project, I tried my best to search for resources that explained how to create an authentication system for `socket.io` connections, but to no avail. They were either not useful for my project's use case or were unclear. With some effort, I was able to build a secure authentication system for the server so that every connecting client will be authenticated before a connection is established.

In this article, I will be taking you through the step-by-step process of building a secure `socket.io` authentication system with JSON web tokens (JWT). The knowledge can also be transferred to other authentication libraries, like `passport.js` or other programming languages' authentication libraries, as I will be using NodeJs.

Let's dive in!

<a name="overview"></a>
## Overview

I will be explaining every detail of the example codes to let everyone follow along regardless of the tech stacks they are familiar with.

However, we will build the authentication system using a MongoDB `User` collection, an `http` server, an Express `app`, and a `socket.io` server instance. Additionally, I will provide a sample client-side code to demonstrate its use case.

The `User` collection is used to store and retrieve the users' credentials.

The `http` server is used to listen to HTTP and socket connection requests.

The express `app` is used to set up function handlers that handle the registration and log-in authentication endpoints. A response containing a JSON web token (JWT) is expected on successful authentication.

The `socket.io` server instance is responsible for managing socket connection events. A middleware function is utilized to validate the JWT sent by the client, ensuring that only authenticated users make socket connection requests.

The client-side code is used to make HTTP requests to the authentication endpoints and stores the JWT response, which is then used to make web socket connection requests to the `socket.io` server instance.

<a name="the-authentication-system"></a>
## The Authentication System

In this section, we build the authentication system. From the database model to the `socket.io` server instance authentication middleware setup.

<a name="database-model"></a>
### Database Model

Let's start by creating the `User` model schema.

```
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
    username: String,
    email: String,
    password: String,
  });

const User = mongoose.model('User', userSchema);

module.exports = User;

```

We import the `mongoose` module, which provides the functionality to define and interact with MongoDB schemas and models.

A `userSchema` is created using the `mongoose.Schema` constructor, specifying the structure and data types of the user object. The schema includes fields for `username`, `email`, and `password`, each of type `String`.

The `userSchema` is then used to create a `User` model. This model allows for interaction with the 'User' collection in the connected MongoDB database.

Finally, the `User` model is exported to make it available for use in the authentication system.

<a name="express-authentication-handlers"></a>
### Express Authentication Handlers

Here, we'll be creating the handler functions for the registration and log-in endpoints using Express.

First of all, let's import the necessary modules.

```
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require("models/user.model");

```

In the code above, the `express`, `bcrypt`, and `jsonwebtoken` module are all being assigned to their respective variables. Additionally, the `User` database model is imported to enable the storage and verification of user information.

Next, we create the Express `app`.

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

// Register API endpoint
app.post('/auth/register', registerUser);

// Login API endpoint
app.post('/auth/login', loginUser);

```

We begin by invoking the `express()` function provided by the express module, which allows us to register endpoints along with their respective handler functions. Additionally, we attach the `express.json()` middleware to parse JSON request payloads. Two `POST` methods are subsequently added to the Express `app`, enabling it to handle client requests at the `/auth/register` and `/auth/login` endpoints. These requests are handled by the `registerUser` and `loginUser` handler functions, respectively.

Next, we proceed to develop the logic for the `registerUser` handler function.

```
// User Registration Handler Function
async function registerUser(req, res) {
  try {
    const { username, email, password } = req.body;

    // Check if the username or email already exists
    const existingUser = await User.findOne().or([{ username }, { email }]);
    if (existingUser) {
      return res.status(400).json({ message: 'Username or email already exists' });
    }

    // Hash the password
    const hashedPassword = await bcrypt.hash(password, 10);

    // Create a new user
    const newUser = new User({
      username,
      email,
      password: hashedPassword,
    });

    // Save the user to the database
    await newUser.save();
    res.status(201).json({ message: 'Registration successful' });
  } catch (error) {
    console.error('Registration error', error);
    res.status(500).json({ message: 'Registration error' });
  }
}

```

We begin by destructuring the `username`, `email`, and `password` from the `req.body` object, which contains the request body sent by the client. Next, we perform a check for the uniqueness of the `email` and `username` by searching for them in the database. If either of them is found, a `400` bad request response is returned. Otherwise, the function proceeds to hash the password using the `bcrypt.hash()` function provided by the bcrypt module. The hashed password, along with the destructured `username` and `email` properties, are then saved to the database for persistence. In the absence of errors, a `201` created response is returned. However, if any error occurs during the execution of the handler function, a `500` internal server error response is returned.

Next, we proceed to develop the logic for the `loginUser` handler function.

```
// ...

// User Login Handler Function
async function loginUser(req, res) {
  try {
    const { username, password } = req.body;

    // Check if the username exists
    const user = await User.findOne({ username });
    if (!user) {
      return res.status(400).json({ message: 'Invalid username or password' });
    }

    // Compare the password
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(400).json({ message: 'Invalid username or password' });
    }

    // Generate a JWT
    const token = jwt.sign({ userId: user._id }, process.env.SECRET_KEY);

    res.json({ token, message: 'Login successful' });
  } catch (error) {
    console.error('Login error', error);
    res.status(500).json({ message: 'Login error' });
  }
}

// ...

```

We first verify if the destructured `username` exists in the database. If it does not exist, a `400` bad request response is returned. Also, if the destructured `password` does not match the user-stored password in the database, a `400` bad response is returned. However, if the passwords match, we proceed to generate a JWT token using the `jsonwebtoken` module. This token is generated by setting the `userId` key to the value of the `_id` property of the User, which is automatically assigned by MongoDB to each saved user information. Assuming no errors occur, a response containing the generated token is returned.

Next, we export the Express `app`.

```
// ...
module.exports = app;

```

The complete code for the authentication endpoints can be found below.

```
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require("models/user.model");

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

// Register API endpoint
app.post('/auth/register', registerUser);

// Login API endpoint
app.post('/auth/login', loginUser);

// User Registration Handler Function
async function registerUser(req, res) {
  try {
    const { username, email, password } = req.body;

    // Check if the username or email already exists
    const existingUser = await User.findOne().or([{ username }, { email }]);
    if (existingUser) {
      return res.status(400).json({ message: 'Username or email already exists' });
    }

    // Hash the password
    const hashedPassword = await bcrypt.hash(password, 10);

    // Create a new user
    const newUser = new User({
      username,
      email,
      password: hashedPassword,
    });

    // Save the user to the database
    await newUser.save();
    res.json({ message: 'Registration successful' });
  } catch (error) {
    console.error('Registration error', error);
    res.status(500).json({ message: 'Registration error' });
  }
}

// User Login Handler Function
async function loginUser(req, res) {
  try {
    const { username, password } = req.body;

    // Check if the username exists
    const user = await User.findOne({ username });
    if (!user) {
      return res.status(400).json({ message: 'Invalid username or password' });
    }

    // Compare the password
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(400).json({ message: 'Invalid username or password' });
    }

    // Generate a JWT
    const token = jwt.sign({ userId: user._id }, process.env.SECRET_KEY);

    res.json({ token, message: 'Login successful' });
  } catch (error) {
    console.error('Login error', error);
    res.status(500).json({ message: 'Login error' });
  }
}

module.exports = app;

```

Our Express `app` is now ready to be used.

<a name="socket-server-setup"></a>
### Socket Server Setup

Now, let's set up the `http` and `socket.io` server instances for authentication.

```
const http = require('http');
const ioServer = require('socket.io');
const app = require('./app');

```

We begin by importing the `http` and `socket.io` modules, as well as our Express `app`.

Next, we proceed to create instances of the `http` and `socket.io` servers.

```
// Create server from express app
const server = http.createServer(app);

// Create the socket server instance
const io = ioServer(server);

```

We use the `createServer()` function provided by the `http` module to create an HTTP server instance. The Express `app` is then passed to the function to handle requests to the authentication endpoints. The `http` server is then used to create a `socket.io` server instance.

Next, we proceed to add the authentication middleware.

```
io.use(async (socket, next) => {
        try {
            const token = socket.handshake.auth.token;

            // Verify and decode the JWT
            const decoded = jwt.verify(token, process.env.SECRET_KEY);

            // Get the user information from the database
            const user = await User.findById(decoded.userId);
            if (!user) {
                throw new Error('User not found');
            }

            // Attach the user object to the socket
            socket.user = user;
            next();
        } catch (error) {
            console.error('Authentication error', error);
            next(new Error('Authentication error'));
        }
    });

    io.on('connection', (socket) => {
        // Handle Events after authentication
    }

```

In the above code, when a client connects to the server, the middleware function `io.use()`, provided by the `socket.io` library, is invoked. Inside the function, we first retrieve the JWT token from the `socket.handshake.auth.token` property sent by the client during the handshake (connection). It then verifies and decodes the token using a secret key stored in an environment variable.

If the token is valid, the middleware retrieves the user information from the database based on the user ID extracted from the token. If the user is found, the user object is attached to the socket for future reference.

If any errors occur during the authentication process, such as an invalid token or user not found, an error is thrown, and the middleware calls the `next()` function with an error argument.

After successful authentication, the `io.on('connection')` event handler is triggered, allowing for further event handling and communication with the authenticated user.

The authentication system is now complete. But to listen to connections, the code below can be added or customized to your liking.

```
server.listen(PORT, () => {
    console.log(` Server started running at ${PORT}`);
});

```

Where `PORT` is the preferred port to listen to the connections. Also, don't forget to connect to your MongoDB collection before starting the server.

The complete code for the server setup can be found below.

```
const http = require('http');
const ioServer = require('socket.io');
const app = require('./app');
// Create server from express app
const server = http.createServer(app);

// Create the socket server instance
const io = ioServer(server);

io.use(async (socket, next) => {
        try {
            const token = socket.handshake.auth.token;

            // Verify and decode the JWT
            const decoded = jwt.verify(token, process.env.SECRET_KEY);

            // Get the user information from the database
            const user = await User.findById(decoded.userId);
            if (!user) {
                throw new Error('User not found');
            }

            // Attach the user object to the socket
            socket.user = user;
            next();
        } catch (error) {
            console.error('Authentication error', error);
            next(new Error('Authentication error'));
        }
    });

    io.on('connection', (socket) => {
        // Handle Events after authentication
    }

    server.listen(PORT, () => {
        console.log(` Server started running at ${PORT}`);
    });

```

<a name="client-side-connection"></a>
## Client-Side Connection

To demonstrate how the client-side can be set up to use the authentication system, we'll be using Axios to make requests to the authentication endpoints and use the retrieved token to connect to the `socket.io` server instance.

Let's start by registering a user.

```
const axios = require('axios');
const io = require('socket.io-client');

const username = 'HayatsCodes';
const email = 'hayatscodes@gmail.com';
const password = 123456;
let token;

try {
    const response = await axios.post(`http://localhost:${PORT}/auth/register`, {
      username,
      email,
      password,
    });
    console.log(response.data.message); // Registration successful
  } catch (error) {
      console.error(error);
  }

```

Firstly, the `axios` and `socket.io-client` libraries are imported.

Then, we are registering a user by making a `POST` request to the `/auth/register` endpoint of the server. The server URL is constructed using the `PORT` variable, which should contain the port number.

The user's `username`, `email`, and `password` are provided as the request payload. We use the `await` keyword to make the request and store the response in the `response` variable.

If the registration is successful, the messsage from the response is logged to the console.

If an error occurs during the registration process, the `catch` block is executed, and the error is logged to the console using `console.error`.

Now, let's sign in the registered user.

```
try {
      const response = await axios.post(`http://localhost:${PORT}/auth/login`, {
        username,
        password,
      });
      token = response.data.token;
      console.log(response.data.message); // login successful
    } catch (error) {
        console.error(error.response.data); 
    }
};

```

We're signing in the registered user by making a POST request to the `/auth/login` endpoint of the server while including the `username` and `password` in the request payload.

If the login is successful, the token is extracted from the `response.data.token` property, and the `message` is logged to the console.

The token is then assigned to the `token` variable for use in the `socket.io` client connection.

If an error occurs during the registration process, the `catch` block is executed, and the error is logged to the console using `console.error`.

Now let's connect to the `socket.io` server.

```
const client = io(`http://localhost:${PORT}`, {
      auth: {
        token
      }
});
// handle events
client.on('connect', () => { console.log('connected!') });
// Additional event handling can follow

```

In the above code snippet, we're establishing a client-side connection to the `socket.io` server using the `io` function provided by the `socket.io-client` library.

The `io` function is called with the socket server URL and an object as an argument, which includes the `auth` property. This property specifies the authentication token that will be sent to the server during the handshake process. The value of the earlier saved `token` variable from the login request is provided as the token value.

Once the connection is established, the `client.on('connect')` event handler is set up to listen for the 'connect' event. When the client successfully connects to the server, the callback function is executed, logging 'connected!' to the console.

Additional event handling and communication with the server can be added within the appropriate event handlers.

<a name="conclusion"></a>
## Conclusion

Building a secure authentication system for `socket.io` connections can be a challenging task due to the limited resources available. However, by leveraging JSON web tokens (JWT), it is possible to create a robust authentication system. In this article, we have covered the step-by-step process of building such a system, including setting up the database model, creating authentication endpoints with Express, implementing authentication middleware with `socket.io`, and demonstrating client-side connections using `axios` and the `socket.io-client` library. By following the provided code examples and explanations, developers can build their own secure authentication system for `socket.io` connections, allowing only authenticated users to establish connections and interact with the server.