Nuxt.js And Express.js Authentication with Auth Module and JWT

Hi, today we want to build an authentication app with Nuxt.js and Express.js, we'll focus on the front-end, and nuxt auth module and we will make a local authentication.
I remember myself when i wanted to add authentication in the app and how I was stuck in it because I couldn't find a good resource for it.
so I decided to write this article for other people to use it and don't get in trouble like me
This article is made for full-stack developers and also front-end developers so if you're not a full-stack web developer don't worry I will explain everything that you can add authentication on your app

This application does not support of Refresh token for back-end, it's supported for front-end but commented because back-end had not set it and if we set it on front-end it will produce an error, in other words for back-end is just all about Access token, if you want to have Refresh token too, please give attention to last lines of this post

before the start I should say that you need to know about these technologies:

Vue & Nuxt.js
Express.js & Mongodb (If you want also implement API)

so let's start it

Nuxt.js

1- make a new app with npx create-nuxt-app front
2- choose the Axios module when making new app (if you didn't don't worry we will install it later)
3- install nuxt module and the Axios if you didn't
yarn add --exact @nuxtjs/auth-next
yarn add @nuxtjs/axios
or with npm
npm install --save-exact @nuxtjs/auth-next
npm install @nuxtjs/axios

then add it on nuxt.config.js like below:

{
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/auth-next'
  ],
  auth: {
    // Options
  }
}

now it's time to add options on auth module :

auth: {
    strategies: {
      local: {
//      scheme: "refresh",
        token: {
          property: "token", //property name that the Back-end sends for you as a access token for saving on localStorage and cookie of user browser
          global: true,
          required: true,
          type: "Bearer"
        },
        user: {
          property: "user",
          autoFetch: true
        },
//      refreshToken: {  // it sends request automatically when the access token expires, and its expire time has set on the Back-end and does not need to we set it here, because is useless
//        property: "refresh_token", // property name that the Back-end sends for you as a refresh token for saving on localStorage and cookie of user browser
//        data: "refresh_token", // data can be used to set the name of the property you want to send in the request.
//      },
        endpoints: {
          login: { url: "/api/auth/login", method: "post" },
//        refresh: { url: "/api/auth/refresh-token", method: "post" },
          logout: false, //  we don't have an endpoint for our logout in our API and we just remove the token from localstorage
          user: { url: "/api/auth/user", method: "get" }
        }
      }
    }
  },

and here is the config for the Axios that I recommend to use it:

axios: {
    baseURL: "http://localhost:8080"  // here set your API url
  },

Tip: if your back-end project has implemented Refresh token you should uncomment the refresh endpoint and change it to the right endpoint which your back-end gave you and also you should uncomment these refreshToken object and also scheme: refresh

now we make some components:

/components/Auth/Login/index.vue

<template>
  <div>
    <form @submit.prevent="login">
      <div class="mb-3">
        <label for="email" class="form-label">Email address</label>
        <input
          type="email"
          class="form-control"
          id="email"
          v-model="loginData.email"
          aria-describedby="emailHelp"
        />
      </div>
      <div class="mb-3">
        <label for="password" class="form-label">Password</label>
        <input
          type="password"
          v-model="loginData.password"
          class="form-control"
          id="password"
        />
      </div>
      <button type="submit" class="btn btn-primary w-100">login</button>
    </form>
  </div>
</template>
<script>
export default {
  data() {
    return {
      loginData: {
        email: "",
        password: ""
      }
    };
  },
  methods: {
    async login() {
      try {
        let response = await this.$auth.loginWith("local", {
          data: this.loginData
        });
        this.$router.push("/");
        console.log(response);
      } catch (err) {
        console.log(err);
      }
    }
  }
};
</script>
<style></style>

/components/Auth/Register/index.vue

<template>
    <div>
      <form @submit.prevent="register">
        <div class="mb-3">
          <label for="fullname" class="form-label">Full Name</label>
          <input
            type="text"
            v-model="registerData.fullname"
            class="form-control"
            id="fullname"
          />
        </div>
        <div class="mb-3">
          <label for="email" class="form-label">Email address</label>
          <input
            type="email"
            class="form-control"
            id="email"
            v-model="registerData.email"
            aria-describedby="emailHelp"
          />
        </div>
        <div class="mb-3">
          <label for="password" class="form-label">Password</label>
          <input
            type="password"
            v-model="registerData.password"
            class="form-control"
            id="password"
          />
        </div>
        <button type="submit" class="btn btn-primary w-100">Register</button>
      </form>
    </div>
  </template>
  <script>
  export default {
    data() {
      return {
        registerData: {
          fullname: "",
          email: "",
          password: ""
        }
      };
    },
    methods: {
      async register() {
        try {
          const user = await this.$axios.$post("/api/auth/signin", {
            fullname: this.registerData.fullname,
            email: this.registerData.email,
            password: this.registerData.password
          });
          console.log(user);
        } catch (err) {
          console.log(err);
        }
      }
    }
  };
  </script>
  <style></style>

