Build Nodejs Passportjs MongoDB Authentication System

Mon Nov 08 2021

In this tutorial we will build an authentication system with nodejs and passportjs. We will use mongoDB as our database to store user information. Users will be able to create an account, login and logout. We will use ejs templating engine to display pages.

Create a folder in desired location and open terminal and run

npm init -y

It will create a package.json file. Now we will install express and ejs packages. In terminal run

npm install express ejs

We will also install nodemon and dotenv as dev dependencies to constantly watch for changes and store environment variables respectively.

npm install -D nodemon dotenv

Open the folder in code editor and at the root level create .env file and .gitignore file. In .gitignore file add .env and node_modules so that they dont become part of version control. In package.json file replace the content inside scripts with

"start": "node index.js",
"dev": "nodemon index.js"

At root level create a file index.js which will be our entry file. Let's create a basic express server. In index.js paste the following

const express = require("express")
const app = express()

app.set("view-engine", "ejs")
app.use(express.urlencoded({ extended: false }))

app.get("/", (req, res) => {
  res.render("index.ejs")
})

app.listen(5000, () => console.log("Server is running"))

At root level create a directory views and inside it create a file index.ejs and paste

<h1>Hello World</h1>

Now if you run the server by typing npm run dev and in browser visit http://localhost:5000 you will see Hello World being displayed.

Let's create routes for register and login. In views directory create register.ejs and login.ejs. In index.js lets add routes for register and login as well. After app.get('/'...) route paste the following

app.get("/register", (req, res) => {
  res.render("register.ejs")
})

app.get("/login", (req, res) => {
  res.render("login.ejs")
})

In register.ejs paste the following

<h1>Create an account</h1>
<form action="/register" method="POST">
  <div>
    <label htmlFor="name">Name</label>
    <input type="text" id="name" name="name" />
  </div>
  <div>
    <label htmlFor="email">Email</label>
    <input type="email" id="email" name="email" />
  </div>
  <div>
    <label htmlFor="password">Password</label>
    <input type="password" id="password" name="password" />
  </div>
  <button type="submit">Register</button>
  <p>Already a user? <a href="/login">Login</a></p>
</form>

So its a simple form with three fields name, email and password, a button to register user and an anchor tag to navigate to login page. Go to login.ejs and paste the following

<h1>Login into your account</h1>
<form action="/login" method="POST">
  <div>
    <label htmlFor="email">Email</label>
    <input type="email" id="email" name="email" />
  </div>
  <div>
    <label htmlFor="password">Password</label>
    <input type="password" id="password" name="password" />
  </div>
  <button type="submit">Login</button>
  <p>Not a user? <a href="/register">Register</a></p>
</form>

login.ejs is almost similar to register.ejs, form have only two fields email and password, a button to login and an anchor tag to navigate to register page. We have to define two routes in index.js now which will be post routes to /register and /login. After login get route, paste

app.post("/register", (req, res) => {})

app.post("/login", (req, res) => {})

Now we will create user model but before that we will connect to mongoDB and to work with mongoDB easily we will install mongoose. Open terminal and run

npm install mongoose

In index.js after we initialized express and stored it into app variable paste the following

const mongoose = require("mongoose");
require("dotenv").config();

mongoose
  .connect(process.env.MONGO_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() => console.log("mongoDB is connected"))
  .catch((e) => console.log(e.message));

Open .env file and add

MONGO_URI=mongodb://localhost:27017/node-passport

I am using local instance of mongodb installed on my system, you can use local or mongodb cloud whichever you like. I called the database node-passport.

At root level create a directory models and inside it create a file user.js and paste the following

const mongoose = require("mongoose");

const userSchema = new mongoose.Schema(
  {
    name: String,
    email: String,
    password: String,
  },
  { timestamps: true }
);

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

Here we have defined how our user schema should look like. User will have three fields name, email and password provided by the user, timestamps will automatically add createdAt and updatedAt fields and id will also be automatically generated by mongodb. Lets complete register route. Open terminal and run

npm install bcryptjs

We need bcryptjs to encrypt password because we dont want to store plain text password into database. In index.js at the top import user model and bcryptjs.

const User = require("./models/user");
const bcrypt = require("bcryptjs");

replace existing register route (post request) with

app.post("/register", async (req, res) => {
  const { name, email, password } = req.body;
  try {
    let user = await User.findOne({ email: req.body.email });
    if (user) {
      return res.status(400).json({ error: "User already exists" });
    }
    const hashedPassword = await bcrypt.hash(password, 10);
    user = new User({ name, email, password: hashedPassword });
    await user.save();
    res.redirect("/login");
  } catch (error) {
    console.log(error);
  }
});

Its an asynchronous function because we will work with database. First we destructured name, email and password from req.body after that we used mongoose findOne method to check if the email is already exists in database or not. If it exists we will return an error otherwise we will encrypt password and then we are creating a new instance of user providing name, email and encrypted password which in this case is stored in hashedPassword variable and then save user into mongodb. Finally, we will redirect user to login page.

Now we will work on login route and for that we will install a few dependencies. Open terminal and run

npm install passport passport-local express-session express-flash

