Understanding User Authentication System the MERN Stack: A Step-by-Step Guide | Part1 - Server Side
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
- Introduction
- Setting Up the Project
- Setting Up Envieronment Variables
- Configuring the Database
- Creating Models
- Building Controllers
- Implementing Middleware
- Setting Up Routes
- 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 serverjsonwebtoken
for handling tokensbcryptjs
for password hashingdotenv
for managing environment variablesmongoose
for our databasenodemon
for automatically restarting the Node.js servercors
for 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 callsnext()
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 dev
and 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.
- cobanalitalha@gmail.com
- github.com/carpodok
- linkedin.com/alitalhacoban