How to Create Secure (JWT) Token Based Authentication API with Node.js

How to Create Secure (JWT) Token Based Authentication API with Node.js

In this tutorial, we are going to learn how to build a secure token-based user authentication REST APIs using JWT (JSON web token), bcrypt, Node, Express, and MongoDB

Creating authentication REST API with Node Js is merely effortless. We will be taking the help of Express js to create the authentication endpoints and also make the MongoDB connection to store the user’s data in it.

Authentication Workflow with JSON Web Tokens

Let’s understand from the below diagram how does the secure authentication system work with JSON web token.

Token Based Authentication API

  • A client makes the API call and sends the user information such as username and password to the webserver.
  • On successful authentication a webserver generates a string-based token and returns to the client system.
  • A client can store this token in the browser’s local storage or in a session.
  • Client sets this token in a header something like “Bearer xxx.xxx.xxx”.
  • On next API call JWT token communicateS with the server, and after the successful verification, the server returns the response to the client.

Initiate Node Token-Based Authentication Project

Create a project folder to build secure user authentication REST API, run the following command.

mkdir server

Get inside the project folder.

cd server

Let’s start the project by first creating the package.json file by running the following command.

npm init

Install NPM Packages to Create Secure Auth API

Next, install the NPM dependencies for the authentication API by running the given below command.

npm install express jsonwebtoken bcryptjs body-parser 
cors mongoose-unique-validator mongoose --save

This is image title

Next, install the nodemon NPM module, it helps in starting the node server when any change occurs in the server files.

npm install nodemon --save-dev

Define Mongoose Schema

Next, we are going to define user schema using mongoose ODM. It allows us to retrieve the data from the database.

Create a folder and name it models inside the project directory, create a file User.js in it.

To prevent storing the duplicate email id in MongoDB database install mongoose-unique-validator package. Below we will learn how to use in mongoose schema to validate duplicate email id from MongoDB database.

npm i mongoose-unique-validator --save

Next, add the following code in models/User.js file:

// models/User.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const uniqueValidator = require('mongoose-unique-validator');

let userSchema = new Schema({
    name: {
        type: String
    },
    email: {
        type: String,
        unique: true
    },
    password: {
        type: String
    }
}, {
    collection: 'users'
})

userSchema.plugin(uniqueValidator, { message: 'Email already in use.' });
module.exports = mongoose.model('User', userSchema)
  • The userSchema.plugin(uniqueValidator) method won’t let duplicate email id to be stored in the database.
  • The unique: true property in email schema does the internal optimization to enhance the performance.

Implement MongoDB Database in Node App

Create database folder in the project folder and create a new file database/db.js in it.

module.exports = {
    db: 'mongodb://localhost:27017/meanauthdb'
}

Create Secure Token-based Authentication REST API in Node

To build secure user authentication endpoints in node, create routes folder, and auth.routes.js file in it.

Here, we will define CRUD Restful APIs using the npm packages for log-in, sign-up, update-user, and delete-user.

// routes/auth.routes.js

const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
const router = express.Router();
const userSchema = require("../models/User");

// Sign-up
router.post("/register-user", (req, res, next) => {
    bcrypt.hash(req.body.password, 10).then((hash) => {
        const user = new userSchema({
            name: req.body.name,
            email: req.body.email,
            password: hash
        });
        user.save().then((response) => {
            res.status(201).json({
                message: "User successfully created!",
                result: response
            });
        }).catch(error => {
            res.status(500).json({
                error: error
            });
        });
    });
});

// Sign-in
router.post("/signin", (req, res, next) => {
    let getUser;
    userSchema.findOne({
        email: req.body.email
    }).then(user => {
        if (!user) {
            return res.status(401).json({
                message: "Authentication failed"
            });
        }
        getUser = user;
        return bcrypt.compare(req.body.password, user.password);
    }).then(response => {
        if (!response) {
            return res.status(401).json({
                message: "Authentication failed"
            });
        }
        let jwtToken = jwt.sign({
            email: getUser.email,
            userId: getUser._id
        }, "longer-secret-is-better", {
            expiresIn: "1h"
        });
        res.status(200).json({
            token: jwtToken,
            expiresIn: 3600,
            msg: getUser
        });
    }).catch(err => {
        return res.status(401).json({
            message: "Authentication failed"
        });
    });
});

