Understanding User Authentication System the MERN Stack: A Step-by-Step Guide | Part1 - Server Side

Ali Talha Çoban
8 min readJun 19, 2024

--

Hello fellows developers! In this story we’ll build an authentication system include both server and client side. However, in this part we’ll build the server side only.

Here is the repo link of this project. You can check it out beforehand

Table of Contents

  1. Introduction
  2. Setting Up the Project
  3. Setting Up Envieronment Variables
  4. Configuring the Database
  5. Creating Models
  6. Building Controllers
  7. Implementing Middleware
  8. Setting Up Routes
  9. Test OurServer

1. Introduction

Authentication is a crucial part of any application. It ensures that users are who they claim to be and grants them access to the resources they are entitled to. In this guide, we’ll build a simple token based authentication system using Node.js, Express, and JWT (JSON Web Tokens).

Your project folder sturcture will be look like this at the end of the story:

OK, let’s get start!

2: Setting Up the Project

First, let’s set up our project. Open your terminal and run the commands below:

mkdir user-auth-system
cd user-auth-system
mkdir server
mkdir client
cd server
npm init -y

We created a project folder includes both server and client folders. However, we’ll build the client side in Part 2. Now let’s install the necessary packages we’ll use in the server side of the project.

We’ll use ;

  • express for our server
  • jsonwebtoken for handling tokens
  • bcryptjs for password hashing
  • dotenv for managing environment variables
  • mongoose for our database
  • nodemon for automatically restarting the Node.js server
  • corsfor enabling Cross-Origin Resource Sharing

Run the command below to install all these packages:

npm install express jsonwebtoken bcryptjs dotenv mongoose nodemon cors

3. Setting Up Enviroment Variables

Create a.env file in the root directory and add the following environment variables.

MONGO_URI="mongodb://127.0.0.1/UserAuthSystem" // replace it with yours, if you have
PORT=5000
JWT_SECRET=your_jwt_secret_key
  • JWT_SECRET : The JWT (JSON Web Token) secret key is a string used to sign and verify tokens. It ensures the integrity and authenticity of the token by allowing the server to confirm that the token was issued by a trusted source and has not been tampered with. Think of the secret key like a signature or seal on a document that proves it is genuine.
  • MONGO_URI: The MongoDB URI is a connection string used to connect your application to a MongoDB database. It contains information such as the database address, port number, database name, and authentication credentials. Think of it like a mailing address for your database, allowing your application to find and communicate with it. Users can use both local and cloud-based connection strings.

Note that for security reasons, the .env file in a Node.js project should be included in the .gitignore file to prevent sensitive information from being exposed in version control.

4. Creating the Server

Let’s create a simple Express server by creating a file named server.js and add this code:

require("dotenv").config();
const express = require("express");
const app = express();
const cors = require("cors");

require("./config/db");

//Middlewares
app.use(cors());
app.use(express.json());

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`SERVER RUNNING ON PORT: ${PORT}`));

Run the code with:

node src/server.js

You can also use the nodemon package to start the server. Using nodemon, you don’t need to restart the server after file changes.

We need to update scripts section of our package.json file:

 "scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js"
},

Now run the command:

npm run dev

You should see Server running on port 5000 in your terminal. If you do, nice! Our server is up and running!

5. Configuring the Database

We’ll use MongoDB as our database. Create a db.js file in the src/config folder to configure the database connection.

const mongoose = require("mongoose");
mongoose
.connect(process.env.MONGO_URI)
.then(() => console.log("DATABASE CONNECTED"))
.catch((err) => {
console.log("DATABASE CONNECTION ERROR", err);
process.exit(1);
});

You can use the URL ‘mongodb://127.0.0.1/UserAuthSystem’ for local usage. However, you can also create a database online from here. All you need to do is create a project and then create a cluster. By using the URL obtained from there, you can use your own database. It’ll work both ways without any problems.

6. Creating Models

Next, we’ll create a User model in src/modesl/User.js

const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");

let UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
index: true,
},
email: {
type: String,
required: true,
unique: true,
match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/],
},
password: {
type: String,
required: true,
select: false,
},
created_at: {
type: String,
},
});

UserSchema.pre("save", async function (next) {
if (!this.isModified("password")) {
return next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});

module.exports = mongoose.model("user", UserSchema);
  • Before saving a user document, pre-save hook checks if the password has been modified.
  • If the password is modified, it generates a salt and hashes the password using bcryptjs before saving it to the database.

7. Building Controllers

Controllers handle the logic for the routes. Now let’s create authController.js in the src/controllers folder.

const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const { check, validationResult } = require("express-validator");

const User = require("../models/User");

async function Login(req, res) {
try {
await check("email", "Please include a valid email").isEmail().run(req);
await check("password", "Password is required").exists().run(req);

const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

const { email, password } = req.body;

let user = await User.findOne({ email }).select("+password");
if (!user) {
return res.status(400).json({
msg: "invalid credentials",
success: false,
});
}

const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(400).json({
msg: "invalid credentials",
success: false,
});
}

jwt.sign(
{ id: user._id },
process.env.JWT_SECRET,
{ expiresIn: "10m" },
(err, token) => {
if (err) throw err;
res.status(200).json({
token,
});
}
);
} catch (err) {
console.log(err);
res.status(400).json({ success: false });
}
}

