Mobile OTP based authentication and authorization API using Nodejs and Mongodb
Harsh Mangalam
Posted on June 6, 2021
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
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
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
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
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
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
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
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
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
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
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
{
"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
Posted on June 6, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.