Implement User/Password Authentication in Socket.IO and ReactJS

Adel Benyahia
11 min readNov 27, 2022
  1. What is this article about?
  2. Why using Socket.IO?
  3. Authentication in Socket.IO
  4. Lets code
  5. Complete project source code
  • Working on MongoDB database
  • The back-end ( NodeJS and Socket.IO )
  • The front-end (ReactJS and Socket.IO-client)

1. What is this article about?

In this tutorial we will use ReactJS to build authentication system to secure Socket.IO access.

In the front-end:

  • ReactJS
  • Socket.IO-Client

In the back-end:

  • NodeJS
  • MongoDB
  • Socket.IO

React makes it painless to create interactive UIs. Design simple views for each state in your application, and React will efficiently update and render just …

All data ( usernames, passwords, notifications, messages…) will be stored in MongoDB

2. Why using Socket.IO

Socket.IO is a library that enables low-latency, bidirectional and event-based communication between a client and a server.

Performant

In most cases, the connection will be established with WebSocket, providing a low-overhead communication channel between the server and the client.

Reliable

Rest assured! In case the WebSocket connection is not possible, it will fall back to HTTP long-polling. And if the connection is lost, the client will automatically try to reconnect.

Scalable

Scale to multiple servers and send events to all connected clients with ease.

3. Authentication in Socket.IO

A lot of articles exist in the internet, showing how to create a chat application using Socket.IO with no authentication at all !!!, or using query strings.

Putting credentials (username and password ) in a query string is a bad security practice, urls are not treated as sensitive information , and all servers logs urls with visible credentials.

If we call Socket.IO in our application with query strings

const connectSocket = io(process.env.REACT_APP_SERVER, {
query: {
username: "user1",
password: "password1"
}
});

And when we open Dev tools, as you can see, query params are included as plain text in the request urls

In our tutorial, we will use a more secure approach, we will send credentials as socket.emit() message

  1. The a connection is established between and the server and the client
const connectSocket = io(process.env.REACT_APP_SERVER);

2. The client then send a socket.emit(“authenticate”, { username, password });

socket.once("connect", () => {
socket.emit("authenticate", {
username,
password,
});
});

3. And the back-end server validate the authentication username and password (socket.auth = true), if not (socket.auth=false)

