Mobile OTP based authentication and authorization API using Nodejs and Mongodb

harshmangalam

Harsh Mangalam

Posted on June 6, 2021

Mobile OTP based authentication and authorization API using Nodejs and Mongodb

Authentication and Authorization is a key features of modern web api .Authentication provide access to user and Authorization allow access for specific role of Authenticated user.We cannot imagine Authorization without Authentication.

In this post we will implement OTP based authentication and authorization where user can access secured api using their identity.

Workflow of Authentication and Authorization in our API

  • User will register their account
  • User will login using mobile number
  • User will get 6 digit OTP on provided mobile number
  • User will verify their OTP
  • On success verification user will get jwt token which they can send on further request as a identity
  • we will add admin role to user if their mobile number will match with env variable mobile number other wise by default they will be normal user
  • admin user can access all endpoints
  • normal user can access all endpoints except admin related endpoints

library we will use

Dependencies

1) Express Js

Express is a back end framework for Node.js.It is designed for building web applications and APIs. It has been called the de facto standard server framework for Node.js

2) Mongoose

Mongoose is a Database ODM for Nodejs. It provide schema based api to model our mongodb schema.It is famous in world of Nodejs and Mongodb.

3) Jsonwebtoken
This package provide api to generate JWT token and verify those token using provided secrets key.

6) cors

cors is a middleware which helps to enable CORS (Cross Origin Resource Sharing). Our api will run on port 5000 and suppose we have client in react which run on port 3000 then CORS will not allow our react application to talk with nodejs api so we will configure cors in backend api

7) dotenv

Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env .

8) fast-two-sms

This package will help to send otp to mobile number using sms.

Dev Dependencies

1) Nodemon

We do not want after every changes to stop our nodejs server and reopen it is annoying and anti dev pattern so we will use nodemon library to automatically restart our server on changes to code.

2) Morgan

This package will log all sorts of meta data related to api request and response.

Initialize brand new nodejs project

yarn init -y

Enter fullscreen mode Exit fullscreen mode

Install all required library

Dependencies

yarn add cors dotenv  express fast-two-sms jsonwebtoken mongoose

Enter fullscreen mode Exit fullscreen mode

Dev Dependencies

yarn add -D nodemon morgan

Enter fullscreen mode Exit fullscreen mode

API endpoints


1) /api/auth/register

method POST
body {
     phone : String
     name : String
}



2) /api/auth/login_with_phone

method POST
body {
     phone : String
}


3) /api/auth/verify_otp

method POST
body {
     otp : String
     userId : String
}


4) /api/auth/me

method GET
headers {
     Authorization : Bearer jwt_token
}
access for both ADMIN and USER role

5) /api/auth/admin

method GET
headers {
    Authorization : Bearer jwt_token
}

access for only ADMIN role

Enter fullscreen mode Exit fullscreen mode

Project structure



app
  - src 
     - index.js

     - models
        - user.model.js

     - routes
        - auth.route.js


     - middlewares
        - checkAuth.js
        - checkAdmin.js

     - controllers
        - auth.controller.js


     - utils

        - token.util.js
        - otp.util.js

     - config.js

     - errors.js



Enter fullscreen mode Exit fullscreen mode

index.js

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


const { PORT, MONGODB_URI, NODE_ENV,ORIGIN } = require("./config");
const { API_ENDPOINT_NOT_FOUND_ERR, SERVER_ERR } = require("./errors");

// routes
const authRoutes = require("./routes/auth.route");


// init express app
const app = express();

// middlewares



app.use(express.json());
app.use(
  cors({
    credentials: true,
    origin: ORIGIN,
    optionsSuccessStatus: 200,
  })
);

// log in development environment

if (NODE_ENV === "development") {
  const morgan = require("morgan");
  app.use(morgan("dev"));
}

// index route

app.get("/", (req, res) => {
  res.status(200).json({
    type: "success",
    message: "server is up and running",
    data: null,
  });
});

// routes middlewares

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


// page not found error handling  middleware

app.use("*", (req, res, next) => {
  const error = {
    status: 404,
    message: API_ENDPOINT_NOT_FOUND_ERR,
  };
  next(error);
});

// global error handling middleware
app.use((err, req, res, next) => {
  console.log(err);
  const status = err.status || 500;
  const message = err.message || SERVER_ERR;
  const data = err.data || null;
  res.status(status).json({
    type: "error",
    message,
    data,
  });
});

async function main() {
  try {
    await mongoose.connect(MONGODB_URI, {
      useNewUrlParser: true,
      useCreateIndex: true,
      useFindAndModify: false,
      useUnifiedTopology: true,
    });

    console.log("database connected");

    app.listen(PORT, () => console.log(`Server listening on port ${PORT}`));
  } catch (error) {
    console.log(error);
    process.exit(1);
  }
}

