DEV Community

Cover image for Mobile OTP based authentication and authorization API using Nodejs and Mongodb
Harsh Mangalam
Harsh Mangalam

Posted on

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

Top comments (12)

Collapse
 
hanpainghtoo profile image
Han Paing Htoo

Hello Sir !
Please Show .env
I can't move on Please Help Sir..

Collapse
 
harshmangalam profile image
Harsh Mangalam • Edited

.env file contains

PORT = 
MONGODB_URI =
NODE_ENV = 

JWT_SECRET = 
ORIGIN =

FAST2SMS =
ADMIN_PHONE = 

Enter fullscreen mode Exit fullscreen mode
Collapse
 
dibyendu_mitraroy_d955f8 profile image
Dibyendu Mitra Roy

Lots of missing links in the example some files / functions missing. I think the developer needs to provide a full-fledged working example when a example is given.

Collapse
 
alihammad profile image
Ali Hammad

Error: Cannot find module '../utils/password.util'

Collapse
 
seankeish profile image
seankeish

hi do u know how to change screens in kivymd using list items in list view?

Collapse
 
templekart profile image
Raj

please Upload it for nextjs

Collapse
 
harshmangalam profile image
Harsh Mangalam

Sure

Collapse
 
templekart profile image
Raj

Thanks

Collapse
 
saivarun2002 profile image
saivarun-2002

many names of the functions in auth controllers is not matching with the ones that exported in different files, please change the wrong ones

Collapse
 
saivarun2002 profile image
saivarun-2002

please add github link bro

Collapse
 
laweyez profile image
Bioga Allen

Does with work with international mobile numbers?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.