DEV Community

MohammadAli Khaksar
MohammadAli Khaksar

Posted on • Edited on

Nuxt.js 2 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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" }
        }
      }
    }
  },
Enter fullscreen mode Exit fullscreen mode

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

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

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>
Enter fullscreen mode Exit fullscreen mode

/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>
Enter fullscreen mode Exit fullscreen mode

and

/components/Home/index.vue

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

Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

and

/pages/index.vue

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

  <script>
  export default {};
  </script>
Enter fullscreen mode Exit fullscreen mode

and

/pages/auth/login/index.vue

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

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

  <style></style>

Enter fullscreen mode Exit fullscreen mode

and

/pages/auth/register/index.vue

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

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

  <style></style>

Enter fullscreen mode Exit fullscreen mode

and

/pages/profile/index.vue

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

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

  <style></style>

Enter fullscreen mode Exit fullscreen mode

and a middleware for checking if user authenticated:

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

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;
    }
  };
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  },
Enter fullscreen mode Exit fullscreen mode

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));

Enter fullscreen mode Exit fullscreen mode

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,
    },
  });
};
Enter fullscreen mode Exit fullscreen mode

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();
};

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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

Top comments (9)

Collapse
 
slidenerd profile image
slidenerd • Edited

nice post! after login you should use router.replace not router.push because if you hit the back button after login you will go back to the login form if you are using push, also in the backend you dont need body-parser anymore, bcryptjs is nice if you dont want dependencies but bcrypt is the way to go for optimal performance, positive note: i have never seen anyone set the access control allow origin headers directly in an express middleware, most examples use the cors module, it didnt occur to me :) you keep learning something new everyday i guess. Another solid suggestion from my end would be to use a relational database for this since the structure of a user (email, password, name) remains the same for each record (it seems in your application that way) you could use the native pg module which requires you to manage connections manually or you could use pg-promise which is very similar to mongoose for postgres database

Collapse
 
mohammadali0120 profile image
MohammadAli Khaksar • Edited

hi, thanks for your good suggestions yeah these days I'm usually using Postgres in my apps, but I just wanted this dummy app would be as easy as it can because almost every Node developer has worked with MongoDB and Mongoose once
and yeah core package is a really good package, and actually, I should use it in my future apps.
and of course router.replace() also would be better way
Thanks for all your goodness, take care of them and also yourself :)

Collapse
 
slidenerd profile image
slidenerd

i am a fullstack developer and i had a lot of trouble in this section due to lack of documentation or examples anywhere on how to use nuxt auth on both the frontend and backend. I really appreciate your effort in putting this up. If it isnt too much to ask, can you please add a part 2 where you add Facebook or Twitter login using nuxt-auth in addition to email password and also use postgres. Super appreciate in advance

Thread Thread
 
mohammadali0120 profile image
MohammadAli Khaksar • Edited

hi, as you say this module does not have many examples or documentation and I actually have no plan to write about Facebook or Google and some stuff like that for authentication
sorry about this, really I don't have time for getting stuck more on this matter
but I introduce to you some examples that might help you.

codesandbox.io/s/kdwxq?file=/nuxt....
codesandbox.io/s/45icg?file=/nuxt....

Collapse
 
jaymefrantz profile image
Jayme Frantz

You really saved my butt, this was just what I needed for my project!! One question, where and when is the isAuth.js middleware called?

Collapse
 
mohammadali0120 profile image
MohammadAli Khaksar

Hi, I'm glad that helped you, I think I forgot to use it in my router in this project
you can see it here:
github.com/mohammadali0120/nuxt-ex...
11th line.

Collapse
 
jgeorge97 profile image
George Martin Jose

Thank you this really helped me

Collapse
 
jannikbuscha profile image
Jannik Buscha

hey, isAuthenticated.js called anywhere in the frontend at all? i'm just wondering what it's for, because i can't find a call

Collapse
 
mohammadali0120 profile image
MohammadAli Khaksar • Edited

Hi, Yep you can see it here /pages/profile/index.vue, it's already in the article.