User Authentication With Mongoose, Bcrypt, Jwt And Node Js

A tutorial on how to authenticate users using node js, express, mongoose, json web token, bcryptjs and your computer.

Β·

20 min read

User Authentication With Mongoose, Bcrypt, Jwt And Node Js

Hello, this tutorial is going to show you one of the many ways to authenticate users, and at the end of this walk through, I will be showing us how to use our authentication code as a template repository on github.

Setting up the project

Checking if node is installed on your computer

Firstly lets check if we have a supported version of node js installed on our computer by typing node -v in your command line

Screenshot (28).png I have version 12 installed on computer

NOTE: my terminal is red because I use vs code

Screenshot (31).png

if you have any of the supported versions on your computer you are good to go.

Initializing the project

Create a new folder on your machine with the name nodeauthserver or any name of your choosing. Navigate to that directory from your command line.

NOTE: if you are not sure how to do that just google this keywords ( how to navigate to a directory in terminal ) and you should find a solution for your computer.

Now lets set up npm by running this in the command line npm init -y to create a package.json file with the default configurations.

Next, lets install the necessary dependencies for our application by running this in our command line

npm install bcryptjs express mongoose jsonwebtoken cors cookie-parser

before we move on, let's configure our start script by checking our package.json file's scripts and adding this line "start" : "node index.js" to the scripts and deleting the test script as we will not be writing tests in this tutorial. So your scripts should now look like this:

"scripts" : { "start" : "node index.js" }

Setting up our server

In our start script we made reference to an index.js file as our entry point which was not in existence. Let's change that by creating an index.js file in the root directory. In our index.js file input the following:

// import express and mongoose
const express = require("express");
const mongoose = require("mongoose");

// creates an instance of the express app and save in the app constant
const app = express();

// sets the port to the environment variable PORT but if the variable is not set we use 5000
const port = process.env.PORT || 5000;

// middleware that parses json 
app.use(express.json());

// a route to / that returns an object 
app.get("/", (req, res) => {
  return res.send({ message: "an auth app " });
});


// listen for requests made to our server on the specified port
app.listen(port, () => {
  console.log(`app listening on port ${port}`);
});

now, lets test our server by running our start script in the command line npm start you should see app listening on port in the command line if everything works out as it should.

NOTE: you would have to kill the process ( by pressing ctrl c on a windows computer ) and restart it every time a change is made to your server. To prevent this, kill the process and install nodemon on your computer, by running this in your command line npm install -g nodemon. If it is installed you can now start your server by typing nodemon index.js in the command line.

Setting up and connecting to mongodb

For this project we will be working with mongodb atlas.

NOTE: mongodb atlas is a cloud hosted mongo db database i.e, your data is stored on another computer, in another location, but can be accessed by you anytime you require it.

Go to mongodb's website , sign in if you already have an account. If you don't click on the start free button to sign up

If you are entirely new to mongodb atlas, I would advise you to go through the tutorial on the documentation here and for the code example look at the Node.js driver examples.

Make sure to copy the password you create for the database user into any file of choice, it would be used to connect to the database we create.

creating a new database for our application

To create a new database we need to first sign-in, after you sign-in follow the following steps to create a new database : p.s if you know how to create a new database do go ahead and do that, you can skip this step

  1. click on collections
  2. click on the create new database button
  3. enter the name of the database
  4. enter the name of the collection which in this case would be the user
  5. click on the create database button
  6. click on overview on the menu
  7. click on the connect button
  8. click on connect your application
  9. copy the connection string into any file of choice, as we will use it later to connect to our database

connecting to our database using mongoose

Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It enables us create schemas, define relationships between documents and it serves as an intermidiary between Node js and Mongodb. We will be using it instead of mongoDB's native driver. Before we continue, In your connection uri make sure to replace the placeholders for the database name and the database user password with the correct data.

In order to connect we will be needing to add the following lines to our already existing code in index.js

 mongoose.connect(
   "connection uri",
   {
     useNewUrlParser: true,
     useFindAndModify: true,
     useCreateIndex: true,
     useUnifiedTopology: true,
   },
  (err, something) => {
   if (err) {
       console.log(err);
     }
}
);

