User sign-in and sign-up

User sign-in and sign-up

#3 Article on Blog API: Tutorial Series

Introduction.

Hello there. It's good to have you here again. This is the third article in this series. In this article, I will be discussing how I implemented the user sign-in and sign-up functionalities. This is a bit of work compared to what we have been doing. First, I will start with how to implement the user model using mongoose schema, passport-jwt for the strategies, jsonwebtoken (jwt) for authentication token and a bunch of other things. So let's get started.

Creating user model.

/models/userModel.js

// require packages
const mongoose = require("mongoose");
const schema = mongoose.Schema;
const bcrypt = require("bcrypt");
// define user schema
const userSchema = new schema(
  {
    email: {
      type: String,
      required: [true, "Email is required"],
      unique: true,
    },
    password: {
      type: String,
      required: [true, "Password is required"],
    },

    first_name: {
      type: String,
      required: [true, "first name is required"],
    },
    last_name: {
      type: String,
      required: [true, "last name is required"],
    },
    article: [
      {
        type: mongoose.Schema.Types.ObjectId,
        ref: "articles",
      },
    ],
  },
  { versionKey: false }
);

The user model provides an interface so that CRUD operation can be done on the user document(data). It involves referencing mongoose, defining the schema and exporting the file. Schema simply defines the structure of the document.

Back to our code, go ahead and create a folder called "models". Inside "models" folder, create a file and name it "userModel.js". The code above goes into the file. Now let's look back to the court.

First, we start by requiring the mongoose library. You will notice that there is another package called "bcrypt", more on this soon. Then I went ahead and defined the schema. Next, we define the properties in the schema. These properties will be in the user document and each property is associated with a schemaType.

You might also notice the "article" property, this will hold all the IDs of every article a particular user might have published. I decided to remove versionKey since I don't need it. You can read more on mongoose scheme here.

Hashing of a user password.

User passwords and other sensitive information are some things that need to be encrypted to avoid cyber attacks. This is why hashing these details is very important.

Hashing turns your password (or any other piece of data) into a short string of letters and/or numbers using an encryption algorithm. To do this, we install the bcrypt which is a package that helps hash our details.

/models/userModel.js

// hash the password using bcrypt
userSchema.pre("save", async function (next) {
  const user = this;
if (!user.isModified("password")) return next();
  const hash = await bcrypt.hash(user.password, 10);
  user.password = hash;
  next();
});

userSchema.methods.isValidPassword = async function (password) {
  const user = this;
  const compare = await bcrypt.compare(password, user.password);
  return compare;
};
module.exports = mongoose.model("user", userSchema);

Still in the /models/userModel.js file, we define a function called userSchema.pre(). This is a function that will be called before a certain function is called, just like middleware. The code inside the userSchema.pre() function is called "pre-hook". The logic here is that before the user information is added to the database, the function is going to be called.

The function takes the user object which contains the user details. Then we take just the user password which bcrypt will hash for us and return ten(10) characters. It then replaces the original password with the hash password. If this is successful, then the program will proceed to the next function. This is what calling next() does.

Methods can also be added to the user schema. In this case, we are adding isValidPassword method so that by calling the model, we can add the method to it. What is method does is take the user's original password and compare it to the hash password. This is useful when the user wants to sign in.

User Authentication and Authorization.

In this section, I will be writing about how the user can be authenticated so that they will be able to access some protected routes. To be authorized, the user must pass the token when making a request. To get this token, we need to implement the authentication middleware. Let's go ahead and implement this.

Create a folder and call it "authentication". Inside this folder, create a file and name it "auth.js". The file location should look like this "/authentication/auth". Go ahead and paste the code below into the file. I will explain afterwards.

/authentication/auth.js

const passport = require("passport");
const JWTStrategy = require("passport-jwt").Strategy;
const ExtractJWT = require("passport-jwt").ExtractJwt;
const localStrategy = require("passport-local").Strategy;
require("dotenv").config();
const userModel = require("../models/userModel");

passport.use(
  new JWTStrategy(
    {
      secretOrKey: process.env.JWT_SECRET,
      jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
    },
    async (token, done) => {
      try {
        return done(null, token.user);
      } catch (err) {
        done(err);
      }
    }
  )
);

In the code above, I imported a bunch of modules. First is the passport module which authenticates requests which go through sets of plugins. Next is passport-jwt which is a passport strategy for authenticating with jsonwebtoken. With passport-jwt, we set a strategy and also extract the jsonwebtoken. Next is passport-local which authenticates users using a username and password stored locally.

The first middleware checks if the request sent includes the token name "secret_token". There are ways of passing the token but in this case, I passed it as a bearer token.

If the token is present, then it is decrypted by a secret pin which is saved in the .env file. So head to the .env file and save your secret into an environment variable I call "JWT_SECRET". The secret pin could be anything. When it decrypts the token and all the user details which were passed when creating the token are present, it means that the user is authenticated. This is very important for authorizing users through some protected routes.

Next is the signup middleware

/authentication/auth.js

passport.use(
  "signup",
  new localStrategy(
    {
      usernameField: "email",
      passwordField: "password",
      passReqToCallback: true,
    },
    async (req, email, password, done) => {
      try {
        const first_name = req.body.first_name;
        const last_name = req.body.last_name;
        const user = await userModel.create({
          email,
          first_name,
          last_name,
          password,
        });

        return done(null, user);
      } catch (err) {
        done(err);
      }
    }
  )
);