4. If authentication failed (socket.auth = true), the socket will be disconnected after 1 sec (1000)

 setTimeout(() => {  
if (!socket.auth) {
return socket.disconnect("unauthorized")}
,1000}

4. Lets code

. Working on MongoDB database

Our MongoDb database will contain 3 collections:

  1. users
 username: { type: String, required: true },
password: { type: String, required: true },
profileImage: { type: String,},

Then we add two users:

Note that the second user don’t have a profile image (we will use a default image in our application for all users that do not have a profile image)

2. notifications

userID: { type: mongoose.Schema.Types.ObjectId, require: true },
title: { type: String, required: true },
text: { type: String, required: true },
read: {
type: Boolean,
},

We will create different notifications for our two users (each notification is linked to a unique userID)

3. messages

userID: { type: mongoose.Schema.Types.ObjectId, require: true },
senderID: { type: mongoose.Schema.Types.ObjectId, require: true },
title: { type: String, required: true },
message: { type: String, required: true },
read: {
type: Boolean,
},

Each message is linked to two users (senderID and userID )

we will create some random messages

. The back-end ( NodeJS and Socket.IO )

In the root folder

Create “.env”

PORT=3500
DATABASE_URI=__mongodb+srv://......__

Create a directory “./backend”

mkdir backend
cd backend

Then run

npm init -y

Install these packages

npm i dotenv cors mongoose socket.io express http

Add this line to your scripts {}

  "scripts": {
"dev": "node --watch server.js"
},

You must have node 18 or higher to use the “ — watch” flag

Now the configurations files

Create a directory “./config”

. allowedOrigins.js

const allowedOrigins = [
"http://localhost:3000",
"http://127.0.0.1:3000",
];
module.exports = allowedOrigins;

. corsConfigs.js

const allowedOrigins = require("./allowedOrigins");

const corsConfigs = {
origin: (origin, callback) => {
if (allowedOrigins.indexOf(origin) !== -1 || !origin) {
// remove ||!origin to block postman request
callback(null, true);
} else {
callback(new Error("Origin not allowed by Cors"));
}
},
credentials: true,
optionsSuccessStatus: 200,
};
module.exports = corsConfigs;

We will use at in the cors configuration, add all allowed front-end ips here

Then

. dbConn.js

const mongoose = require("mongoose");

const connectDB = async () => {
try {
mongoose.connect(process.env.DATABASE_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
} catch (err) {
console.log(err);
}
};
module.exports = connectDB;

The configuration for your mongoose connection

Now create a “./models” folder

Add these files

. message.js

const mongoose = require('mongoose')

const messageSchema = new mongoose.Schema({
userID: { type: mongoose.Schema.Types.ObjectId, require: true },
senderID: { type: mongoose.Schema.Types.ObjectId, require: true },
title: { type: String, required: true },
message: { type: String, required: true },
read: {
type: Boolean,
},
});
module.exports = mongoose.model("Message", messageSchema);

. notification.js

const mongoose = require('mongoose')

const notificationSchema = new mongoose.Schema({
userID: { type: mongoose.Schema.Types.ObjectId, require: true },
title: { type: String, required: true },
text: { type: String, required: true },
read: {
type: Boolean,
},
});
module.exports = mongoose.model("Notification", notificationSchema);

. user.js

const mongoose = require('mongoose')

const userSchema = new mongoose.Schema({
username: { type: String, required: true },
password: { type: String, required: true },
profileImage: {
type: String,
},
})
module.exports = mongoose.model('User', userSchema)

These are the schemas for our MongoDB collections

In the root folder create “socketio.js”

const User = require("./models/user");
const Notification = require("./models/notification");
const Message = require("./models/message");

module.exports = function (io) {

io.on("connection", (socket) => {
socket.auth = false;
socket.on("authenticate", async (auth) => {
const { username, password } = auth;
// Find user
const user = await User.findOne({ username }).exec();
if (user === null) {
socket.emit("error", { message: "No user found" });
} else if (user.password !== password) {
socket.emit("error", { message: "Wrong password" });
} else {
socket.auth = true;
socket.user = user;
}
});
setTimeout(() => {
// If the authentication failed, disconnect socket
if (!socket.auth) {
console.log("Unauthorized: Disconnecting socket ", socket.id);
return socket.disconnect("unauthorized");
}
return socket.emit("authorized");
}, 1000);
console.log("🔥 Socket connected: ", socket.id);
socket.on("getNotifications", async (userID) => {
const notification = await Notification.find({ userID }).lean().exec();
if (notification === null) {
socket.emit("notifications", []);
} else {
socket.emit("notifications", notification);
}
});
socket.on("getMessages", async (userID) => {
const message = await Message.find({ userID }).lean().exec();
if (message === null) {
socket.emit("messages", []);
} else {
socket.emit("messages", message);
}
});
socket.on("getUser", () => {
socket.emit("user", {
id: socket.user._id,
username: socket.user.username,
profileImage: socket.user.profileImage,
});
});
socket.on("disconnect", () => {
socket.disconnect("disconnect");
});
});
};

In this part of the code

io.on("connection", (socket) => {
socket.auth = false;
socket.on("authenticate", async (auth) => {
const { username, password } = auth;
// Find user
const user = await User.findOne({ username }).exec();
if (user === null) {
socket.emit("error", { message: "No user found" });
} else if (user.password !== password) {
socket.emit("error", { message: "Wrong password" });
} else {
socket.auth = true;
socket.user = user;
}
});
setTimeout(() => {
// If the authentication failed, disconnect socket
if (!socket.auth) {
console.log("Unauthorized: Disconnecting socket ", socket.id);
return socket.disconnect("unauthorized");
}
return socket.emit("authorized");
}, 1000);
}

When the socket connect, we add “socket.auth = false” and listen to “authenticate” on event. then

We check our MongoDB, if the user exist and the password is correct then

socket.auth = true;
socket.user = user;

Then come this block of code

setTimeout(() => {
// If the authentication failed, disconnect socket
if (!socket.auth) {
console.log("Unauthorized: Disconnecting socket ", socket.id);
return socket.disconnect("unauthorized");
}
return socket.emit("authorized");
}, 1000);

The server will wait for 1 sec for the “authenticate” event, then

if “socket.auth = false” the socket will be disconnected

return socket.disconnect("unauthorized");

else

return socket.emit("authorized");

a “authorized” event will be returned to the front-end

and the code code continue with these listeners getNotifications, getMessages, getUser, disconnect.

Finally “server.js”

require("dotenv").config();
const express = require("express");
const cors = require("cors");
const corsConfigs = require("./config/corsConfigs");
const allowedOrigins = require("./config/allowedOrigins");
const mongoose = require("mongoose");
const app = express();
const port = process.env.PORT || 3500;
app.use(cors(corsConfigs));
app.use(express.json());
const connectDB = require("./config/dbConn");

// Socketio must be declared before API routes
const server = require("http").createServer(app);
const io = require("socket.io")(server, {
transports: ["websocket","polling"],
maxHttpBufferSize: 1e8, // 100 MB we can upload to server (By Default = 1MB)
pingTimeout: 60000, // increate the ping timeout
cors: { origin: allowedOrigins },
});
require("./socketio.js")(io);

// socketio auth
// io.use(auth);

connectDB();
mongoose.connection.once("open", () => {
server.listen(port, () => {
console.log("🔗 Successfully Connected to MongoDB");
console.log(`✅ Application running on port: ${port}`);
});
});
mongoose.connection.on("error", (err) => {
console.log(err);
});

.cors are called with “configuration”

app.use(cors(corsConfigs));

Connection to MongoDB , with the “dbConn” as configuration

const connectDB = require("./config/dbConn");

Now we connect our io

const io = require("socket.io")(server, {
transports: ["websocket","polling"],
maxHttpBufferSize: 1e8, // 100 MB we can upload to server (By Default = 1MB)
pingTimeout: 60000, // increate the ping timeout
cors: { origin: allowedOrigins },
});

Then we passe our io to the “socketio.js”

require("./socketio.js")(io);

Then

connectDB();
mongoose.connection.once("open", () => {
server.listen(port, () => {
console.log("🔗 Successfully Connected to MongoDB");
console.log(`✅ Application running on port: ${port}`);
});
});

We connect to the mongodb and listen our server on “port ”

. The front-end (ReactJS and Socket.IO-client)

We will go faster here,

I your react app, create “ .env”

REACT_APP_SERVER = http://localhost:3500

We need only one extra package here

npm i socket.io-client

. Create “.env” file in the front-end root folder

REACT_APP_SERVER = http://localhost:3500

. CSS styling in App.css

.App {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.user__container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.user__container > img {
width: 100px;
height: 100px;
}
.user__container button {
margin: 5px;
height: 40px;
border: 1px solid gray;
border-radius: 5px;
width: 120px;
cursor: pointer;
}
.login__container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}

.login__container button,
input {
margin: 5px;
height: 40px;
border: 1px solid gray;
border-radius: 5px;
}
.login__container button,
input {
padding-left: 5px;
}
.login__container input {
width: 250px;
}
.login__container button {
width: 120px;
cursor: pointer;
}

. Create a folder and add a picture that will represent the default user profile picture (if a user don’t have a profileImage link)

./assets/default_profile_image.png

. Now our App.js

import "./App.css";
import { useState, useEffect } from "react";
import io from "socket.io-client";
import defaultProfileImage from "./assets/default_profile_image.png";
function App() {
const [notifications, setNotifications] = useState(null);
const [user, setUser] = useState(null);
const [messages, setMessages] = useState(null);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [socketError, setSocketError] = useState(null);
const [socket, setSocket] = useState(null);

const handleLogin = () => {
const connectSocket = io(process.env.REACT_APP_SERVER);
setSocket(connectSocket);
setSocketError(null);
setNotifications([]);
};
const handleLogout = () => {
socket.disconnect();
setUser(null);
};
useEffect(() => {
if (socket) {
socket.once("connect", () => {
socket.emit("authenticate", {
username,
password,
});
});
socket.on("error", (err) => {
setSocketError(err?.message);
console.log(err?.message);
});
socket.on("unauthorized", (data) => {
setSocketError("unauthorized: ", data);
});
socket.on("authorized", () => {
socket.emit("getUser");
});
socket.on("notifications", (data) => {
setNotifications(data);
});
socket.on("messages", (data) => {
setMessages(data);
});
socket.on("user", (data) => {
setUser(data);
});
socket.on("disconnect", (data) => {
console.log("disconnected");
});
}
return () => {
socket?.off();
socket?.disconnect();
};
}, [password, socket, username]);

const getNotifications = (userID) => {
socket.emit("getNotifications", userID);
};
const getMessages = (userID) => {
socket.emit("getMessages", userID);
};
return (
<div className="App">
<h1>Socketio-auth</h1>
{user ? (
<div className="user__container">
<span>Welcome {user?.username}</span>
<img
src={user?.profileImage ? user?.profileImage : defaultProfileImage}
alt="Profile"
/>
{socket.connected ? (
<span>🟢 Connected</span>
) : (
<span>🔴 Disconnected</span>
)}

<button type="button" onClick={handleLogout}>
Logout
</button>
<button
onClick={() => getNotifications(user.id)}
disabled={!socket?.connected}
>
Get notifications
</button>
{notifications && notifications?.length !== 0 ? (
<ul>
{notifications.map((item) => (
<li key={item._id}>
id: {item._id}, title: {item.title}, text: {item.text}
</li>
))}
</ul>
) : (
<p>No notifications</p>
)}
<button
onClick={() => getMessages(user.id)}
disabled={!socket?.connected}
>
Get Messages
</button>
{messages && messages?.length !== 0 ? (
<ul>
{messages.map((item) => (
<li key={item._id}>
id: {item._id}, senderID: {item.senderID}, title: {item.title}
, message: {item.message}
</li>
))}
</ul>
) : (
<p>No messages</p>
)}
</div>
) : (
<div className="login__container">
<h1>Login</h1>
<input
type="text"
value={username}
placeholder="Username"
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="button" onClick={handleLogin}>
Login
</button>
</div>
)}
{socketError ? <p>{socketError}</p> : null}
</div>
);
}

export default App;

We have two functions:

handleLogin: linked to the logon button onClick event

handleLogout: linked to the logout button onClick event

const handleLogin = () => {
const connectSocket = io(process.env.REACT_APP_SERVER);
setSocket(connectSocket);
setSocketError(null);
setNotifications([]);
};
const handleLogout = () => {
socket.disconnect();
setUser(null);
};

and a useEffect hook that run for the first time with the application, and when ever the [password, socket, username] changes

,[password, socket, username]}
useEffect(() => {
if (socket) {
socket.once("connect", () => {
socket.emit("authenticate", {
username,
password,
});
});
socket.on("error", (err) => {
setSocketError(err?.message);
console.log(err?.message);
});
socket.on("unauthorized", (data) => {
setSocketError("unauthorized: ", data);
});
socket.on("authorized", () => {
socket.emit("getUser");
});
socket.on("notifications", (data) => {
setNotifications(data);
});
socket.on("messages", (data) => {
setMessages(data);
});
socket.on("user", (data) => {
setUser(data);
});
socket.on("disconnect", (data) => {
console.log("disconnected");
});
}
return () => {
socket?.off();
socket?.disconnect();
};
}, [password, socket, username]);

First listen to a connection event from the socket and immediately emit “authenticate” event to the server with the username and the pa

socket.once("connect", () => {
socket.emit("authenticate", {
username,
password,
});
});

Then listen to “authorized” event

socket.on("authorized", () => {
socket.emit("getUser");
});

If authorized then get the user information with a “getUser” emit event

Or

“unauthorized” event, then show an unauthorized error

      socket.on("unauthorized", (data) => {
setSocketError("unauthorized: ", data);
});

Other listeners : “error”, “notifications”, “user”, messages”

A useEffect Cleanup function to

    return () => {
socket?.off();
socket?.disconnect();
};

. close all socket listeners

. close the socket connection with .disconnect()

If there is a user the application will render the first part

     <div className="user__container">
<span>Welcome {user?.username}</span>
<img
src={user?.profileImage ? user?.profileImage : defaultProfileImage}
alt="Profile"
/>
{socket.connected ? (
<span>🟢 Connected</span>
) : (
<span>🔴 Disconnected</span>
)}

<button type="button" onClick={handleLogout}>
Logout
</button>
<button
onClick={() => getNotifications(user.id)}
disabled={!socket?.connected}
>
Get notifications
</button>
{notifications && notifications?.length !== 0 ? (
<ul>
{notifications.map((item) => (
<li key={item._id}>
id: {item._id}, title: {item.title}, text: {item.text}
</li>
))}
</ul>
) : (
<p>No notifications</p>
)}
<button
onClick={() => getMessages(user.id)}
disabled={!socket?.connected}
>
Get Messages
</button>
{messages && messages?.length !== 0 ? (
<ul>
{messages.map((item) => (
<li key={item._id}>
id: {item._id}, senderID: {item.senderID}, title: {item.title}
, message: {item.message}
</li>
))}
</ul>
) : (
<p>No messages</p>
)}
</div>

Else a login form with a username and password inputs and a login button

        <div className="login__container">
<h1>Login</h1>
<input
type="text"
value={username}
placeholder="Username"
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="button" onClick={handleLogin}>
Login
</button>
</div>

At the and a <p> tag to show socket errors

  {socketError ? <p>{socketError}</p> : null}

5. Complete Project source code

--

--

Adel Benyahia

Web application developer (HTML │ CSS │ JS | ReactJS | NextJS | NestJS | MERN)