main();



Enter fullscreen mode Exit fullscreen mode

config.js


exports.PORT = process.env.PORT;
exports.MONGODB_URI = process.env.MONGODB_URI;
exports.NODE_ENV = process.env.NODE_ENV;

exports.JWT_SECRET = process.env.JWT_SECRET;
exports.ORIGIN = process.env.ORIGIN;

exports.FAST2SMS = process.env.FAST2SMS
exports.ADMIN_PHONE = process.env.ADMIN_PHONE


Enter fullscreen mode Exit fullscreen mode

errors.js



exports.API_ENDPOINT_NOT_FOUND_ERR = "Api endpoint does not found";

exports.SERVER_ERR = "Something went wrong";

exports.AUTH_HEADER_MISSING_ERR = "auth header is missing";

exports.AUTH_TOKEN_MISSING_ERR = "auth token is missing";

exports.JWT_DECODE_ERR = "incorrect token";

exports.USER_NOT_FOUND_ERR = "User not found";


exports.ACCESS_DENIED_ERR = "Access deny for normal user";


Enter fullscreen mode Exit fullscreen mode

models/user.model.js


const { model, Schema } = require("mongoose");

const userSchema = new Schema(
  {
    name: {
      type: String,
      required: true,
      trim: true,

    },

    phone: {
      type: String,
      required: true,
      trim: true,
      unique: true,
    },

    role :{
     type : String,
     enum:["ADMIN","USER"],
     default:"USER",
    },


   phoneOtp:String


  },
  { timestamps: true }
);

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


Enter fullscreen mode Exit fullscreen mode

routes/auth.route.js



const express = require("express");
const router = express.Router();


const checkAuth = require("../middlewares/checkAuth");
const checkAdmin = require("../middlewares/checkAdmin");
const {
  fetchCurrentUser,
  loginUser,
  registerUser,
  verifyOTP,
  handleAdmin
} = require("../controllers/auth.controller");


router.post("/register", registerUser);

router.post("/login_with_phone", loginUser);


router.post("/verify", verifyOTP);

router.get("/me", checkAuth, fetchCurrentUser);

router.get("/admin", checkAuth, checkAdmin, handleAdmin);

module.exports = router;

Enter fullscreen mode Exit fullscreen mode

middlewares/checkAuth.js


const User = require("../models/user.model")

const { AUTH_TOKEN_MISSING_ERR, AUTH_HEADER_MISSING_ERR, JWT_DECODE_ERR, USER_NOT_FOUND_ERR } = require("../errors")
const { verifyJwtToken } = require("../utils/token.util")




module.exports = async (req, res, next) => {
    try {
        // check for auth header from client 
        const header = req.headers.authorization

        if (!header) {
            next({ status: 403, message: AUTH_HEADER_MISSING_ERR })
            return
        }

        // verify  auth token
        const token = header.split("Bearer ")[1]

        if (!token) {
            next({ status: 403, message: AUTH_TOKEN_MISSING_ERR })
            return
        }

        const userId = verifyJwtToken(token,next)

        if (!userId) {
            next({ status: 403, message: JWT_DECODE_ERR })
            return
        }

        const user = await User.findById(userId)

        if (!user) {
            next({status: 404, message: USER_NOT_FOUND_ERR })
            return
        }

        res.locals.user = user

        next()
    } catch (err) {
        next(err)
    }
}

Enter fullscreen mode Exit fullscreen mode

middlewares/checkAdmin.js


const { ACCESS_DENIED_ERR } = require("../errors");

module.exports = (req, res, next) => {
  const currentUser = res.locals.user;

  if (!currentUser) {
    return next({ status: 401, message: ACCESS_DENIED_ERR });
  }

  if (currentUser.role === "admin") {
    return next();
  }

  return next({ status: 401, message: ACCESS_DENIED_ERR });
};

Enter fullscreen mode Exit fullscreen mode

controllers/auth.controller.js


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

const {
  PHONE_NOT_FOUND_ERR,

  PHONE_ALREADY_EXISTS_ERR,
  USER_NOT_FOUND_ERR,
  INCORRECT_OTP_ERR,
  ACCESS_DENIED_ERR,
} = require("../errors");

const { checkPassword, hashPassword } = require("../utils/password.util");
const { createJwtToken } = require("../utils/token.util");

const { generateOTP, fast2sms } = require("../utils/otp.util");

// --------------------- create new user ---------------------------------