async function Register(req, res) {
try {
await check("username", "Username is required").not().isEmpty().run(req);
await check("email", "Please include a valid email").isEmail().run(req);
await check("password", "Password must be 6 or more characters")
.isLength({ min: 6 })
.run(req);

const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}

const { username, email, password } = req.body;

let user = await User.findOne({ email });
if (user) {
return res.status(400).json({
msg: "user already exists",
success: false,
});
}

user = new User({
username,
email,
password,
});

await user.save();

jwt.sign(
{ id: user._id },
process.env.JWT_SECRET,
{ expiresIn: 36000 },
(err, token) => {
if (err) throw err;
res.status(200).json({
token,
});
}
);
} catch (err) {
console.log(err);
res.status(400).json({ success: false });
}
}

async function GetUser(req, res) {
try {
const user = await User.findById(req.user.id);
res.status(200).json({
user,
success: true,
});
} catch (err) {
console.error(err.message);
res.status(500).json({ msg: "SERVER ERROR" });
}
}

module.exports = { Login, Register, GetUser };
  • This controller file provides the core functionality for logging in, registering, and retrieving user data, utilizing JSON Web Tokens for authentication. We’ll use these functions in our routes.
  • In the Login and Register parts of our authentication system, a token is generated. This token is included in the response sent to the client. It serves a crucial role in authentication verification. Additionally, on the client side, the token is saved in cookies for future use.
  • We use express-validator npm package in order to check if the email and password are provided and valid.

8. Implementing Middleware

We need a middleware to verify the token. Before continuning, let me explain what middleware is.

  • Middleware is a function in a web application that processes requests before they reach the final route handler. It can handle tasks such as authentication, logging, or modifying request and response objects.
  • Think of middleware like security checks and processes at an airport. Just as you go through multiple checkpoints (like security, customs, and boarding) before getting on the plane, a request passes through various middleware functions before reaching the final route handler. Each middleware performs specific tasks and then passes the request to the next one, making the code modular and reusable.

We achieve this in src/middlewares/verfyAuth.js.

const jwt = require("jsonwebtoken");
module.exports = (req, res, next) => {
const token = req.header("x-token");
if (!token) {
return res.status(401).json({
msg: "No valid token",
success: false,
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(400).json({
msg: "No valid token",
success: false,
});
}
};
  • This code snippet check the token from the request header that is assigned as x-token.
  • If the token is valid, it decodes the token and assigns the decoded user information to req.user, then calls next() to pass control to the next middleware or route handler.

9. Setting Up Routes

Now, let’s set up our routes. Create auth.js in the src/routes folder.

const express = require("express");
const { Login, Register, GetUser } = require("../controllers/authController");
const verifyAuth = require("../middlewares/verifyAuth");

const router = express.Router();

router.post("/login", Login);

router.post("/register", Register);

router.get("/user", verifyAuth, GetUser);

module.exports = router;

We also need to update our server.js file and connect our routes.

require("dotenv").config();
const express = require("express");
const app = express();
const cors = require("cors");

require("./config/db");

//Middlewares
app.use(cors());
app.use(express.json());

// Routes
app.use("/", require("./routes/auth"));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`SERVER RUNNING ON PORT: ${PORT}`));

10. Test Our Server

It’s time to test our server. Let’s see if it works as it should

  • Run the server again with npm run devand test the /register and /login endpoints using one of the API platform like Postman.
  • Alternatively, I can suggest another easy way to test your endpoints. You can use a VS Code extension REST Client.
  • In order to utilize this extension, you need to create a file with .rest extension. And in that file you can type your http methods and send request in an easy way. You can also use this method if you prefer to test your routes.

Here is the routes.rest

POST http://localhost:3000/login
Content-Type: application/json

{
"email":"test@gmail.com",
"password":"123456"
}

###

POST http://localhost:3000/register
Content-Type: application/json

{

"email":"test@gmail.com",
"username":"alitalhacoban",
"password":"123456"
}

###

GET http://localhost:3000/user
Content-Type: application/json
x-token:your_access_token

In this article, we’ve built a simple token-based authentication system in Node.js. This setup gives you a solid foundation for implementing secure authentication in your Node.js applications. In Part 2, we’ll build the client side of this project, diving into how to create an user interface for authentication and how to connect with our server.

I hope the article helps you guys. Stay tuned and give it a clap if you want more content like this! Happy coding!

Check out the link below to continue the journey.

--

--