This content originally appeared on DEV Community and was authored by Nuel
In this tutorial, we will build a full-stack MERN (MongoDB, Express.js, React.js, and Node.js) authentication web app. We will use JSON Web Tokens (JWT) for authentication and authorization purposes.
Before we start, make sure you have installed the latest versions of Node.js and MongoDB on your machine.
We be creating two directories for this project; one for the backend and another for the frontend. We will start the backend directory. Create a new directory, name it anything you like (I have named mine "mern-auth-backend").
Firstly, let's define our project structure:
We can use this ensure we're on track.
Step 1: Installation
For this project, we need would a couple of dependencies. In your terminal, navigate to the backend directory and write:
npm init -y
This would create a new package.json file in the directory. Next install the following dependencies:
npm i express bcryptjs cors dotenv express-async-handler jsonwebtoken mongoose nodemailer nodemon
Step 2: Set up the Server
Open the package.json file and add the following to the scripts object:
"server": "nodemon server.js"
Nodemon is a tool that automatically detects changes to a Node.js application and restarts the server. It's often used during development to speed up the development process by eliminating the need to manually restart the server every time changes are made to the code.
Next, Create a new file called server.js and add the following code:
const express = require("express");
const dotenv = require("dotenv");
const cors = require("cors");
dotenv.config();
const port = 5000;
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.listen(port, () => {
console.log(`Server running on port: ${port}`);
});
In the above code, we have imported the modules necessary to spin up a basic server, defined our port address and initialized the necessary middleware. We can now start our server by running the following command:
npm run server
If everything is in order, we should see the following in our terminal:
Now, we have successfully created an express server running on localhost port 5000.
Step 3: Set up the Database
Head over to MongoDB Atlas to set up the database.
MongoDB project setup
Next, we will connect our mongoDB database to our backend application. After creating a project, you can get your connection string (explained in the video above).
We will create a .env file in our application to put all the environment variables we would need in building our server. This file is usually where we put variables that are private and should not be exposed to public view.
In the .env file, add the following:
PORT=5000
MONGO_URI=<YOUR MONGODB CONNECTION STRING>
NODE_ENV=development
Back in our server.js file, we can get our port variable from the .env file:
const port = process.env.PORT;
Our server file now looks like this:
const express = require("express");
const dotenv = require("dotenv");
const cors = require("cors");
dotenv.config();
const port = process.env.PORT;
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.listen(port, () => {
console.log(`Server running on port: ${port}`);
});
Next, create a directory called "config". In that directory, create a file called "db.js" and add the following code:
const mongoose = require("mongoose");
mongoose.set("strictQuery", false);
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI);
console.log("MongoDB connected");
} catch (error) {
console.log(error);
process.exit(1);
}
};
module.exports = connectDB;
Back in our server.js file, we will import the connection function which we wrote in the db.js file and call the function:
const connectDB = require("./config/db");
connectDB();
Now our server.js file looks like this:
const express = require("express");
const dotenv = require("dotenv");
const cors = require("cors");
dotenv.config();
const port = process.env.PORT;
const connectDB = require("./config/db");
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
connectDB();
app.listen(port, () => {
console.log(`Server running on port: ${port}`);
});
If we've done done everything correctly, we should see the following in our terminal:
Notice the "MongoDB connected" in the terminal. This means our database is now connected to our application.
Step 4: Create User Model and token Model
Create a new directory called "Models", within this directory, we will create two new files called "userModel.js" and "tokenModel.js". In these files, we will define what our registered users should look like and what properties a user should have, also we would define how token should look like. The token is what we would use to verify our users. Add the following codes to their respective file:
userModel.js
const { Schema, model } = require("mongoose");
const userSchema = Schema(
{
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
password: {
type: String,
required: true,
},
verified: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
}
);
const User = model("User", userSchema);
module.exports = User;
tokenModel.js
const { Schema, model } = require("mongoose");
const tokenSchema = Schema({
user: {
type: Schema.Types.ObjectId,
required: true,
ref: "User",
},
token: {
type: String,
required: true,
},
});
const Token = model("Token", tokenSchema);
module.exports = Token;
Step 5: Create Middlewares
Middlewares are extra function that we use handle various tasks. For this project, we are to create two (2) middlewares: "errorMiddleware.js" and "emailMiddleware.js".
errorMiddleware.js
const errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode ? res.statusCode : 500;
res.status(statusCode);
res.json({
message: err.message,
stack: process.env.NODE_ENV === "production" ? null : err.stack,
});
};
module.exports = {
errorHandler,
};
With the above code, we can send customized errors to our users when the need arises.
emailMiddleware.js
const nodemailer = require("nodemailer");
const sendEmail = async (email, subject, html) => {
try {
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL,
pass: process.env.PASS,
},
authMethod: "PLAIN",
});
await transporter.sendMail({
from: process.env.USER,
to: email,
subject: subject,
html: html,
});
console.log("email sent");
} catch (error) {
console.log(error);
console.log("email not sent");
}
};
module.exports = sendEmail;
With the above code, we can send emails from one Gmail account to another with customized messages and links, etc.
The above code needs some environment variables and some further tweaking before it can run smoothly. In our .env file, let us add the necessary variables:
EMAIL=yourgmail@gmail.com
PASS=<YOUR SECRET PASSWORD>
BASE_URL=http://localhost:5000
FRONTEND_URL=http://localhost:5173
- The PASS variable refers to "an application-specific password", not your actual email password. To generate an application-specific password for your Node.js application to use with Gmail, follow these steps:
Go to your Google Account settings (https://myaccount.google.com/).
Click on "Security" on the left-hand side of the page.
Under the "Signing in to Google" section, click on "App passwords".
Select "Mail" and "Other (custom name)".
Enter a name for your app password (e.g. "Node.js app").
Click on "Generate".
Copy the generated password and use it in your Node.js code.
The BASE_URL is our backend url "http://localhost:5000"
The FRONTEND_URL is our frontend domain/url. Mine is "http://localhost:5173".
Next, in our server.js, we can use the errorMiddleware to catch all the errors within our application.
const { errorHandler } = require("./middlewares/errorMiddleware");
app.use(errorHandler);
Now, our server.js file looks like this:
const express = require("express");
const dotenv = require("dotenv");
const cors = require("cors");
dotenv.config();
const port = process.env.PORT;
const { errorHandler } = require("./middlewares/errorMiddleware");
const connectDB = require("./config/db");
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(errorHandler);
connectDB();
app.listen(port, () => {
console.log(`Server running on port: ${port}`);
});
Step 6: Create user controller
Create a new directory called "controllers", add a new file to this directory called "userController.js". In this file, we would write the various functions to create/Register a user, send verification emails to verify our users, login users and update forgotten password.
First, Let's tackle the registration of users. In the userController.js file, add the following:
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const asyncHandler = require("express-async-handler");
const User = require("../models/userModel");
const Token = require("../models/tokenModel");
const crypto = require("crypto");
const sendEmail = require("../middlewares/emailMiddleware");
const register = asyncHandler(async (req, res) => {
const { name, email, password } = req.body;
if (!name || !email || !password) {
res.status(400);
throw new Error("Please add all fields");
}
const existingUser = await User.findOne({
email: new RegExp("^" + email + "$", "i"),
});
if (existingUser) {
res.status(400);
throw new Error("User already exist");
}
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const user = await User.create({
name,
email,
password: hashedPassword,
});
const token = await Token.create({
user: user.id,
token: crypto.randomBytes(32).toString("hex"),
});
const message = `
<h3>Welcome to MERN-Auth-Tutorial.</h3>
<p>Please click the link below to verify your email.</p>
<a href="${process.env.BASE_URL}/verify/${user.id}/${token.token}?redirect=${process.env.FRONTEND_URL}/verify/${user.id}/${token.token}">verify email</a>
`;
await sendEmail(user.email, "verify email", message);
res.status(201).json({
message: "An email has been sent to your account",
});
});
module.exports = {
register,
};
In the above code, we imported the necessary modules, including the userSchema and tokenSchema so we can create users and tokens according to the schemas, we also imported crypto which is a javascript in-built function which we used to generate random strings as tokens for verification.
Then we:
- Get the required fields to create a user,
- Check that user registering does not already exist,
- hash the user's password for secrecy,
- save the user in the database,
- generate a token for verification,
- send a customized message that will eventually lead the verified user to the frontend application.
Login
const login = asyncHandler(async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({
email: new RegExp("^" + email + "$", "i"),
});
if (!user) {
res.status(400);
throw new Error("Invalid Credentials");
}
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
res.status(400);
throw new Error("Invalid Credentials");
}
res.status(200).json({
id: user.id,
name: user.name,
email: user.email,
verified: user.verified,
token: generateToken(user.id),
});
});
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "1d" });
};
module.exports = {
register,
login,
};
In the above code, we:
- Get the login parameters (email and password),
- Check that user to be logged in exists in the database,
- Match the password with that of the existing user,
- Then return the logged in user with a jwt token.
Verify Email
const verifyEmail = asyncHandler(async (req, res) => {
const user = await User.findOne({
_id: req.params.id,
});
if (!user) {
res.status(400);
throw new Error("Invalid link");
}
const token = await Token.findOne({
user: user.id,
token: req.params.token,
});
if (!token) {
res.status(400);
throw new Error("Invalid link");
}
await User.findOneAndUpdate(
{
_id: req.params.id,
},
{
verified: true,
},
{
new: true,
}
);
const redirectUrl = req.query.redirect;
res.redirect(redirectUrl);
});
In the above code, we:
- Get the user id and token from request parameters,
- verify that both the user and token exist in the database,
- After verifying, we update the verified property of the user to true.
Send Reset Password Link
const sendResetPasswordLink = asyncHandler(async (req, res) => {
const { email } = req.body;
if (!email) {
res.status(400);
throw new Error("Please enter your email");
}
const user = await User.findOne({
email: new RegExp("^" + email + "$", "i"),
});
if (!user) {
res.status(400);
throw new Error("This user does not exist");
}
const token = await Token.create({
user: user.id,
token: crypto.randomBytes(32).toString("hex"),
});
const message = `
<h3>Password Reset</h3>
<p>Please click the link below to reset your password.</p>
<a href="${process.env.BASE_URL}/reset-password/${user.id}/${token.token}?redirect=${process.env.FRONTEND_URL}/reset-password/${user.id}/${token.token}">Reset password</a>
`;
await sendEmail(user.email, "Password reset", message);
res.status(200).json({
message: "An email has been sent to your account",
});
});
In the code above, we send a reset password link to the user's email.
Verify Password Link
const verifyResetPasswordLink = asyncHandler(async (req, res) => {
const user = await User.findOne({
_id: req.params.id,
});
if (!user) {
res.status(400);
throw new Error("Invalid link");
}
const token = await Token.findOne({
user: user.id,
token: req.params.token,
});
if (!token) {
res.status(400);
throw new Error("Invalid link");
}
const redirectUrl = req.query.redirect;
res.redirect(redirectUrl);
});
In the above code, we verify the user and the token and then navigate to the frontend application for the user to enter a new password.
Update Password
const updatePassword = asyncHandler(async (req, res) => {
const { password } = req.body;
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const updatedUser = await User.findOneAndUpdate(
{
_id: req.params.id,
},
{
password: hashedPassword,
},
{
new: true,
}
);
res.status(200).json({
id: updatedUser.id,
name: updatedUser.name,
email: updatedUser.email,
verified: updatedUser.verified,
token: generateToken(updatedUser.id),
});
});
In the above code, we take the new password, hash it and then replace the old password with new one. Now the user can log in with a new password.
Now our userController file looks like this:
const jwt = require("jsonwebtoken");
const bcrypt = require("bcryptjs");
const asyncHandler = require("express-async-handler");
const User = require("../models/userModel");
const Token = require("../models/tokenModel");
const crypto = require("crypto");
const sendEmail = require("../middlewares/emailMiddleware");
const register = asyncHandler(async (req, res) => {
const { name, email, password } = req.body;
if (!name || !email || !password) {
res.status(400);
throw new Error("Please add all fields");
}
const existingUser = await User.findOne({
email: new RegExp("^" + email + "$", "i"),
});
if (existingUser) {
res.status(400);
throw new Error("User already exist");
}
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const user = await User.create({
name,
email,
password: hashedPassword,
});
const token = await Token.create({
user: user.id,
token: crypto.randomBytes(32).toString("hex"),
});
const message = `
<h3>Welcome to MERN-Auth-Tutorial.</h3>
<p>Please click the link below to verify your email.</p>
<a href="${process.env.BASE_URL}/verify/${user.id}/${token.token}?redirect=${process.env.FRONTEND_URL}/verify/${user.id}/${token.token}">verify email</a>
`;
await sendEmail(user.email, "verify email", message);
res.status(201).json({
message: "An email has been sent to your account",
});
});
const login = asyncHandler(async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({
email: new RegExp("^" + email + "$", "i"),
});
if (!user) {
res.status(400);
throw new Error("Invalid Credentials");
}
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
res.status(400);
throw new Error("Invalid Credentials");
}
res.status(200).json({
id: user.id,
name: user.name,
email: user.email,
verified: user.verified,
token: generateToken(user.id),
});
});
const verifyEmail = asyncHandler(async (req, res) => {
const user = await User.findOne({
_id: req.params.id,
});
if (!user) {
res.status(400);
throw new Error("Invalid link");
}
const token = await Token.findOne({
user: user.id,
token: req.params.token,
});
if (!token) {
res.status(400);
throw new Error("Invalid link");
}
await User.findOneAndUpdate(
{
_id: req.params.id,
},
{
verified: true,
},
{
new: true,
}
);
const redirectUrl = req.query.redirect;
res.redirect(redirectUrl);
});
const sendResetPasswordLink = asyncHandler(async (req, res) => {
const { email } = req.body;
if (!email) {
res.status(400);
throw new Error("Please enter your email");
}
const user = await User.findOne({
email: new RegExp("^" + email + "$", "i"),
});
if (!user) {
res.status(400);
throw new Error("This user does not exist");
}
const token = await Token.create({
user: user.id,
token: crypto.randomBytes(32).toString("hex"),
});
const message = `
<h3>Password Reset</h3>
<p>Please click the link below to reset your password.</p>
<a href="${process.env.BASE_URL}/reset-password/${user.id}/${token.token}?redirect=${process.env.FRONTEND_URL}/reset-password/${user.id}/${token.token}">Reset password</a>
`;
await sendEmail(user.email, "Password reset", message);
res.status(200).json({
message: "An email has been sent to your account",
});
});
const verifyResetPasswordLink = asyncHandler(async (req, res) => {
const user = await User.findOne({
_id: req.params.id,
});
if (!user) {
res.status(400);
throw new Error("Invalid link");
}
const token = await Token.findOne({
user: user.id,
token: req.params.token,
});
if (!token) {
res.status(400);
throw new Error("Invalid link");
}
const redirectUrl = req.query.redirect;
res.redirect(redirectUrl);
});
const updatePassword = asyncHandler(async (req, res) => {
const { password } = req.body;
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const updatedUser = await User.findOneAndUpdate(
{
_id: req.params.id,
},
{
password: hashedPassword,
},
{
new: true,
}
);
res.status(200).json({
id: updatedUser.id,
name: updatedUser.name,
email: updatedUser.email,
verified: updatedUser.verified,
token: generateToken(updatedUser.id),
});
});
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: "1d" });
};
module.exports = {
register,
login,
verifyEmail,
sendResetPasswordLink,
verifyResetPasswordLink,
updatePassword,
};
Step 7: Create user routes
Routes in Node.js are like directions for a server application to respond to requests from clients (such as web browsers or mobile apps). They specify the URL patterns and HTTP methods that should trigger specific code to run in the server, which generates a response to send back to the client.
Create a new directory called "routes". In this directory, create a file called "userRoutes.js". Add the following code the file:
const express = require("express");
const router = express.Router();
const {
register,
login,
verifyEmail,
sendResetPasswordLink,
verifyResetPasswordLink,
updatePassword,
} = require("../controllers/userController");
router.post("/register", register);
router.post("/login", login);
router.get("/verify/:id/:token", verifyEmail);
router.post("/forgot-password", sendResetPasswordLink);
router.get("/reset-password/:id/:token", verifyResetPasswordLink);
router.put("/update-password/:id", updatePassword);
module.exports = router;
In the above code, we have import the necessary modules and also imported the functions we created in userController.js. These functions such as register would be triggered when these routes are hit by a client.
Back in our server.js file, we would import the user routes:
const userRoutes = require("./routes/userRoutes");
app.use("/", userRoutes);
In the code above, we imported the user routes we created in userRoutes.js and "used" them in our server. The "/" in the second line of code simply implies that our base url for routes is "http://localhost:5000".
Now our complete server.js file looks like this:
const express = require("express");
const dotenv = require("dotenv");
const cors = require("cors");
dotenv.config();
const port = process.env.PORT;
const { errorHandler } = require("./middlewares/errorMiddleware");
const connectDB = require("./config/db");
const userRoutes = require("./routes/userRoutes");
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(errorHandler);
app.use("/", userRoutes);
connectDB();
app.listen(port, () => {
console.log(`Server running on port: ${port}`);
});
And with that, our backend is fully functional. We can create new users, login, verify emails and change passwords!
We can test our API endpoints in postman before we build our frontend.
We would build the frontend of our application in another article. Cheers!!
Resources
This content originally appeared on DEV Community and was authored by Nuel
Nuel | Sciencx (2023-02-28T23:47:05+00:00) MERN Authentication app (Backend). Retrieved from https://www.scien.cx/2023/02/28/mern-authentication-app-backend/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.