and

/components/Home/index.vue

<template>
    <div>
      <h2>You're in home page</h2>
    </div>
  </template>
  <script>
  export default {};
  </script>
  <style></style>

and

/components/User/index.vue

<template>
    <div>
      Hello dear <b style="color:red">{{ getUserInfo.fullname }}</b> you're in
      profile page
      <hr />
      This is your information:
      <br /><br />
      <table class="table">
        <thead>
          <tr>
            <th scope="col">ID</th>
            <th scope="col">FullName</th>
            <th scope="col">Email</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>{{ getUserInfo.id }}</td>
            <td>{{ getUserInfo.fullname }}</td>
            <td>{{ getUserInfo.email }}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </template>

  <script>
  export default {
    computed: {
      getUserInfo() {
        return this.$store.getters.getUserInfo;
      }
    }
  };
  </script>

  <style></style>

and

/components/Layouts/Header/index.vue

<template>
    <div>
      <nav class="navbar navbar-expand-lg navbar-light bg-light">
        <div class="container-fluid">
          <nuxt-link class="navbar-brand" to="/">Navbar</nuxt-link>
          <button
            class="navbar-toggler"
            type="button"
            data-bs-toggle="collapse"
            data-bs-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent"
            aria-expanded="false"
            aria-label="Toggle navigation"
          >
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
              <template v-if="!isAuthenticated">
                <li class="nav-item">
                  <nuxt-link
                    class="nav-link active"
                    aria-current="page"
                    to="/auth/login"
                    >Login</nuxt-link
                  >
                </li>
                <li class="nav-item">
                  <nuxt-link
                    class="nav-link active"
                    aria-current="page"
                    to="/auth/register"
                    >Register</nuxt-link
                  >
                </li>
              </template>
              <template v-else>
                <li class="nav-item" @click="logout">
                  <nuxt-link class="nav-link active" aria-current="page" to="#"
                    >Logout</nuxt-link
                  >
                </li>
                <li>
                  <nuxt-link
                    class="nav-link active"
                    aria-current="page"
                    to="/profile"
                  >
                    Profile
                  </nuxt-link>
                </li>
              </template>
            </ul>
          </div>
        </div>
      </nav>
    </div>
  </template>

  <script>
  export default {
    methods: {
      async logout() {
        await this.$auth.logout();  // this method will logout the user and make token to false on the local storage of the user browser
      }
    },
    computed: {
      isAuthenticated() {
        return this.$store.getters.isAuthenticated;  // it check if user isAuthenticated 
      }
    }
  };
  </script>

  <style></style>

and
/layouts/default.vue

<template>
    <div>
      <layouts-header />
      <div class="container">
        <br />
        <Nuxt />
      </div>
    </div>
  </template>

  <script>
  export default {};
  </script>

  <style>
  @import url("https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.0.2/css/bootstrap.min.css");
  </style>

and

/pages/index.vue

<template>
    <div>
      <home />
    </div>
  </template>

  <script>
  export default {};
  </script>

and

/pages/auth/login/index.vue

<template>
    <div>
      <auth-login />
    </div>
  </template>

  <script>
  export default {};
  </script>

  <style></style>

and

/pages/auth/register/index.vue

<template>
    <div>
      <auth-register />
    </div>
  </template>

  <script>
  export default {};
  </script>

  <style></style>

and

/pages/profile/index.vue

<template>
    <div>
      <user />
    </div>
  </template>

  <script>
  export default {
    middleware: "isAuthenticated"  // it will use `isAuthenticated` middleware
  };
  </script>

  <style></style>

and a middleware for checking if user authenticated:

export default function({ store, redirect }) {
    if (!store.state.auth.loggedIn) {
      return redirect("/auth/login");
    }
}

and at last vuex store :

export const getters = {
    isAuthenticated(state) {
      return state.auth.loggedIn; // auth object as default will be added in vuex state, when you initialize nuxt auth
    },
    getUserInfo(state) {
      return state.auth.user;
    }
  };

Express.js

That's was our nuxt codes and now its time to make our API:

first of all make a directory and enter this command on Terminal/Cmd and npm init -y
then npm install express body-parser bcryptjs jsonwebtoken mongoose
and then
npm install --save-dev nodemon it will add nodemon as a dev dependency

Cauation if "main" was like this "main":"index.js" on package.json file, change it to "main": "app.js"

now its time we create some files:

nodemon.js root direcotry that you just made

{
  "env": {
    "MONGO_USER": "mohammadali", // your cluster name
    "MONGO_PASS": "20212021", // your cluster password
    "MONGO_DB": "auth" // your db name
  }
}