// Get Users
router.route('/').get((req, res) => {
    userSchema.find((error, response) => {
        if (error) {
            return next(error)
        } else {
            res.status(200).json(response)
        }
    })
})

// Get Single User
router.route('/user-profile/:id').get((req, res, next) => {
    userSchema.findById(req.params.id, (error, data) => {
        if (error) {
            return next(error);
        } else {
            res.status(200).json({
                msg: data
            })
        }
    })
})

// Update User
router.route('/update-user/:id').put((req, res, next) => {
    userSchema.findByIdAndUpdate(req.params.id, {
        $set: req.body
    }, (error, data) => {
        if (error) {
            return next(error);
            console.log(error)
        } else {
            res.json(data)
            console.log('User successfully updated!')
        }
    })
})

// Delete User
router.route('/delete-user/:id').delete((req, res, next) => {
    userSchema.findByIdAndRemove(req.params.id, (error, data) => {
        if (error) {
            return next(error);
        } else {
            res.status(200).json({
                msg: data
            })
        }
    })
})

module.exports = router;
  • To secure the password, we are using the bcryptjs, It stores the hashed password in the database.
  • In the signin API, we are checking whether the assigned and retrieved passwords are the same or not using the bcrypt.compare() method.
  • In the signin API, we set the JWT token expiration time. Token will be expired within the defined duration.

Verify Node Authentication REST API

Next, we will verify the auth API using the JWT token. Create a middlewares folder and create a auth.js file inside of it, then include the following code in it.

Note: In the real world app the secret should not be kept in the code as declared below. The best practice is to store as an environment variable and it should be complex combination of numbers and strings.

// middlewares/auth.js

const jwt = require("jsonwebtoken");

module.exports = (req, res, next) => {
    try {
        const token = req.headers.authorization.split(" ")[1];
        jwt.verify(token, "longer-secret-is-better");
        next();
    } catch (error) {
        res.status(401).json({ message: "Authentication failed!" });
    }
};

Now, we will learn to implement JWT verification in the /user-profile endpoint. Import the following auth.js file from middlewares folder.

// Get User Profile
router.route('/user-profile/:id').get(authorize, (req, res, next) => {
    userSchema.findById(req.params.id, (error, data) => {
        if (error) {
            return next(error);
        } else {
            res.status(200).json({
                msg: data
            })
        }
    })
})

We added the authorize variable inside the user-profile API. It won’t render the data unless it has the valid JWT token. As you can see in the below screenshot, we have not defined the JWT token in get request, so we are getting the “No token provided” error.

JWT Verification Error in Express API

Adding Input Validation in Express RESTful API

Next, we will learn to implement validation in Express auth API using POST body request. Install express-validator npm library to validate name, email and password.

The express-validator is an express.js middleware for validating POST body requests.

Run the below command to install the express-validator package.

npm install express-validator --save

Add the following code in the middlewares/auth.routes.js file.

// routes/auth.routes.js

const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
const router = express.Router();
const userSchema = require("../models/User");
const authorize = require("../middlewares/auth");
const { check, validationResult } = require('express-validator');

// Sign-up
router.post("/register-user",
    [
        check('name')
            .not()
            .isEmpty()
            .isLength({ min: 3 })
            .withMessage('Name must be atleast 3 characters long'),
        check('email', 'Email is required')
            .not()
            .isEmpty(),
        check('password', 'Password should be between 5 to 8 characters long')
            .not()
            .isEmpty()
            .isLength({ min: 5, max: 8 })
    ],
    (req, res, next) => {
        const errors = validationResult(req);
        console.log(req.body);

        if (!errors.isEmpty()) {
            return res.status(422).jsonp(errors.array());
        }
        else {
            bcrypt.hash(req.body.password, 10).then((hash) => {
                const user = new userSchema({
                    name: req.body.name,
                    email: req.body.email,
                    password: hash
                });
                user.save().then((response) => {
                    res.status(201).json({
                        message: "User successfully created!",
                        result: response
                    });
                }).catch(error => {
                    res.status(500).json({
                        error: error
                    });
                });
            });
        }
    });

