Mobile OTP based authentication and authorization API using Nodejs and Mongodb
Tags: mongodb, webdev, node, javascript
Published: 2021-06-06 16:53:50

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

```


##  Install all required library

### Dependencies

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

```


### Dev Dependencies

```
yarn add -D nodemon morgan

```


## 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

```




## 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

  

```



`index.js`

```javascript
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();



```


`config.js`

```javascript

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


```



`errors.js`


```javascript


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";


```


`models/user.model.js`

``` javascript

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);


```



`routes/auth.route.js`

``` javascript


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;

```



`middlewares/checkAuth.js`


```javascript

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)
    }
}

```


`middlewares/checkAdmin.js`

```javascript

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 });
};

```


`controllers/auth.controller.js`

```javascript

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);
  }
};



```



`utils/token.util.js`

```javascript

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);
  }
};

```



`utils/otp.util.js`


```javascript


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);
  }
};

```


`package.json`

```javascript


{
  "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"
  }
}


```

`start server`

```
yarn dev

```