In this tutorial, I will implement the JWT authorization with the express application to protect endpoints. So resources will be login authorization, user registration, refresh token can generate new access and refresh token
What are authentication and authorization?
Authentication and authorization are used to protect applications and endpoints to secure applications. Particularly when it comes to getting access to applications. Yet, there is a significant distinction between gaining entry into a system (authentication) and what you can do while inside (authorization).
What is Authentication?
Authentication is the process of verifying who a user is, through the credentials and using those credentials to confirm the user’s identity. This is the first step in any security process
You are already aware of the authentication process, logging into your computer) or logging into an application/website. Yet, the truth is that most “things” connected to the Internet require you to prove your identity by providing credentials.
What is Authorization?
Authorization in the application is the process of giving the user permission to access a specific resource or endpoints, The authorization process always follows the authentication procedure.
Giving someone permission to download a particular file and access endpoints on providing credentials
App initial setup
Generate nodejs app
mkdir node-jwt && cd node-jwt npm init -y
Install dependencies
npm i express jsonwebtoken dotenv bcrypt joi mongoose npm i nodemon -D
I install dependencies like express, jsonwebtoken, dotenv, joi, mongoose and for the development process, I install nodemon to restart the server when the file changes.
Jsonwebtoken will generate tokens, dotenv help us to configure esally environment variables. Joi help us to validate post data. bcrypt help us to store the password as hash form.
I am going to use MongoDB for the database, so for MongoDB, I install mongoose dependency.
Create directories and files
I create folders to the structure of the express app.
mkdir config db models middlewares routes
Create files
touch config/index.js db/connect.js models/user.model.js middlewares/auth.js routes/index.js routes/auth.routes.js .env
Create server files
touch app.js routes.js
Project folder and file system
sajal@sajal-PC:~/techincent/node-jwt$ tree -L 2 -I node_modules . ├── app.js ├── config │ └── index.js ├── db │ └── connect.js ├── middlewares │ └── auth.js ├── models │ ├── refresh-token.model.js │ └── user.model.js ├── package.json ├── package-lock.json ├── routes │ ├── auth.route.js │ └── index.js └── services └── jwt-service.js 6 directories, 11 files
Create node server and connect to mongo database
Set envirments veriables for project
At first, we are going to set the environment variables config. In the .env file, add environment variables.
APP_PORT=8000 DB_URL=mongodb://localhost:27017/jwt-example-db NODE_ENV=development SECRET_KEY=5cc4f1f6d0f7b86d4b2d3fe97cf80870393e9c0a0848d7aa45fe9920 REFRESH_KEY=27dbf432b305a6d445cf78bb9ab89ec14e1c00715ddd31eb0c2b2299
I am gonna using my local machine MongoDB database. and my database name jwt-example-db.
Then import environment variable to project in config/index.js file
It’s better to use module import all environments variables in project config. which is good to maintain the project.
const dotenv = require('dotenv'); dotenv.config(); module.exports = { APP_PORT, DB_URL, NODE_ENV, SECRET_KEY, REFRESH_KEY } = process.env;
Create database connect
In the db/connect.js file, add the MongoDB configuration connect function
const mongoose = require('mongoose'); const { DB_URL } = require('../config'); exports.connect = () => { return mongoose.connect(DB_URL, { useNewUrlParser: true, useUnifiedTopology: true }) .then(() => { console.info("Database connected...") }) .catch((err) => { console.error(err); process.exit(1); }) }
Configrations root route file
In the routes/index.js, create a basic route
module.exports = function (app) { app.get('', (req, res, next) => { res.json({'message': 'Hello! developer'}) }) // Auth routes will come below }
Connect routes and start applicaion
In the app.js file,
const express = require('express'); const { APP_PORT } = require('./config'); const dbConnect = require('./db/connect'); const app = express(); const routes = require('./routes'); app.use(express.json()); app.use(express.urlencoded({ extended: false })) app.listen(APP_PORT || 8000, () => { console.info(`Server listen at http://localhost:${APP_PORT || 8000}`) // Database dbConnect.connect(); // Routes routes(app); })
you can see the single routes function which is connect all routes to this project
Add node app terminal script command
In the package.json file, add app server start command
... "scripts": { "start": "nodemon app.js", "test": "echo \"Error: no test specified\" && exit 1" }, ...
Start node server
npm start
Great, Bravo successfully start node serve, you can test simple API server with http://localhost:8000
Create auth routes
In the routes/auth.route.js. create user login, register, and refresh token routes. All three routes will be posted to get clint input data
const Joi = require('joi'); const express = require('express'); const router = express.Router(); const JwtService = require('../services/jwt-service'); const User = require('../models/user.model'); const { REFRESH_KEY } = require('../config'); router.post('/login', async (req, res, next) => { // Logic will be here }) router.post('/register', async (req, res, next) => { // logic will be here }), router.post('/refresh', async (req, res, next) => { // logic will be here }) module.exports = router
Connect auth route to project root route, go to routes/index.js file, and add auth routes
const authRoutes = require('./auth.route'); module.exports = function (app) { ... // Auth routes app.use('/auth', authRoutes); }
Implement JWTServices
In the services/jwt-service.js. define JwtService class which contain two static method sign, verify. One “sign” method helps to generate jwt token. In the other verify help us to verify tokens. Both methods use SECRET_KEY as the default key.
const jwt = require('jsonwebtoken'); const { SECRET_KEY } = require('../config'); class JwtService { static sign(payload, expiry='6h', secret=SECRET_KEY) { return jwt.sign(payload, secret, { expiresIn: expiry }) } static verify(token, secret=SECRET_KEY) { return jwt.verify(token, secret) } } module.exports = JwtService
Implement register route logic
In routes/auth.route.js file
// ... router.post('/register', async (req, res, next) => { const formData = req.body; const registerSchema = Joi.object({ name: Joi.string().required(), email: Joi.string().email().required(), number: Joi.number(), password: Joi.string().required(), password_repeat: Joi.ref('password'), }) const { error } = registerSchema.validate(formData); if (error) { return res.status(422).json({ status: 422, message: error?.message }); } // Check user exists // console.log('Checking user is exists') try { const isExists = await User.exists({ email: formData.email }); if (isExists) { return res.status(409).json({ status: 409, message: 'User exists! This email is taken' }); } } catch (err) { return next(err) } // Create user // console.log('Create user') const userPayload = { name: formData.name, email: formData.email, number: formData.number, password: formData.password } try { let user = new User(userPayload); // const user = await User.create(userPayload); user = await user.save(); const payload = { _id: user._id, name: user.name, role: user.role, email: user.email } const access_token = JwtService.sign(payload); const refresh_token = JwtService.sign(payload, '1y', REFRESH_KEY); // Store refresh token await RefreshToken.create({ token: refresh_token }); res.status(201).json({ status: 201, message: 'User created', access_token, refresh_token, data: user }) } catch (err) { return res.status(500).json({ status: 500, message: err.message }); } }), // ...
Note: Make sure your API request headers contain, “Content-Type”: “application/json”
When you post register API. It will create a user in “user” table and a refresh token in “refreshtokens” table. which mean this store refresh token valid to request new access_token
Implement login route logic
In routes/auth.route.js file
// ... router.post('/login', async (req, res, next) => { // Request validation const loginSchema = Joi.object({ email: Joi.string().email().required(), password: Joi.string().required() }) const { error } = loginSchema.validate(req.body); if (error) { return res.status(422).json({ status: 422, message: error.message }); } // Start database query try { const user = await User.findOne({ email: req.body.email }).select("-updateAt -__v"); if (!user) { return res.status(401).json({ status: 401, message: 'User is not found' }); } // Check password const isPasswordMatch = await user.comparePassword(req.body.password); if (!isPasswordMatch) { return res.status(401).json({ status: 401, message: 'Your password is wrong' }); } // Generate token const payload = { _id: user._id, name: user.name, role: user.role, email: user.email } const access_token = JwtService.sign(payload); const refresh_token = JwtService.sign(payload, '30d', REFRESH_KEY) // Store refresh token await RefreshToken.create({ token: refresh_token }); res.json({ access_token, refresh_token, data: user, message: 'Sign in success', status: 200 }) res.end(); } catch (err) { return res.status(500).json({ status: 500, message: err.message }); } }) // ...
Implement refresh token route logic
In routes/auth.route.js file
// ... router.post('/refresh', async (req, res, next) => { const tokenSchema = Joi.object({ refresh_token: Joi.string().required() }) const { error } = tokenSchema.validate(req.body); if (error) { return res.status(422).json({ status: 422, message: error.message }); } try { const refreshTokenObj = await RefreshToken.findOne({ token: req.body.refresh_token }); console.log('refresh token', refreshTokenObj) if (!refreshTokenObj) { return res.status(401).json({ status: 401, message: 'Invalid, Token is not found' }); } let userId; try { const { _id } = JwtService.verify(refreshTokenObj.token, REFRESH_KEY); userId = _id } catch (err) { return res.status(401).json({ status: 401, message: 'Invalid refresh token' }) } const user = await User.findOne({ _id: userId }).select("-password -updatedAt -__v") if (!user) { return res.status(401).json({ status: 401, message: 'No user found!' }) } const payload = { _id: user._id, name: user.name, email: user.email, role: user.role } const access_token = JwtService.sign(payload); const refresh_token = JwtService.sign(payload, '30d', REFRESH_KEY) // Store refresh token await RefreshToken.create({ token: refresh_token }); res.status(201).json({ access_token, refresh_token, user }) } catch (err) { console.log(err) return res.status(500).json({ status: 500, message: err.message }); } }) // ...
Create authorization middleware
In the middlewares/auth.js file
const JwtService = require("../services/jwt-service"); const authorizationHandler = (req, res, next) => { const headerAuthorization = req.headers.authorization; if (!headerAuthorization) { return res.status(401).json({ status: 401, message: 'Unauthorize user!' }); } // It's for Bearer token header const token = headerAuthorization.split(' ')[1]; try { const { _id, email, role } = JwtService.verify(token); const user = { _id, email, role } req.user = user; return next() } catch (err) { return res.status(401).json({ status: 401, message: 'Invalid token' }); } } module.exports = authorizationHandler;
Create user list protected endpoint
In the routes/index.js file, add users endpoint and applied “authorizationHandler” middleware
// ... const User = require('../models/user.model'); const authorizationHandler = require('../middlewares/auth'); module.exports = function (app) { // ... app.get('/users', authorizationHandler, async (req, res, next) => { try { const users = await User.find(); return res.json({ status: 200, message: 'Fetch user list', data: users }) } catch (err) { return res.status(500).json({ status: 500, message: err.message }) } }) // ... }
Let’s test users protected endpoint
Without access_token
Let’s try with access_token
Get tutorial code in Github, Click here