// Sign-in
router.post("/signin", (req, res, next) => {
    let getUser;
    userSchema.findOne({
        email: req.body.email
    }).then(user => {
        if (!user) {
            return res.status(401).json({
                message: "Authentication failed"
            });
        }
        getUser = user;
        return bcrypt.compare(req.body.password, user.password);
    }).then(response => {
        if (!response) {
            return res.status(401).json({
                message: "Authentication failed"
            });
        }
        let jwtToken = jwt.sign({
            email: getUser.email,
            userId: getUser._id
        }, "longer-secret-is-better", {
            expiresIn: "1h"
        });
        res.status(200).json({
            token: jwtToken,
            expiresIn: 3600,
            _id: getUser._id
        });
    }).catch(err => {
        return res.status(401).json({
            message: "Authentication failed"
        });
    });
});

// Get Users
router.route('/').get((req, res) => {
    userSchema.find((error, response) => {
        if (error) {
            return next(error)
        } else {
            res.status(200).json(response)
        }
    })
})

// Get Single User
router.route('/user-profile/:id').get(authorize, (req, res, next) => {
    userSchema.findById(req.params.id, (error, data) => {
        if (error) {
            return next(error);
        } else {
            res.status(200).json({
                msg: data
            })
        }
    })
})

// Update User
router.route('/update-user/:id').put((req, res, next) => {
    userSchema.findByIdAndUpdate(req.params.id, {
        $set: req.body
    }, (error, data) => {
        if (error) {
            return next(error);
            console.log(error)
        } else {
            res.json(data)
            console.log('User successfully updated!')
        }
    })
})

// Delete User
router.route('/delete-user/:id').delete((req, res, next) => {
    userSchema.findByIdAndRemove(req.params.id, (error, data) => {
        if (error) {
            return next(error);
        } else {
            res.status(200).json({
                msg: data
            })
        }
    })
})

module.exports = router;

We Passed the validation array with the check() method inside the post() method as a second argument. Next, we called the validationResult() method to validate errors, and it returns the errors if found any.

Following validation we implemented in ("/register-user") api.

  • Check if the value is required.
  • Check min and max character’s length.

Express input validation

Final auth.routes.js

// routes/auth.routes.js

const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
const router = express.Router();
const userSchema = require("../models/User");
const authorize = require("../middlewares/auth");
const { check, validationResult } = require('express-validator');

// Sign-up
router.post("/register-user",
    [
        check('name')
            .not()
            .isEmpty()
            .isLength({ min: 3 })
            .withMessage('Name must be atleast 3 characters long'),
        check('email', 'Email is required')
            .not()
            .isEmpty(),
        check('password', 'Password should be between 5 to 8 characters long')
            .not()
            .isEmpty()
            .isLength({ min: 5, max: 8 })
    ],
    (req, res, next) => {
        const errors = validationResult(req);
        console.log(req.body);

        if (!errors.isEmpty()) {
            return res.status(422).jsonp(errors.array());
        }
        else {
            bcrypt.hash(req.body.password, 10).then((hash) => {
                const user = new userSchema({
                    name: req.body.name,
                    email: req.body.email,
                    password: hash
                });
                user.save().then((response) => {
                    res.status(201).json({
                        message: "User successfully created!",
                        result: response
                    });
                }).catch(error => {
                    res.status(500).json({
                        error: error
                    });
                });
            });
        }
    });

// Sign-in
router.post("/signin", (req, res, next) => {
    let getUser;
    userSchema.findOne({
        email: req.body.email
    }).then(user => {
        if (!user) {
            return res.status(401).json({
                message: "Authentication failed"
            });
        }
        getUser = user;
        return bcrypt.compare(req.body.password, user.password);
    }).then(response => {
        if (!response) {
            return res.status(401).json({
                message: "Authentication failed"
            });
        }
        let jwtToken = jwt.sign({
            email: getUser.email,
            userId: getUser._id
        }, "longer-secret-is-better", {
            expiresIn: "1h"
        });
        res.status(200).json({
            token: jwtToken,
            expiresIn: 3600,
            msg: getUser
        });
    }).catch(err => {
        return res.status(401).json({
            message: "Authentication failed"
        });
    });
});

// Get Users
router.route('/').get(authorize, (req, res) => {
    userSchema.find((error, response) => {
        if (error) {
            return next(error)
        } else {
            res.status(200).json(response)
        }
    })
})

