Authentication and authorisation

Implementing basic authentication and authorization for your e-commerce MERN app.

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:

screenshot

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:

screenshot

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.

screenshot screenshot

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:

  1. In your /backend folder, create a new sub-folder named Middleware.
  2. In this backend/Middleware sub-folder, create a new text file named auth.js and paste the following code into 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;
    This code checks for an "Authorization" header in the request, and then decodes it.
  3. To use the auth middleware, import it into your index.js file and add it to the app’s middleware stack: screenshot screenshot

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:

  1. In your frontend/src folder, create a new sub-folder named context.
  2. 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: () => {},
    });
  3. Also in your frontend/src folder, create a new sub-folder named util.
  4. 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;
  5. 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.

Next: Adding UI elements for user flow.