User Authentication and Authorization using npm bcryptjs, JWT, and MongoDB Atlas
Let us build an express API for this.
We will be going through the following steps to build this API.
1. Installation:
Make sure you have NodeJS, npm, and MongoDB installed on your system to move forward with the blog.
- Create a project folder and use the terminal to run the command
npm init -y
- Let us first download the npm packages
npm i express bcryptjs jsonwebtoken config mongoose
2. Initialize express app:
const express = require('express');
const app = express();
app.get('/',(req,res)=> res.json({msg:"Hello World!"}));
const PORT = 5000;
app.listen(PORT,()=>console.log('Server started on port: 5000'));
Add the script "start": "node server.js"
in package.json file and run npm start
Bravo! Our app is ready to run at localhost:5000
3. Connection to MongoDB Atlas and writing mongoose schema
- Create an account at MongoDB Atlas
- Create a cluster and add your connection string into your application code in a default.json file of the config folder. npm config organizes hierarchical configurations for app deployments.
{ "mongoURI": "mongodb+srv://<User>:<password>@cluster0.qkgl9.mongodb.net/<Database Name>" }
- Create db.js file in same config folder to establish connection with database.
const mongoose = require('mongoose'); const config = require('config'); const db = config.get('mongoURI'); const connectDB = ()=>{ //Connection to MongoDB cluster mongoose.connect(db,{ useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology:true, useFindAndModify: false }).then(()=>console.log('MongoDB connected')) .catch(err=>{ console.error("Database error",err.message); process.exit(1); }); }; module.exports = connectDB;
- Let us now create a mongoose schema for the user model (A Mongoose schema is a document data structure that allows you to define the fields stored in each document along with their validation requirements and default values. Mongoose model provides an interface to the database for CRUD operations.)
- Create Users.js file in models folder to add the below schema.
const mongoose = require('mongoose'); const UserSchema = mongoose.Schema({ email:{ type:String, required:true, unique:true }, password:{ type:String, required:true }, date:{ type:Date, default: Date.now } }); module.exports = mongoose.model('user',UserSchema);
- Call connectDB() in server.js
const connectDB = require('./config/db') //Connect database connectDB();
Before creating routes let us dive into npm bcrypt and jsonwebtoken.
bcryptjs
It allows us to save the password in the database with hashing technique in a secure way. Plain-text (unencrypted) passwords must not be stored in the database. It has to be encrypted and for this bcryptjs provides certain methods. The methods which we are going to use are
- bcrypt.gensalt(saltRounds_value): Instead of hashing only the password bcrypt uses salt with the password. This method generates the salt which is a random string.
- bcrypt.hash(userPassword, salt): By hashing a plain text password plus a salt, the hash algorithm’s output becomes unpredictable. Store hashed password in DB.
- bcrypt.compare(password,user.password): when a user attempts to log in, we have to compare the password entered by the user with the one stored in the database.
jsonwebtoken
The JSON web token (JWT) is one method for allowing authentication, without actually storing any information about the user on the system itself. Authorization is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Read more about JWT at jwt.io.
4. User authorization using JWT
- When the user registers or logs in to the account token gets generated for the user with the help of JWT. We can return the token as a JSON object with the API result.
- We can store the token returned by the backend into the local-storage for the specific period and whenever the user tries to access the website we can send the token in the request header to get verified by the backend. After getting verified we can return user details to the frontend.
- To verify the token let us create the auth.js middleware(a function that has access to request and response cycle object).
Here I have saved the jwtSecret string in config folder's default.json file. We can use different strings for development and production by saving it in two different files.const jwt = require('jsonwebtoken'); const config = require('config'); module.exports = function(req,res,next){ //Get token from request header const token = req.header('x-auth-token'); if(!token){ return res.status(401).json({msg:'No token, auth failed'}); } try { //verify token & extract payload const decoded = jwt.verify(token,config.get('jwtSecret')); req.user = decoded.user; next(); } catch (err) { res.status(401).json({msg: 'Token is invalid'}); } }
5. Creation of Routes
- We will be using Application-level and Router-level middlewares to handle the request coming from client.(Visit Using middleware to learn more about types of express middleware)
- Add below routes in server.js
app.use(express.json({extended:false})); //bodyparser-Used to parse JSON bodies
//Define Routes
app.use('/api/users',require('./routes/users'));
app.use('/api/auth',require('./routes/auth'));
- Create routes folder with auth.js and users.js files. auth route will help to authenticate the user at the time of login and authorize the user with the help of token verification middleware. users route will help to register the user and save details into the database. I haven't provided validation middleware, but you can add validations afterward.
auth.js:
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const config = require('config');
const auth = require('../middleware/auth')
const User = require('../models/User');
//@route GET api/auth
//@desc Get logged in user
//@access Private
router.get('/', auth, async (req,res)=>{
try {
//Get user details except the password
const user = await User.findById(req.user.id).select('-password');
res.json(user);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
//@route POST api/auth
//@desc Auth user and get token(login)
//@access Public
router.post('/',
async (req,res)=>{
const {email,password} = req.body;
try {
let user = await User.findOne({email});
if(!user){
return res.status(500).json({msg:"Invalid credentials"});
}
const isMatch = await bcrypt.compare(password,user.password);
if(!isMatch){
return res.status(500).json({msg:"Password invalid"});
}
const payload = {
user:{
id:user.id
}
}
jwt.sign(payload,config.get('jwtSecret'),
{
expiresIn:360000
},
(err,token)=>{
if(err) throw err;
res.json({token});
})
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
module.exports = router;
users.js
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const config = require('config');
const router = express.Router();
const User = require('../models/User');
//@route POST api/users
//@desc User Registration
//@access Public
router.post('/', async (req,res)=>{
const { email,password } = req.body;
try {
let user = await User.findOne({email});
if(user){
return res.status(400).json({msg:'User already exist'});
}
user = new User({
email,
password
});
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password,salt);
await user.save();
const payload = {
user:{
id:user.id
}
}
jwt.sign(payload,config.get('jwtSecret'),
{
expiresIn:360000
},
(err,token)=>{
if(err) throw err;
res.json({token});
}
)
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
});
module.exports = router;
Now our express server is ready to authorize the user. Run npm start and test the API routes with the help of Postman. For better understanding, refer to my GitHub Repo.