the mongoose.conect( ) is a method that allows us connect to mongoDB using the connection uri which it accepts as one of it's parameters along with an options object which is important for using mongoose. Now let's setup an event listener for when our connection is open

mongoose.connection.on("open", (err) => {
  if (err) {
     console.log(err);
   } else {
        console.log("connected to database");
  }
 });

When we are successfully connected to the our cloud database, connected to database is logged in our console.

Creating authentication routes

In our root folder create a new folder called routes , it would house all of our routes. Now we would create a new file auth.js in our newly created routes and paste in the following code

const express = require("express");


//creating a new router object from the express global object via the Router method
const router = express.Router();

// route to sign up  that is, it would handle requests to this route that end in /signin
router.post("/signup", (req, res) => {
  const { email, password } = req.body;
  if (!email.trim() || !name.trim() || !password.trim()) {
    return res.send("fill in correct details");
  }
  return res.send("signed up");
});

//route to sign in that is, it would handle requests to this route that end in /signin
router.post("/signin", (req, res) => {
  const { email, password } = req.body;
if (!email.trim() || !password.trim()) {
    return res.send("fill in correct details");
  }
  return res.send("signed in");
});

module.exports = router;

this code will serve as boiler plate for the signin and signup routes.

What we did there was create routes through HTTP methods and export the `router object to enable us use it in another file.

In our index.js file let's import and use our auth.js file and use it.

We do this by inputting the following code in our index.js file

//importing our auth route as auth
const auth = require("./routes/auth");

and

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

now our index.js should be looking like this

// import express and mongoose
const express = require("express");
const mongoose = require("mongoose");

//importing our auth route as auth
const auth = require("./routes/auth");

// creates an instance of the express app and save in the app constant
const app = express();

//connect to mongoose

mongoose.connect(
  "mongodb+srv://databaseUser:yourpassword@cluster0.gltnq.mongodb.net/databasename?retryWrites=true&w=majority",
  {
    useNewUrlParser: true,
    useFindAndModify: true,
    useCreateIndex: true,
    useUnifiedTopology: true,
  },
  (err, something) => {
    if (err) {
      console.log(err);
    }
  }
);

//listen for the connection open event
mongoose.connection.on("open", (err) => {
  if (err) {
    console.log(err);
  } else {
    console.log("connected to database");
  }
});

// sets the port to the environment variable PORT but if the variable is not set we use 5000
const port = process.env.PORT || 5000;

// middleware that parses json
app.use(express.json());

// uses the auth route for any request made to the /api/user endpoint
app.use("/api/user", auth);

// a route to / that returns an oject
app.get("/", (req, res) => {
  return res.send({ message: "an auth app " });
});

// listen for requests made to our server on the specified port
app.listen(port, () => {
  console.log(`app listening on port ${port}`);
});

before we go on let's ensure that our routes are working as we want them to work. How do we do that without a frontend ? well we use Postman an api exploration and testing tool.

Testing our routes with the Postman app

In order to test our apis we need either a frontend or a good api testing tool. My preferred tool for this task is Postman. If you don't have Postman installed install it or use the web platform to follow along.

Creating a new request

  1. click on the + New button on the top right corner
  2. click on Request
  3. enter a request name
  4. scroll down and create a folder / collection
  5. click on save to request

Adding request detail

  1. click on the drop down menu with GET as the default option
  2. select POST as that was the verb we used in our route that is, we are making a post request
  3. copy this url localhost:/api/user/signup to enable us route to our signup route

Sending the body

  1. click on Body to open the Body tab

  2. click on raw

  3. click on the drop down menu with Text as the default option

  4. select JSON as that is the data format we will be sending

next lets test our api

Before you send any request to your server please make sure your server is still running, if it is lets send a request to our signup route by inputting the data we want to send to our server into the textbox in the JSON format, which for now is an email, a username and a password. If it is successful we should get a response from the server saying "signed up"

Saving our user to the database

We can't save our password as it is, we will be needing to encrypt our passwords before it is stored in our database for security reasons. To do this we will be using a Mongoose middleware or as it is popularly called a pre hook. Create a new folder called models in the root folder and in this folder create a new file with the name userModel.js which would house our UserSchema. In the file paste in the following code to create the UserSchema

const mongoose = require("mongoose");
const UserSchema = mongoose.Schema({
  name: { type: String, maxlength: 50 },
  email: {
    type: String,
    trim: true,
    unique: 1,
  },
  password: {
    type: String,
    minlength: 5,
  },
token:{
type: String,
},
});

const User = mongoose.model("user", UserSchema);

module.exports = User;

In the block of code above we do the following:

1). we require Mongoose

2). we create an instance of the mongoose.Schema method passing it an object containing our data types and save it in the UserSchema constant

3). we create an instance of the mongoose.model method passing it a string which will be the name of the model and the schema we created earlier

4). we export the model we created

Before we save our file lets make sure we secure our password by hashing it using bcryptjs a library that helps us hash our password. We will do this with the pre save hook, which is actually a mongoose middleware that is run before our document is saved. we do that with the following lines of code

NOTE: make sure to import bcryptjs at the top of file before copying this code. Like this const bcrypt = require("bcryptjs");

UserSchema.pre("save", function (next) {
  // do stuff
  let user = this;
  if (user.isModified("password")) {
    bcrypt.genSalt(10, (err, salt) => {
      if (err) {
        return next(err);
      }

      bcrypt.hash(user.password, salt, (err, hashedPassword) => {
        if (err) {
          return next(err);
        }
        user.password = hashedPassword;

        next();
      });
    });
  } else {
    next();
  }
});

NOTE: In the code above this refers to the document that is the new User we are attempting to save.

In the code above we do the folowing:

1). save this as user

2). check if the user we saved earlier has the password modified

3). if it is modified we generate a salt using the bcrypt.genSalt which takes two arguments which are:

a). the iteration count

b). a callback function which uses the salt to hash the password

4). handle any errors that may occur

5). hash using bcrypt.hash method which takes 3 arguments, which are: a). the string we wish to hash b). the salt that was generated earlier c). a callback function

6). save the hashed password to our database

Now our complete userModel.js should look like this

const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");

const UserSchema = mongoose.Schema({
  name: { type: String, maxlength: 50 },
  email: {
    type: String,
    trim: true,
    unique: 1,
  },
  password: {
    type: String,
    minlength: 5,
  },
token:{
type: String,
},
});

UserSchema.pre("save", function (next) {
  // do stuff
  let user = this;
  if (user.isModified("password")) {
    bcrypt.genSalt(10, (err, salt) => {
      if (err) {
        return next(err);
      }

      bcrypt.hash(user.password, salt, (err, hashedPassword) => {
        if (err) {
          return next(err);
        }
        user.password = hashedPassword;

        next();
      });
    });
  } else {
    next();
  }
});

const User = mongoose.model("user", UserSchema);

module.exports = User;

Now that we have configured our UserSchema to hash the password before we save it. Let's go to out auth.js file and make some changes, to allow us to save our user to the database and not just display signed in .

In our auth.js file let's make the some adjustments to our code in our signup route and change it to this :

router.post("/signup", (req, res) => {
  let { email, name, password } = req.body;

  // removing white space from our data using the trim method
  //to enable proper validation

  if (!email.trim() || !name.trim() || !password.trim()) {
    return res.send("fill in correct details");
  }

  const user = new User({ email, name, password });
  user
    .save()
    .then((user) => return res.json({
              message: `${user.name} signed up`
            });)
    .catch((err) => {
      console.log(err.message);
      return res.send(err.message);
    });
});

What we do in the code above is to :

1). ensure the user enters all the required data

2). create a new instance of our User with our data gotten from the user

3). we then save the user to our data base and send the details back to our user

Our auth.js all together should look like this :

const express = require("express");


//creating a new router object from the express global object via the Router method
const router = express.Router();

// route to sign up  that is, it would handle requests to this route that end in /signin
router.post("/signup", (req, res) => {
  let { email, name, password } = req.body;

  // removing white space from our data using the trim method
  //to enable proper validation

  if (!email.trim() && !name.trim() && !password.trim()) {
    return res.send("fill in correct details");
  }

  const user = new User({ email, name, password });
  user
    .save()
    .then((user) => res.send(user))
    .catch((err) => {
      console.log(err.message);
      return res.send(err.message);
    });
});

//route to sign in that is, it would handle requests to this route that end in /signin
router.post("/signin", (req, res) => {
  const { email, password } = req.body;
if (!email.trim() || !password.trim()) {
    return res.send("fill in correct details");
  }
  return res.send("signed in");
});

module.exports = router;

We are now done with our sign up route πŸŽ‰πŸŽˆ hooray 🎈🎊

Let's build the signin route

Go back to our auth.js file and in the signin route and modify with the following code :

router.post("/signin", (req, res) => {
  const { email, password } = req.body;
  if (!email.trim() || !password.trim()) {
    return res.send("fill in correct details");
  }

  // Load user details from your password DB.

  User.findOne({ email })
    .then((user) => {
      user.validatePassword(
        user.password,
        password,
        function (error, isAMatch) {
          if (error) {
            return res.json(err);
          }
          user.generateToken(user, (err, user) => {
            if (err) {
              return res.json(err);
            }
// storing our user token in a cookie
            res.cookie("urcookie", user.token, {
              maxAge: 90000,
              httpOnly: true,
            });

            return res.json({
              message: `${user.name} signed in`
            });
          });
        }
      );
    })
    .catch((err) => console.log(err));
});

What we do in the code above is to :

1). we check if the user sent in all the correct details

2). we check if the email is in our database and if it is we use the validatePassword method on our user document to check if the password the user sent in is correct

3). if our password is correct then we use the generateToken method on our user document to generate a json token with jwt

4). we store the token we created in a cookie using the name urcookie

5). we return a message to the user saying they have signed in successfully

creating the validatePassword and generateToken methods

The following code would be added to userModel.js :

UserSchema.methods.validatePassword = function (hashedPassword, password, cb) {
  bcrypt.compare(password, hashedPassword, function (err, res) {
    if (err) {
      cb(err);
    }
    cb(null, res);
  });
};

UserSchema.methods.generateToken = function (user, cb) {
  const token = jwt.sign({ id: user._id }, "jwtsecret");

  user.token =token;

  user.save().then((user)=>{cb(null,user)}).catch((err)=>cb(err))

};

What we did in the code above was to :

1). we create the validatePassword method on the document which is a function that uses bcrypt.compare method, to check if the inputed password is the same as the password hashed and saved in the database. This method takes 3 parameters (i) the inputed password (ii) the hashedPassword (iii) a callback function that sends back the result of the password check

2). we create the generateToken method on the document which is a function that uses jwt.sign method, to generate a token and it takes two arguments (i) a payload that is, what would be sent to the user (ii) a secret

3). we save the token we created to our database

NOTE: these methods we created would only be available on any document (user) we save to our database and not the Schema method used in creating them

Now our userModel.js should be looking like this :

const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");

const UserSchema = mongoose.Schema({
  name: { type: String, maxlength: 50 },
  email: {
    type: String,
    trim: true,
    unique: 1,
  },
  password: {
    type: String,
    minlength: 5,
  },
  token: {
    type: String,
  },
});

UserSchema.pre("save", function (next) {
  // do stuff
  let user = this;
  if (user.isModified("password")) {
    bcrypt.genSalt(10, (err, salt) => {
      if (err) {
        return next(err);
      }

      bcrypt.hash(user.password, salt, (err, hashedPassword) => {
        if (err) {
          return next(err);
        }
        user.password = hashedPassword;

        next();
      });
    });
  } else {
    next();
  }
});

UserSchema.methods.validatePassword = function (hashedPassword, password, cb) {
  bcrypt.compare(password, hashedPassword, function (err, res) {
    if (err) {
      cb(err);
    }
    cb(null, res);
  });
};

UserSchema.methods.generateToken = function (user, cb) {
  const token = jwt.sign({ id: user._id }, "jwtsecret");

  user.token =token;

  user.save().then((user)=>{cb(null,user)}).catch((err)=>cb(err))

};

const User = mongoose.model("user", UserSchema);

module.exports = User;

The entire point of generating a token is that select resources can be protected from unauthorized users. In the next section we will be creating a new route that would be using a middleware to validate users and returning user details to authorized users.

Creating the user detail route

Before we create our user detail route let's create a middleware which verifies our user.

creating middleware

Let's go back to our root folder and create a folder called middleware and in it create a file validateUser.js which would hold the code for user validation. Input the following code into validateUser.js :

const  User  = require("../models/userModel");

let validateUser = (req, res, next) => {
  let urcookie = "urcookie";
  let token = req.cookies.urcookie;

  User.validateToken(token, (err, user) => {
    if (err) {
      throw err;
    }
    if (!user) {
      return res.json({
        isAuth: false,
        error: true,
      });
    }

    req.token = token;
    req.user = user;
    next()
  });
};


module.exports = validateUser;

What we do in the code above is to :

1). import our User model

2). create a function validateUser that takes three parameters (i) the request object req (ii) the response object res (iii) the next middleware function next

3). we fetch our token and store it in a variable token

4). we call the validateToken which will be stored on our UserSchema. It takes two arguments the token and a callback function

5). we save our token and user in our request object

6). we export our validateUser function

Now let's create our validateToken method

creating validateToken

add the following code to your userModel.js

UserSchema.statics.validateToken = function (token, cb) {
  let user = this;
  jwt.verify(token, "jwtsecret", function (err, decoded) {
    if (err) {
      cb(err);
    }
    user
      .findById(decoded.id)
      .then((UserInfo) => {
        cb(null, UserInfo);
      })
      .catch((err) => cb(err));
  });
};

what we do in the code above :

1). we create our validateToken method on our UserSchema as a function that takes two parameters (i) a token (ii) a callbak function

2). we use the jwt.verify method to verify the token sent in by the user

3). if the token is valid we send the user details to the user

Our userModel.js should now look like this

const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");

const UserSchema = mongoose.Schema({
  name: { type: String, maxlength: 50 },
  email: {
    type: String,
    trim: true,
    unique: 1,
  },
  password: {
    type: String,
    minlength: 5,
  },
  token: {
    type: String,
  },
});

UserSchema.pre("save", function (next) {
  // do stuff
  let user = this;
  if (user.isModified("password")) {
    bcrypt.genSalt(10, (err, salt) => {
      if (err) {
        return next(err);
      }

      bcrypt.hash(user.password, salt, (err, hashedPassword) => {
        if (err) {
          return next(err);
        }
        user.password = hashedPassword;

        next();
      });
    });
  } else {
    next();
  }
});

UserSchema.methods.validatePassword = function (hashedPassword, password, cb) {
  bcrypt.compare(password, hashedPassword, function (err, res) {
    if (err) {
      cb(err);
    }
    cb(null, res);
  });
};

UserSchema.methods.generateToken = function (user, cb) {
  const token = jwt.sign({ id: user._id }, "jwtsecret");

  user.token = token;

  user
    .save()
    .then((user) => {
      cb(null, user);
    })
    .catch((err) => cb(err));
};

UserSchema.statics.validateToken = function (token, cb) {
  let user = this;
  jwt.verify(token, "jwtsecret", function (err, decoded) {
    if (err) {
      cb(err);
    }
    user
      .findById(decoded.id)
      .then((UserInfo) => {
        cb(null, UserInfo);
      })
      .catch((err) => cb(err));
  });
};

const User = mongoose.model("user", UserSchema);

module.exports = User;

add the following code to our index.js :

app.get("/userdetails", validateUser,(req,res)=>{
  let { user } = req;
  return res.send(user);
})

In the code above we simply destructure the user object from our request and send it back to our user.

Our index.js should now look like this

const express = require("express");
const mongoose = require("mongoose");
const cookieParser = require("cookie-parser");
const cors = require("cors");

//importing our auth route as auth
const auth = require("./routes/auth");
const validateUser = require("./middleware/validateUser");
// creates an instance of the express app and save in the app constant
const app = express();

//connect to mongoose

mongoose.connect(
  "mongodb+srv://botanicalCoder:mongopassword@cluster0.gltnq.mongodb.net/boilerplateyoutube?retryWrites=true&w=majority",
  {
    useNewUrlParser: true,
    useFindAndModify: true,
    useCreateIndex: true,
    useUnifiedTopology: true,
  },
  (err, something) => {
    if (err) {
      console.log(err);
    }
  }
);

//listen for the connection open event
mongoose.connection.on("open", (err) => {
  if (err) {
    console.log(err);
  } else {
    console.log("connected to database");
  }
});

// sets the port to the environment variable PORT but if the variable is not set we use 5000
const port = process.env.PORT || 5000;

// middlewares 
app.use(express.json());
app.use(cookieParser());
app.use(cors());

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

// a route to / that returns an oject
app.get("/", (req, res) => {
  return res.send({ message: "welcome to the auth app " });
});

app.get("/userdetails", validateUser, (req, res) => {
  let { user } = req;
  return res.send(user);
});

// listen for requests made to our server on the specified port
app.listen(port, () => {
  console.log(`app listening on port ${port}`);
});

Since we have a sign in route it is only right we create a sign out route to log off our users.

creating a sign out route

At the top of our auth.js route import our validateUser.js file like this const validateUser = require("../middleware/validateUser"); to enable use the middleware input the following code

router.get("/signout", validateUser, (req, res) => {
  res.cookie("urcookie",{maxAge: 0});
  User.findByIdAndUpdate(req.user._id, { token: "" })
    .then((user) => {
      return res.send("you've signed out");
    })
    .catch((err) => res.send(err));
});

In the code above we :

1). we delete our cookie by setting it's maximum age to 0 that is, it expires

2). we find our user document and delete the token we stored earlier

3). we send a message to the user saying "you've signed out"

Our `auth.js file should look like this now

