Content covered-
What is JWT
Project setup
User creation
User Login
Verifying the Refresh token and generating a new access token using the Refresh token
Verifying the access token for all authorized endpoints
- What is JWT
JWT a.k.a(JSON WEB TOKEN) is token-based authentication through which we can verify the user identity and allow access to the application if a user is found to be registered with valid credentials
JWT consists of Header, Payload, and Signature separated by dots (.) Eg: xxxx.yyyy.zzzz
Header -
The header consists of a token type and algorithm used which in our case is JWT and HS256 respectively
Eg-
{
"alg": "HS256",
"typ": "JWT"
}
Payload -
It consists of user information which is sent while creating token, token creation and expiry time
Eg-
{
"user_id": "6314c5c21e066cf54427dcf2",
"email": "
goofranshaikh@gmail.com
",
"iat": 1665211626,
"exp": 1665211746
}
Signature -
To sign the JWT token it uses header , payload , secret key and algorithm specified in header
Eg-
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
- Project setup
Open the terminal in the project folder and run the below command to install all the dependencies
npm install express bcryptjs dotenv jsonwebtoken mongoose
Create .env file on your root folder and define some environment variable which we will use in the project
.env
API_PORT=3000
MONGO_URI= mongodb://
localhost:27017
TOKEN_KEY="accessToken@123"
REFRESH_TOKEN="refreshToken@123"
- Create a config folder and create database.js file, In our case, we are using mongodb as a database
database.js
const mongoose = require("mongoose");
const { MONGO_URI } = process.env;
exports.connect = () => {
// Connecting to the database
mongoose
.connect(MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log("Successfully connected to database");
})
.catch((error) => {
console.log("database connection failed. exiting now...");
console.error(error);
process.exit(1);
});
};
- Create index.js file in your root folder, here we're going to create our server and listen to it
index.js
const http = require("http");
const app = require("./app");
const server = http.createServer(app);
const { API_PORT } = process.env;
const port = process.env.PORT || API_PORT;
// server listening
server.listen(port, () => {
console.log(`Server running on port ${port}`);
});
- User Creation -
First, we are going to create the file app.js in our root folder , where we going to implement the logic for registering the user and its login process
- Creating the user schema object , which defines the user document object in mongoDB
Create model folder in base folder and add use.js file in it
(Model → user.js)
user.js
const mongoose = require("mongoose"); //importing mongoose
const userSchema = new mongoose.Schema({ //defining schema object to be stored in mongoDB collection
firstName: { type: String, default: null },
lastName: { type: String, default: null },
email: { type: String, unique: true },
password: { type: String }
});
module.exports = mongoose.model("blogUser", userSchema); //Create model with 'blogUser' as collection name inside mongoDB
app.js
require("dotenv").config();
require("./config/database").connect();
const express = require("express");
const bcrypt = require('bcryptjs'); //to encrypt the password of the user while saving to db
const jwt = require('jsonwebtoken')
// importing user model
const User = require("./model/user");
app.use(express.json());
// Register
app.post
("/register", async (req, res) => {
// Our register logic starts here
try {
// Get user input
const { firstName, lastName, email, password } = req.body;
// Validate user input
if (!(email && password && firstName && lastName)) {
res.status(400).send("All input is required");
}
// check if user already exist
// Validate if user exist in our database
const oldUser = await User.findOne({ email });
if (oldUser) {
return res.status(409).send("User Already Exist. Please Login");
}
//Encrypt user password
encryptedPassword = await bcrypt.hash(password, 10);
// Create user in our database
const user = await User.create({
firstName,
lastName,
email: email.toLowerCase(),
password: encryptedPassword,
});
res.status(201).json(user);
} catch (err) {
console.log(err);
}
// Our register logic ends here
});
In the above code when we hit the /register endpoint, first it checks
for all the required inputs,
If not found then we can send a bad request status code with the error message,
But if all the required inputs are provided by the user then we are checking if the user already exists in the database using User.findOne() method
If the user not exist then we are hashing the password using bcrypt library before saving the user to the DB,
User.create() method will insert the user record under the “blogusers” collection name
Let’s try to hit the endpoint on postman
Record inserted in the mongoDB
4. User Login
Write the below code in app.js
app.post
("/login", async (req, res) => {
// Our login logic starts here
try {
// Get user input
const { email, password } = req.body;
// Validate user input
if (!(email && password)) {
res.status(400).send("All input is required");
}
// Validate if user exist in our database
const user = await User.findOne({ email });
if (user && (await
bcrypt.compare
(password, user.password))) {
// Create token
const accesstoken = jwt.sign(
{ user_id: user._id, email },
process.env.TOKEN_KEY,
{
expiresIn: "5m",
}
);
const refreshtoken = jwt.sign(
{ user_id: user._id, email },
process.env.REFRESH_TOKEN,
{
expiresIn: "2h",
}
)
// save user token
const accessToken= accesstoken;
const refreshToken =refreshtoken
// user
res.status(200).json({user,accessToken,refreshToken});
}
res.status(400).send("Invalid Credentials");
} catch (err) {
console.log(err);
}
// Our register logic ends here
});
When the user hits /login route we are checking for the required input if found then we are proceeding forward and checking for the credentials provided by the user are valid or not, By using bcrypt.compare method we are checking payload request password with the password saved in DB for the corresponding user If this is matched then we are generating access token with the expiry of 5 minutes and refresh token with the expiry of 2 hours
In the above example, we have an access token to access the authorized endpoints, without this access token in the request we will not allow the user to access the data and return 401 unauthorized error to the user
Once this token expires in 5 minutes then user will not be able to access the data and gets logged out, if we are going to increase the token expiry time then that might be a security issue, so to solve this issue we have a refresh token, once the access token gets expired then the user will send refresh token, without the user getting logged out we will receive new access token and also security will not get compromised
5.Verifying Refresh token and generating new access token using the Refresh token
app.post
("/refresh",async(req,res)=>{
try{
const {email,refreshToken}=req.body
if(!refreshToken){
return res.status(400).send("refresh Token is required")
}
const isVerified=verifyRefreshToken(email,refreshToken)
if(isVerified){
const accToken= jwt.sign({email},
process.env.TOKEN_KEY,
{expiresIn:"5m"}
)
res.status(200).send({token:accToken})
}
else{
console.log('refresh token not valid')
res.status(401).send('Unauthorized')
}
}
catch(err)
{
console.log(err)
}
})
We are getting user's email and refresh token from the user to generate the access token again, first, we are checking if the user has sent the refresh token in the request body if found then we are verifying the validity of the refresh token in verfyRefreshToken method (we will see this method later in blog), if this method returns TRUE then this is correct refresh token so we will generate the new access token and send it back to the user
Verifying refresh token validity
(middleware/verifyRefresh.js)
In jwt.verify method we are passing a token and secret key, which will decode the refresh token and give us the user info, since we know while we have created the token we have passed user's email, so while decoding we are comparing it with the logged in user email,if both the emails are matched then this is authenticated user and we are returning true in that case which indicates refresh token is valid
const jwt = require("jsonwebtoken");
const config = process.env;
function verifyRefreshToken(email,token){
try{
const decodedToken = jwt.verify(token,config.REFRESH_TOKEN)
return
decodedToken.email
== email? true:false
}
catch(err){
res.send(err)
}
}
module.exports=verifyRefreshToken
Verifying access token validity
(middleware/auth.js)
This is the middleware for protecting the API endpoints,
In this, we are accepting authorization tokens from the user in headers, request body, and query params,if it is not found then we are returning 403 error code which indicates the user is forbidden to access the endpoint.
In case of the token is present we are verifying the token and allowing user to the endpoint if the token is found to be true, else we are returning 401 error code, which indicated the user is unauthorized to access the endpoint
auth.js
const jwt = require("jsonwebtoken");
const config = process.env;
const verifyToken=(req, res, next)=> {
console.log(req.headers["authorization"].split(' ')[1])
const token =
req.body.token || req.query.token || req.headers["authorization"].split(' ')[1];
if (!token) {
return res.status(403).send("A token is required for authentication");
}
try {
const decoded = jwt.verify(token, config.TOKEN_KEY);
console.log(decoded,'decode')
req.user = decoded;
} catch (err) {
return res.status(401).send(err);
}
return next();
};
module.exports = verifyToken;
6. Verifying the access token for all authorized endpoints
Authenticated route
app.post("/welcome" ,verifyToken,(req, res) => {
res.status(200).send(JSON.stringify("Hey, have you found this blog useful..."));
});
module.exports = app;
Hope you have find the blog useful
Below is the github repo of the code
https://github.com/GoofranShaikh/blogpostweb