Project Goals
In this seventh stage of your MERN project, you will:
- Begin the process of adding user authentication and authorization features to the MERN app.
Making a new copy of your MERN app
Before continuing further, make a copy of your existing MERN app and place it in a new folder. See the example below:
This makes it easier for me to grade your work and award you marks.
In this copy of your work to date, start the frontend and backend servers, and verify the code runs correctly.
Introduction
Authentication is the process of verifying the identity of a user or system, whereas authorization is the process of granting or denying access to a resource based on the authenticated user’s privileges or permissions. In other words, authentication is about proving who you are, while authorization is about determining what you are allowed to do.
Adding backend security packages
Your first step is to add to your /backend folder the following two packages:
npm i jsonwebtoken bcrypt
Updating your .env
file
In the /backend folder, open your .env file and add a SECRET_KEY value to it. For example:
Adding the users.js
Model file
In your /backend/Models sub-folder, you previously created a products.js model file. Now, create a users.js file in the same sub-folder and paste the following code into it:
import jwt from "jsonwebtoken";
import mongoose from "mongoose";
import dotenv from "dotenv";
dotenv.config();
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
minlength: 5,
maxlength: 50,
},
email: {
type: String,
required: true,
unique: true,
minlength: 5,
maxlength: 225,
},
password: {
type: String,
required: true,
unique: true,
minlength: 5,
maxlength: 1024,
},
isAdmin: {
type: Boolean,
default: false,
},
});
userSchema.methods.generateAuthToken = function () {
const token = jwt.sign(
{ _id: this._id, isAdmin: this.isAdmin },
process.env.SECRET_KEY
);
return token;
};
const User = mongoose.model("User", userSchema);
export default User;
Adding the users.js
Routes file
In your /backend/Routes sub-folder, you previously created a products.js routes file. Now, create a users.js file in the same sub-folder and paste the following code into it:
import express from "express";
import bcrypt from "bcrypt";
import User from "../Models/users.js";
const router = express.Router();
router.get("/:id", async (req, res) => {
const user_id = req.params.id;
const user = await User.findById(user_id);
res.send(JSON.stringify(user));
});
router.post("/register", async (req, res) => {
const newUser = new User({
username: req.body.data.username,
email: req.body.data.email,
password: req.body.data.password,
isAdmin: req.body.data.isAdmin,
});
const user = await User.findOne({ email: newUser.email });
if (user) {
throw new Error("Already in db");
}
const salt = await bcrypt.genSalt(10);
newUser.password = await bcrypt.hash(newUser.password, salt);
await newUser.save();
const token = newUser.generateAuthToken();
const data = {
token: token,
id: newUser.id,
isAdmin: newUser.isAdmin,
};
res.send(data);
});
router.post("/login", async (req, res) => {
const user = await User.findOne({ email: req.body.data.email });
if (!user) {
throw new Error("Not user with that email");
}
const validPassword = await bcrypt.compare(req.body.data.password, user.password);
if (!validPassword) {
throw new Error("Invalid password");
}
const token = user.generateAuthToken();
const data = {
token: token,
userId: user.id,
isAdmin: user.isAdmin,
};
res.send(data);
});
export default router;
Your next step is to import and access the users.js Routes file in your index.js file.
In your index.js code, you are importing the userRoutes from the users.js file and using them as middleware for the path /users.
Note the plural ‘s’.
This means that any request to the server with a path that starts with /users will be handled by the routes defined in users.js.
Note also that you do not need to create a users collection in your MongoDB Atlas database. A collection with that name will be created when you register your first user.
Adding the security middleware
To secure the routes, follow these steps to add a middleware function:
- In your /backend folder, create a new sub-folder named Middleware.
-
In this backend/Middleware sub-folder, create a new text file named
auth.js and paste the following code into it:
This code checks for an "Authorization" header in the request, and then decodes it.import jwt from "jsonwebtoken"; import dotenv from "dotenv"; dotenv.config(); function isAuth(req, res, next) { const authHeader = req.get("Authorization"); if (!authHeader) { req.isAuth = false; return next(); } const token = authHeader.split(" ")[1]; if (!token || token === "") { { req.isAuth = false; return next(); } } try { const decoded = jwt.verify(token, process.env.SECRET_KEY); req.user = decoded; req.isAuth = true; next(); } catch (ex) { req.isAuth = false; next(); } } export default isAuth;
- To use the auth middleware, import it into your index.js file and add it to the app’s middleware stack:
Adding a context provider
To handle the authentication flow in the frontend, you need to use a context provider. The context provider ensures that we can access the authentication variables wherever we need them. Follow these steps to create the required context:
- In your frontend/src folder, create a new sub-folder named context.
- In this sub-folder, create a new text file named authContext.jsx, and paste into it the code below:
import React from "react"; export const AuthContext = React.createContext({ token: null, userId: null, isAdmin: false, login: (token, userId, isAdmin) => {}, logout: () => {}, });
- Also in your frontend/src folder, create a new sub-folder named util.
- In this sub-folder, create a new text file named authRoutes.jsx, and paste into it the code below:
import React from "react"; import { AuthContext } from "../context/authContext.jsx"; import { useLocation, Navigate } from "react-router-dom"; const RequiredAuth = ({ children }) => { const authContext = React.useContext(AuthContext); const location = useLocation(); const token = localStorage.getItem("token"); console.log(token); if (token === null && authContext.token === null) { return <Navigate to="/login" state={{ from: location }} replace />; } return children; }; export default RequiredAuth;
- Finally, in your frontend/src folder, update your app.jsx file as follows:
import React, { useState } from "react"; import { Route, Routes } from "react-router-dom"; import HomePage from "./pages/HomePage.jsx"; import AddProductPage from "./pages/AddProductPage.jsx"; import UpdateProductPage from "./pages/UpdateProductPage.jsx"; import AuthPage from "./pages/AuthPage.jsx"; import AdminPage from "./pages/AdminPage.jsx"; import { AuthContext } from "./context/authContext.jsx"; import RequiredAuth from "./util/authRoutes.jsx"; function App() { const [userLoggedData, setUserLoggedData] = useState({ token: null, userId: null, isAdmin: false, }); const login = (token, userId, isAdmin) => { //console.log("app token", token); localStorage.setItem("token", token); localStorage.setItem("userId", userId); setUserLoggedData({ token: token, userId: userId, isAdmin: isAdmin }); }; const logout = () => { setUserLoggedData({ token: null, userId: null, isAdmin: false }); localStorage.removeItem("token"); localStorage.removeItem("userId"); }; return ( <AuthContext.Provider value={{ token: userLoggedData.token, userId: userLoggedData.userId, isAdmin: userLoggedData.isAdmin, login: login, logout: logout, }} > <Routes> <Route path="/" element={<HomePage />} /> <Route path="/login" element={<AuthPage />} /> {/* protected views*/} <Route path="/addProduct" element={ <RequiredAuth> <AddProductPage /> </RequiredAuth> } /> <Route path="/update/:id" element={ <RequiredAuth> <UpdateProductPage /> </RequiredAuth> } /> <Route path="/admin" element={ <RequiredAuth> <AdminPage /> </RequiredAuth> } /> </Routes> </AuthContext.Provider> ); } export default App;
In its current form, the Express backend of your MERN app should run correctly. However, the ReactJS frontend will generate errors as a result of missing, yet-to-be created component files.