Welcome, programming, buddies! Today, 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.
Click on the below button to get the complete code of this project on GitHub.
JSON Web Token (JWT) is a JSON object that is described in RFC 7519 as a safe approach to transfer a set of information between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
Let’s understand from the below diagram how does the secure authentication system work with JSON web token.
image source medium.com
Table of contents
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
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
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
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)
unique: true
property in email schema does the internal optimization to enhance the performance.Create database folder in the project folder and create a new file database/db.js in it.
module.exports = {
db: 'mongodb://localhost:27017/meanauthdb'
}
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;
bcrypt.compare()
method.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.
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.
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;
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.
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.
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 #node #security #web-development #javascript