There are different strategies for passport. We will use passport local strategy in this tutorial. express-session will help us to persist user across different pages and express-flash will be used to display error messages. At root level create a file passport.js and we will write functionality related to passport inside this file. In passport.js paste the following

const LocalStrategy = require("passport-local").Strategy;
const User = require("./models/user");
const bcrypt = require("bcryptjs");

function initialize(passport) {
  const authenticateUser = async (email, password, done) => {
    try {
      const user = await User.findOne({ email }).select("-password");
      if (user === null) {
        return done(null, false, { message: "Invalid Credentials" });
      }
      const isMatch = await bcrypt.compare(password, user.password);
      if (!isMatch) {
        return done(null, false, { message: "Invalid Credentials" });
      }
      return done(null, user);
    } catch (error) {
      done(error);
    }
  };

  passport.use(
    new LocalStrategy(
      {
        usernameField: "email",
      },
      authenticateUser
    )
  );

  passport.serializeUser((user, done) => done(null, user.id));
  passport.deserializeUser(async (id, done) => {
    const user = await User.findById({ _id: id });
    done(null, user);
  });
}

module.exports = initialize;

To work with passport, we need a strategy. We are using local strategy in this tutorial so at the top we have imported it. We have also imported user model and bcryptjs. We then defined a function initialize which takes passport as argument. After that we have defined an arrow function authenticateUser which takes email, password and done (which is a callback) as arguments.

First we will find user by email. If there is no user with that email we will call done callback. The first argument is error which is null in this case, second argument is data which is false as we didnt find any user with the given email and last argument is the message which we will show to the user.

If we found the user then the next step is to check the password whether the provided password is correct or not and for that we used bcrypt compare method which takes plain text password provided by user and password stored in database. If the password is not matched we will call done callback where error is null, data is false and displaying the error message.

If password matched we will call done again, error is null but this time data will be the user object except for the password field which will not be returned because when we used findOne method to find the user by email, we also added select("-password").

After that we are creating a new LocalStrategy to be used providing email as usernameField. By default usernameField value is username but we want to login the user with email so we have to specify it. The second argument to LocalStrategy is a callback function which in this case is authenticateUser we have already defined above.

Next step is to serialize and deserialize user.

passport.serializeUser((user, done) => done(null, user.id));

passport.deserializeUser(async (id, done) => {
  const user = await User.findById({ _id: id });
  done(null, user);
});

In the above code, only the user ID is serialized to the session, keeping the amount of data stored within the session small. When subsequent requests are received, this ID is used to find the user, which will be restored to req.user. Finally we exported initialize function so that we can use it in index.js file. In index.js file at the top

const passport = require("passport");
const initializePassport = require("./passport");
const session = require("express-session");
const flash = require("express-flash");

initializePassport(passport);

After app.use(express.urlencoded({ extended: false })); paste the following

app.use(flash());
app.use(
  session({
    secret: process.env.SECRET,
    resave: false, // if nothing is changed dont resave
    saveUninitialized: false, // dont save empty value in session
  })
);
app.use(passport.initialize());
app.use(passport.session());

Here we have setup flash messages, initialized passport and session. In .env file add

SECRET=any-secret-value

Replace the existing login post route with

app.post(
  "/login",
  passport.authenticate("local", {
    successRedirect: "/",
    failureRedirect: "/login",
    failureFlash: true,
  })
);

Here we are using passport.authenticate which takes strategy as first argument which in this case is local and a few options as second arguments. Open login.ejs and before Login button add

<p><% if(messages.error) { %> <%= messages.error %> <% } %></p>

If there is an error then it will be displayed. Open terminal and run

npm run dev

Visit http://localhost:5000/register to register a user. Upon successful registration you will be redirected to login page. Upon successful login, you will be redirected to home page.

We will protect home page so that only logged in user can visit this page and also we will provide a way to logout. Open terminal and tun

npm install method-override

We need a middleware to check for authentication in order to protect home route. In index.js file add

function requireLogin(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect("/login");
}

This middleware takes request, response and next as arguments. Here we are checking if user is logged in or not. If user is logged in then req.isAuthenticated() will be true and in that case we will call next() and pass the control to the function user is trying to execute otherwise we will redirect user to login page. Replace the home route with

app.get("/", requireLogin, (req, res) => {
  res.render("index.ejs");
});

Here as second argument we have passed requireLogin middleware which will run before the body of the function so user will only be able to visit home page if logged in. Now we will work on logout functionality. In index.js at the top paste

const override = require("method-override");

and before the home route add

app.use(override("_method"));

Finally paste the following

app.delete("/logout", (req, res) => {
  req.logOut();
  res.redirect("/login");
});

Since we cant use delete request in html that's why we are using method-override package. Now replace the index.ejs content with

<h1>Hello There</h1>
<form action="/logout?_method=DELETE" method="POST">
  <button type="submit">Logout</button>
</form>

Here in action we are overriding the POST method with DELETE. Run the server again. Login with credentials and then logout. Now try to visit http://localhost:5000 you will be redirected to login page because of requireLogin middleware added to the "/" route.

So that's how we can use authentication with passport in nodejs app.

Share

Privacy Policy
icon
icon
icon
icon

Developed By Farhan Farooq