The signup middleware is using the localStrategy for the username and password. The username is the email here. I also set the passReqToCallback to true so that I can add more fields to the local authentication strategy. Next is to create the user details in the database. But before the user details are created, the password is been hashed in the userSchema.pre() function we defined in the user schema.

Next is the signin middleware

/authentication/auth.js

passport.use(
  "login",
  new localStrategy(
    {
      usernameField: "email",
      passwordField: "password",
    },
    async (email, password, done) => {
      try {
        const user = await userModel.findOne({ email });
        if (!user) {
          return done(null, false, { message: "User not found" });
        }
        const validPassword = await user.isValidPassword(password);
        if (!validPassword) {
          return done(null, false, { message: "Password is incorrect" });
        }
        return done(null, user, { message: "Login successfull" });
      } catch (err) {
        return done(err);
      }
    }
  )
);

The signup middleware is also using the localStrategy for the username and password. The username is the email here. Then the code looks for a user with that email. If it doesn't exist, it returns the error message. Next, the isValidPassword() method we created in the userModel.js file compares the password with the password the user sent when signing in.

If it doesn't match, the returns the error message. If it matches, then the returns a success message. If the entire code in the try block isn't successful, then the code in the catch block returns the error.

This is all for the middleware. Next is to import the file into the app.js file like this:

/app.js

require("./authentication/auth");

app.use(function (err, req, res, next) {
  res.status(err.status || 500);
  res.json({ error: err });
});

Make sure the imported file is at the top of your code. Next, we add the error handler to handle the error. This middleware returns all errors we might encounter.

User Authentication Routes

Now that we have implemented the authentication middleware, the next thing is to implement the routes for sign-in and sign-up. Let's start with sign-up first.

I implemented this with the MVC architecture. So go ahead and create two(2) folders and call them "controllers" and "routes".

  1. Signup Route

    In the "controllers" folder, create a file and name it "authController.js". Then copy the code below and paste it into the file.

    /controllers/authController.js

const jwt = require("jsonwebtoken");
const passport = require("passport");
require("dotenv").config();

exports.signUp = async (req, res, next) => {
  try {
    res.status(201).send({
      user: req.user,
    });
  } catch (err) {
    return next(err);
  }
};

First, we start by importing the necessary packages we might need. But don't forget to install them. We are familiar with passport and dotenv. The unfamiliar package is "jwt" which is used for securely transmitting information between parties.

The signUp function returns the user details when a user routes to /api/signup route. More on routes soon. If there is an error, it is been handled in the catch block

Next is to implement the route for user signup. Open the routes folder you created before and then create a new file called "authRoute.js". Go ahead and paste the code below into the file

/routes/authRoute.js

const express = require("express");
const userRouter = express.Router();
const authController = require("../controllers/authController");
userRouter.post(
  "/signup",
  passport.authenticate("signup", { session: false }),
  authController.signUp
);

First, we import the express module, and then create a new router object to handle requests. Next, we import the "authController.js" file we implemented earlier before.

Next, we define the routing method which is POST in this case. We route to /api/signup and pass in the signup middleware we implemented earlier before. Don't bother about the path for now. I will write about that soon.

  1. Signin Route.

    Now go back to the authController.js file and copy and paste the code below which I will explain afterwards.

    /controllers/authController.js

exports.login = async (req, res, next) => {
  passport.authenticate("login", async (err, user, info) => {
    try {
      if (err) {
        return next(err);
      }
      if (!user) {
        return next(info.message);
      }
      if (!user.password) {
        return next(info.message);
      }

      req.login(user, { session: false }, async (error) => {
        if (error) return next(error);
        const body = {
          _id: user._id,
          email: user.email,
        };
        const token = jwt.sign({ user: body }, process.env.JWT_SECRET, {
          expiresIn: "1hr",
        });
        return res.status(200).json({ token });
      });

      next();
    } catch (err) {
      return next(err);
    }
  })(req, res, next);
};

Here, I create a function called "login". Notice how I immediately exported the function. Then I passed in the login middleware we created earlier before. This is followed by a function middleware that returns error information if the user isn't found in the database or the user password is not found too.

If the user details were found in the database, then the user should be logged in. The session is set to false because we want to return a token from the user object. Next is to encrypt the body object with jwt which contains the user email and _id. jwt then signs the body object by using the secret key we stored in the .env file. The token is then returned which expires after 1 hour. And if there is an error, the error is then returned also.

Next is to implement the user sign-in route. Open the "authRoute.js" file you create before. Go ahead and paste the code below into the file.

/routes/authRoute.js

userRouter.post("/login", authController.login);
module.exports = userRouter;

Next, we define the routing method which is POST in this case which routes to /api/login. More on this url path very soon. And there is no need to pass in the passport middleware because I have already added that when creating the controller for the sign-in route.

Next is the import of the authRoute.js file into app.js file. This is how /api came about.

/app.js

const userRouter = require("./routes/authRoute");
app.use("/api", userRouter);

Testing Authentication Routes.

Now, it's time to test our routes. I am going to use thunderclient for sending requests and receiving responses. It is a vscode extension that can be installed.

First, we start by registering a user. we route to /api/signup and pass the user details through the form-encode. You should receive a response similar to mine.

To log in the user, route to /api/login and pass in the user's email and password. It should return the token as it did here.

Conclusion.

User authentication and authorization are common features in the modern web. In this article, we were able to discuss how to implement these features. In the next article, we will discuss how to create articles by a signed-in user.

Let's Connect

Thanks for reading.