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
Top comments (9)
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
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 :)
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
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....
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?
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.
Thank you this really helped me
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
Hi, Yep you can see it here
/pages/profile/index.vue
, it's already in the article.