on package.json will be on root directory add these lines of codes on scripts like as I did in below

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start-server": "node app.js",
    "dev": "nodemon app.js"
  },

app.js on root direcotry that you just made

const express = require("express");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");

// routes
const authRouter = require("./routes/authRouter");

const app = express();

app.use(bodyParser.json());

app.use((req, res, next) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader(
    "Access-Control-Allow-Methods",
    "OPTIONS, GET, POST, PUT, PATCH, DELETE"
  );
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  next();
});

app.use("/api/auth/", authRouter);

app.use((error, req, res, next) => {
  console.log(error);
  const status = error.statusCode || 500;
  const message = error.message;
  const data = error.data;
  res.status(status).json({ message: message, data: data });
});

// connect to db
const MONGOOSE_URI = `mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@cluster0.4r3gv.mongodb.net/${process.env.MONGO_DB}`;
mongoose
  .connect(MONGOOSE_URI)
  .then((result) => {
    app.listen(process.env.PORT || 8080);
  })
  .catch((err) => console.log(err));

and

/controllers/authController.js

const bcrypt = require("bcryptjs");
const userModel = require("../models/userModel");
const jwt = require("jsonwebtoken");

exports.postSignin = async (req, res, next) => {
  const { fullname, email, password } = req.body;
  try {
    const exsitUser = await userModel.findOne({ email: email });
    if (exsitUser) {
      const error = new Error(
        "Eamil already exist, please pick another email!"
      );
      res.status(409).json({
        error: "Eamil already exist, please pick another email! ",
      });
      error.statusCode = 409;
      throw error;
    }

    const hashedPassword = await bcrypt.hash(password, 12);
    const user = new userModel({
      fullname: fullname,
      email: email,
      password: hashedPassword,
    });
    const result = await user.save();
    res.status(200).json({
      message: "User created",
      user: { id: result._id, email: result.email },
    });
  } catch (err) {
    if (!err.statusCode) {
      err.statusCode = 500;
    }
    next(err);
  }
};

let loadedUser;
exports.postLogin = async (req, res, next) => {
  const { email, password } = req.body;

  try {
    const user = await userModel.findOne({ email: email });

    if (!user) {
      const error = new Error("user with this email not found!");
      error.statusCode = 401;
      throw error;
    }
    loadedUser = user;

    const comparePassword = bcrypt.compare(password, user.password);

    if (!comparePassword) {
      const error = new Error("password is not match!");
      error.statusCode = 401;
      throw error;
    }
    const token = jwt.sign({ email: loadedUser.email }, "expressnuxtsecret", {
      expiresIn: "20m", // it will expire token after 20 minutes and if the user then refresh the page will log out
    });
    res.status(200).json({ token: token });
  } catch (err) {
    if (!err.statusCode) {
      err.statusCode = 500;
    }
    next(err);
  }
};

exports.getUser = (req, res, next) => { // this function will send user data to the front-end as I said above authFetch on the user object in nuxt.config.js will send a request and it will execute
  res.status(200).json({
    user: {
      id: loadedUser._id,
      fullname: loadedUser.fullname,
      email: loadedUser.email,
    },
  });
};

and

/middleware/isAuth.js

const jwt = require("jsonwebtoken");

module.exports = (req, res, next) => {
  const authHeader = req.get("Authorization");
  if (!authHeader) {
    const error = new Error("Not authenticated.");
    error.statusCode = 401;
    throw error;
  }
  const token = authHeader.split(" ")[1];
  let decodedToken;
  try {
    decodedToken = jwt.verify(token, "expressnuxtsecret");
  } catch (err) {
    err.statusCode = 500;
    throw err;
  }
  if (!decodedToken) {
    const error = new Error("Not authenticated.");
    error.statusCode = 401;
    throw error;
  }
  req.userId = decodedToken.userId;
  next();
};

and

/models/userModel.js

const express = require("express");
const mongoose = require("mongoose");
const Schema = mongoose.Schema;

const UserSchema = new Schema(
  {
    fullname: {
      type: String,
      required: true,
    },
    email: {
      type: String,
      required: true,
    },
    password: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);

module.exports = mongoose.model("User", UserSchema);

and

/routes/routes.js

const express = require("express");
const router = express.Router();
const authController = require("../controllers/authController");

router.post("/signin", authController.postSignin);
router.post("/login", authController.postLogin);
router.get("/user", authController.getUser);

module.exports = router;

Note that this was a test project and typically you have to add some validation for client-side app inputs, and also server-side app

finally it's done, hope you've enjoyed it, and there is my Github link that you can find the source code

Some suggestions that might be useful for you:

For those people, that want to have both (refresh token and access token) for their application and also want to use Postgresql instead of MongoDB, I suggest this GitHub repository.

And for those people, that want to have both (refresh token and access token) for their application and also want to use Postgresql instead of MongoDB and also use Typescript for their Back-end application, I suggest this GitHub repository

17