// Get Single User
router.route('/user-profile/:id').get((req, res, next) => {
    userSchema.findById(req.params.id, (error, data) => {
        if (error) {
            return next(error);
        } else {
            res.status(200).json({
                msg: data
            })
        }
    })
})

// Update User
router.route('/update-user/:id').put((req, res, next) => {
    userSchema.findByIdAndUpdate(req.params.id, {
        $set: req.body
    }, (error, data) => {
        if (error) {
            return next(error);
            console.log(error)
        } else {
            res.json(data)
            console.log('User successfully updated!')
        }
    })
})

// Delete User
router.route('/delete-user/:id').delete((req, res, next) => {
    userSchema.findByIdAndRemove(req.params.id, (error, data) => {
        if (error) {
            return next(error);
        } else {
            res.status(200).json({
                msg: data
            })
        }
    })
})

module.exports = router;

Node Server Configuration

Create a server.js file in the token-based authentication project’s folder and paste the following code in it.

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const bodyParser = require('body-parser');
const dbConfig = require('./database/db');

// Express APIs
const api = require('./routes/auth.routes');

// MongoDB conection
mongoose.Promise = global.Promise;
mongoose.connect(dbConfig.db, {
    useNewUrlParser: true,
    useUnifiedTopology: true
}).then(() => {
    console.log('Database connected')
},
    error => {
        console.log("Database can't be connected: " + error)
    }
)

// Remvoe MongoDB warning error
mongoose.set('useCreateIndex', true);

// Express settings
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
    extended: false
}));
app.use(cors());

// Serve static resources
app.use('/public', express.static('public'));

app.use('/api', api)

// Define PORT
const port = process.env.PORT || 4000;
const server = app.listen(port, () => {
    console.log('Connected to port ' + port)
})

// Express error handling
app.use((req, res, next) => {
    setImmediate(() => {
        next(new Error('Something went wrong'));
    });
});

app.use(function (err, req, res, next) {
    console.error(err.message);
    if (!err.statusCode) err.statusCode = 500;
    res.status(err.statusCode).send(err.message);
});

In this file we defined mongoDB database, express routes, PORT and errors.

Start Node Server

Now, we have placed everything at its place, and now it’s time to start the Node server. Open the terminal and run the given below commands to start the Node server and mongoDB:

Start the MongoDB database:

mongod

Start the nodemon server:

nodemon

You can test Node server on the following URL: http://localhost:4000/api

Here, are the user authentication CRUD REST APIs built with Node.js.

API Methods API URL
GET (Users List) /api
POST (Sign in) /api/signin
POST (Sign up) /api/register-user
GET (User Profile) /api/user-profile/id
PUT (Update User) /api/update-user/id
DELETE (Delete User) /api/delete-user/id

Conclusion

Finally, we have completed secure Token-Based Authentication REST API with Node.js tutorial. So far, In this tutorial we have learned how to securely store the password in the database using the hash method with bcryptjs, how to create JWT token to communicate with the client and a server using jsonwebtoken. We also implemented the Express input validation using the express-validator plugin.

I hope you liked this tutorial, please share it with others, thanks for reading!

node.js Express MongoDB JWT

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

Node.js Express MongoDB Tutorial

Node.js Express MongoDB Tutorial is CRUD example, I have created to showcase Express web framework, MongoDB NoSQL database, and Node.js server platform.

How to Use Express.js, Node.js and MongoDB.js

In this post, I will show you how to use Express.js, Node.js and MongoDB.js. We will be creating a very simple Node application, that will allow users to input data that they want to store in a MongoDB database. It will also show all items that have been entered into the database.

Ecommerce MERN(MongoDB Express.js React.js Node.js) Redux Tailwind

Ecommerce MERN(MongoDB Express.js React.js Node.js) Redux Tailwind

Chat App using Node.js, Express, MongoDB, React.js, Socket.io and JWT

In this post, you'll learn how to Build Chat Application using Node.js, Express, MongoDB, React.js, Socket.io and JWT i.e in MERN STACK.

Build a REST API using Node.js, Express.js, Mongoose.js and MongoDB

Node.js, Express.js, Mongoose.js, and MongoDB is a great combination for building easy and fast REST API. You will see how fast that combination than other existing frameworks because of Node.js is a packaged compilation of Google’s V8 JavaScript engine and it works on non-blocking and event-driven I/O. Express.js is a Javascript web server that has a complete function of web development including REST API.