26
JWT Authentication Tutorial with Node JS
- JWT stands for
JSON WEB TOKEN
. - JWTs are great way to implement authentication. It is a standard that defines a compact and self-contained way to securely transmit information between a client and a server as a JSON object.
You can find the entire code here: https://github.com/harsha-sam/jwt-auth-tutorial
Before JWT:
With JWT:
JWT token looks like this:
Reference: https://jwt.io/
-
JWT has three parts separated by dots (.) . JWT will be created with a secret.
-
Header
: First part denotes the hash of header (header generally consists of algorithm used for hashing and type) -
Payload
: Second part will have hash of the payload (payload will contain user id and info, this will be decoded when we verify the JWT. -
Signature
: Third part will contain an hash of (header + '.' + payLoad + secret). This part plays a crucial role in finding whether the user or anyone didn't tamper with the token before sending the request.
-
So, what verifying JWT will do is, it generates the third part of the hash again from the first and second parts of the JWT token sent with the request. If it matches, then we can get the payload.
Even if any payload or data is modified in frontend and sent to backend. JWT verify will fail because third hash going to be different if data is tampered.
The advantage of the JWT is we are storing the user info in token itself. So, it will work across all servers.
Let's dive into the implementation:
Create a new directory and move in to the directory
Now, run:
npm init - y
The above command will initialise the package.json
file
- Let's install all required dependencies:
Run:
npm i express jsonwebtoken dotenv bcrypt cors express
To install
nodemon
as a dev-dependency
npm i —save-dev nodemon
Now, package.json will look something like this:
{
"name": "jwt-auth-tutorial",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
// added devStart command
"devStart": "nodemon server.js",
"start": "node server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.0.1",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1"
},
"devDependencies": {
"nodemon": "^2.0.12"
}
}
- Add
start
anddevStart
commands in your package.json file, if they don't exist.
- Create a file with name
.env
in your project folder where we are going to store all our app secrets đź”’ - Let's add our first secret
APP_PORT
which basically stores the port number on which our server is going to run. - Now, your
.env
file should look something like this
APP_PORT=3000
- Let's create our first endpoint with express in our
index.js
file. ( Create the file, if it doesn't exist)
// index.js
var express = require('express');
require('dotenv').config() // will config the .env file present in the directory
const PORT = process.env.APP_PORT || "8081";
const app = express();
app.get('/', (req, res) => {
res.send("Hello !")
})
app.listen(PORT, () => {
console.log("Listening on port", PORT);
})
- Let's test this endpoint with
Postman
Great, seems like our endpoint is working
- Before creating a login route, let's first create a fake db that stores credentials
// index.js
var express = require('express');
require('dotenv').config() // will config the .env file present in the directory
const db = [
{
username: "Harsha",
password: "hello123"
},
{
username: "Sam",
password: "hello12345"
},
]
const POSTS = [
{
name: "Harsha",
title: "Post 1",
body: "1234"
},
{
name: "Sam",
title: "Post 2",
body: "1234"
},
]
const PORT = process.env.APP_PORT || "8081";
const app = express();
app.get('/', (req, res) => {
res.send("Hello !")
})
app.get("/posts", (req, res) => {
res.status(200).json(POSTS);
})
app.listen(PORT, () => {
console.log("Listening on port", PORT);
})
- Let's create a login endpoint now, which will authenticate the user first and then generates a JWT token.
- To generate a JWT token, we use
jwt.signin(user_info, secret, {expiresIn})
method, we'll pass in user info object and a secret and expires in time, if you want to expire the token. - Secret token can be anything but for best practice let us generate this secret token using crypto node library as shown below
- Add these secrets generated in
.env
file asACCESS_TOKEN_SECRET
andREFRESH_TOKEN_SECRET
Complete Implementation:
var express = require('express');
var bcrypt = require('bcrypt');
var jwt = require('jsonwebtoken');
require('dotenv').config()// will config the .env file present in the directory
let POSTS = [
{
username: "Harsha",
title: "Post 1",
body: "1234"
},
{
username: "Harsha",
title: "Post 2",
body: "1234"
},
{
username: "Harsha",
title: "Post 2",
body: "1234"
},
{
username: "Sm",
title: "Post 2",
body: "1234"
},
{
username: "no",
title: "Post 2",
body: "1234"
},
]
let DB = []
// used to store refresh tokens, as we will manually expire them
let SESSIONS = []
const generateAccessToken = (user) => {
// jwt will make sure to expire this token in 1 hour
return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, {
'expiresIn': '1h'
})
}
const PORT = process.env.APP_PORT || "8081";
const app = express();
app.use(express.json())
// middlewares
const validateToken = async (token, tokenSecret) => {
// returns user info, if the jwt token is valid
return await jwt.verify(token, tokenSecret,
(error, payload) => {
if (error) {
throw (error)
}
return payload
})
}
const validateAccessToken = async (req, res, next) => {
// returns user info, if the jwt token is valid
try {
req.user = await validateToken(req.body['accessToken'], process.env.ACCESS_TOKEN_SECRET)
next();
}
catch (error) {
res.status(401).
json({ error: error.message || 'Invalid access token' })
}
}
const validateRefreshToken = async (req, res, next) => {
try {
req.user = await validateToken(req.body['refreshToken'], process.env.REFRESH_TOKEN_SECRET)
next();
}
catch (error) {
res.status(401).
json({ error: error.message || 'Invalid refresh token' })
}
}
app.get("/posts", validateAccessToken, (req, res) => {
const { username } = req.user;
const userPosts = POSTS.filter((post) => post.username === username)
res.json(userPosts)
})
app.post("/register", async (req, res) => {
const { username, password } = req.body;
let hash = "";
const salt = await bcrypt.genSalt(12);
hash = await bcrypt.hash(password, salt);
DB.push({ username, passwordHash: hash })
console.log(DB);
res.json("Successfully registered")
})
app.post("/login", async (req, res) => {
const { username, password } = req.body;
for (let user of DB) {
// authentication - checking if password is correct
if (user.username === username && await bcrypt.compare(password, user.passwordHash)) {
const accessToken = jwt.sign({ username: user.username }, process.env.ACCESS_TOKEN_SECRET, {
'expiresIn': '1h'
})
// In this implementation, refresh token doesn't have any expiration date and it will be used to generate new access token
const refreshToken = jwt.sign({ username: user.username }, process.env.REFRESH_TOKEN_SECRET)
// We will store refresh token in db and it'll expire when the user logs out
SESSIONS.push(refreshToken);
// sending accesstoken and refresh token in response
res.json({ accessToken, refreshToken });
}
}
})
app.post('/token', validateRefreshToken, (req, res) => {
// generating new access token, once the refresh token is valid and exists in db
const { username } = req.user;
if (SESSIONS.includes(req.body['refreshToken'])) {
res.json({ accessToken: generateAccessToken({ username })})
}
else {
res.status(403).json('Forbidden: refresh token is expired')
}
})
app.delete("/logout", async (req, res) => {
// deleting refresh token from db
SESSIONS = SESSIONS.filter((session) => session != req.body['refreshToken']);
res.sendStatus(204);
})
app.get('/', (req, res) => {
res.send("Hello !")
})
app.listen(PORT, () => {
console.log("Listening on port", PORT);
})
26