exports.createNewUser = async (req, res, next) => {
  try {
    let { phone, name } = req.body;


    // check duplicate phone Number
    const phoneExist = await User.findOne({ phone });

    if (phoneExist) {
      next({ status: 400, message: PHONE_ALREADY_EXISTS_ERR });
      return;
    }


    // create new user
    const createUser = new User({
      phone,
      name,
      role : phone === process.env.ADMIN_PHONE ? "ADMIN" :"USER"
    });

    // save user

    const user = await createUser.save();

    res.status(200).json({
      type: "success",
      message: "Account created OTP sended to mobile number",
      data: {
        userId: user._id,
      },
    });

    // generate otp
    const otp = generateOTP(6);
    // save otp to user collection
    user.phoneOtp = otp;
    await user.save();
    // send otp to phone number
    await fast2sms(
      {
        message: `Your OTP is ${otp}`,
        contactNumber: user.phone,
      },
      next
    );
  } catch (error) {
    next(error);
  }
};



// ------------ login with phone otp ----------------------------------

exports.loginWithPhoneOtp = async (req, res, next) => {
  try {

    const { phone } = req.body;
    const user = await User.findOne({ phone });

    if (!user) {
      next({ status: 400, message: PHONE_NOT_FOUND_ERR });
      return;
    }

    res.status(201).json({
      type: "success",
      message: "OTP sended to your registered phone number",
      data: {
        userId: user._id,
      },
    });

    // generate otp
    const otp = generateOTP(6);
    // save otp to user collection
    user.phoneOtp = otp;
    user.isAccountVerified = true;
    await user.save();
    // send otp to phone number
    await fast2sms(
      {
        message: `Your OTP is ${otp}`,
        contactNumber: user.phone,
      },
      next
    );
  } catch (error) {
    next(error);
  }
};

// ---------------------- verify phone otp -------------------------

exports.verifyPhoneOtp = async (req, res, next) => {
  try {
    const { otp, userId } = req.body;
    const user = await User.findById(userId);
    if (!user) {
      next({ status: 400, message: USER_NOT_FOUND_ERR });
      return;
    }

    if (user.phoneOtp !== otp) {
      next({ status: 400, message: INCORRECT_OTP_ERR });
      return;
    }
    const token = createJwtToken({ userId: user._id });

    user.phoneOtp = "";
    await user.save();

    res.status(201).json({
      type: "success",
      message: "OTP verified successfully",
      data: {
        token,
        userId: user._id,
      },
    });
  } catch (error) {
    next(error);
  }
};


// --------------- fetch current user -------------------------

exports.fetchCurrentUser = async (req, res, next) => {
  try {
    const currentUser = res.locals.user;


    return res.status(200).json({
      type: "success",
      message: "fetch current user",
      data: {
        user:currentUser,
      },
    });
  } catch (error) {
    next(error);
  }
};

// --------------- admin access only -------------------------

exports.handleAdmin = async (req, res, next) => {
  try {
    const currentUser = res.locals.user;

    return res.status(200).json({
      type: "success",
      message: "Okay you are admin!!",
      data: {
        user:currentUser,
      },
    });
  } catch (error) {
    next(error);
  }
};



Enter fullscreen mode Exit fullscreen mode

utils/token.util.js


const jwt = require("jsonwebtoken");
const { JWT_DECODE_ERR } = require("../errors");
const { JWT_SECRET } = require("../config");

exports.createJwtToken = (payload) => {
  const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "12h" });
  return token;
};

exports.verifyJwtToken = (token, next) => {
  try {
    const { userId } = jwt.verify(token, JWT_SECRET);
    return userId;
  } catch (err) {
    next(err);
  }
};

Enter fullscreen mode Exit fullscreen mode

utils/otp.util.js



const fast2sms = require("fast-two-sms");
const {FAST2SMS} = require("../config");

exports.generateOTP = (otp_length) => {
  // Declare a digits variable
  // which stores all digits
  var digits = "0123456789";
  let OTP = "";
  for (let i = 0; i < otp_length; i++) {
    OTP += digits[Math.floor(Math.random() * 10)];
  }
  return OTP;
};

exports.fast2sms = async ({ message, contactNumber }, next) => {
  try {
    const res = await fast2sms.sendMessage({
      authorization: FAST2SMS,
      message,
      numbers: [contactNumber],
    });
    console.log(res);
  } catch (error) {
    next(error);
  }
};

Enter fullscreen mode Exit fullscreen mode

package.json



{
  "name": "app",
  "version": "1.0.0",
  "description": "otp base authentication & authorization",
  "main": "index.js",
  "author": "Harsh Mangalam",
  "license": "MIT",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "dependencies": {
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "fast-two-sms": "^3.0.0",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^5.12.3",

  },
  "devDependencies": {
    "morgan": "^1.10.0",
    "nodemon": "^2.0.7"
  }
}


Enter fullscreen mode Exit fullscreen mode

start server

yarn dev

Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
harshmangalam
Harsh Mangalam

Posted on June 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related