const express = require("express");
const validateUser = require("../middleware/validateUser");
const User = require("../models/userModel");

const router = express.Router();

router.post("/signup", (req, res) => {
  let { email, name, password } = req.body;

  if (!email.trim() || !name.trim() || !password.trim()) {
    return res.send("fill in correct details");
  }

  const user = new User({ email, name, password });
  user
    .save()
    .then((user) =>
      res.json({
        message: `${user.name} signed up`,
      })
    )
    .catch((err) => {
      console.log(err.message);
      return res.send(err.message);
    });
});

router.post("/signin", (req, res) => {
  const { email, password } = req.body;
  if (!email.trim() || !password.trim()) {
    return res.send("fill in correct details");
  }


  User.findOne({ email })
    .then((user) => {
      user.validatePassword(
        user.password,
        password,
        function (error, isAMatch) {
          if (error) {
            return res.json(err);
          }
          user.generateToken(user, (err, user) => {
            if (err) {
              return res.json(err);
            }


            res.cookie("urcookie", user.token, {
              maxAge: 90000,
              httpOnly: true,
            });

            return res.json({
              message: `${user.name} signed in`,
            });
          });
        }
      );
    })
    .catch((err) => console.log(err));
});

router.get("/signout", validateUser, (req, res) => {
  res.cookie("urcookie",{maxAge: 0});
  User.findByIdAndUpdate(req.user._id, { token: "" })
    .then((user) => {
      return res.send("you've signed out");
    })
    .catch((err) => res.send(err));
});

module.exports = router;

HoorayπŸŽŠπŸŽ‰ hoorayπŸŽŠπŸŽ‰ We have successfully created our user authentication app. I hope you were able to learn a thing or two. The complete code is found in this repo

Bonus how to use a git hub repo as your template

For this we will be using the web interface of git hub

follow the steps below to use a template repo today

1). search for the template repo

2). click on use this template

and you're good to go