All about JSON Web Token(JWT) And Re-Authentication


JWT (JSON Web Token) is a compact, self-contained way of securely transmitting information between parties as a JSON object. It's signed using a secret (with HMAC) or a public/private key pair (with RSA or ECDSA), ensuring the integrity and authenticity of the data.
A typical JWT consists of three parts:
Header – contains token type and signing algorithm.
Payload – contains the user information and claims.
Signature – the hashed combination of the header, payload, and secret.
Authentication Using JWT :
By using Authentication, It verifies user’s identity in a MERN stack Application.
User Registration: The backend hashes the user's password (using ‘bcrypt’) and stores it in MongoDB.
User Login: The backend compares the entered password with the stored hash. If it matches, a JWT token is generated and sent to the client.
Here We use two types of JWT token, Access and Refresh Token, The Refresh Token is basically stored in the DB. When the entered password matched with the Hashed Password, then The Access and Refresh Tokens are generated.
Token Storage: The client stores the token (typically in ‘localStorage’ or ‘HttpOnly Cookies’).
Authenticated Requests: For protected API requests, the token is sent in the
Authorization
header (Bearer <token>
).Token Verification: The backend middleware verifies the token and grants or denies access.
Deep Dive to Refresh and Access Token and Re-Authentication:
Access Token : An Access Token is a short-lived token (usually JWT) issued to a user after successful login. It is used to access protected resources (e.g., APIs).
Basically the access token is short-lived and also it has more content in payload than the Refresh Token.
userSchema.methods.generateAccessToken = function(){
return jwt.sign(
{
_id: this._id,
email: this.email,
username: this.username,
fullName: this.fullName
},
process.env.ACCESS_TOKEN_SECRET,
{
expiresIn: process.env.ACCESS_TOKEN_EXPIRY // These are stored in .env file.
}
)
}
Refresh Token: A Refresh Token is a long-lived token used to generate new Access Tokens without requiring the user to log in again.
userSchema.methods.generateRefreshToken = function(){
return jwt.sign(
{
_id: this._id, // Only Id is used as the payload of the JWT token.
},
process.env.REFRESH_TOKEN_SECRET,
{
expiresIn: process.env.REFRESH_TOKEN_EXPIRY
}
)
}
The Tokens are stored in the cookies by using a middleware called ‘cookie-parser’ in the main.js file. We can fetch the token from it after the user login, But for android development it is not possible, no cookie concept is there, so we need to use the req.header("Authorization")?.replace("Bearer ", "") to fetch the tokens.
The Refresh Token is stored in the Data-Base, So In this case When the Access token is expired, we can recreate the access token or just matching the user refresh token to the Database refresh token.
But How can you get the user data?
Here comes the Authentication Middleware, which basically verify the JWT token of the request data using the ‘JWT secret code’ . If it matches The user data can be fetched using the stored payload of the Decoded Token (which is the verified one using the secret code and the JWT token).
import { ApiError } from "../utils/ApiError.js";
import { asyncHandler } from "../utils/asyncHandler.js";
import jwt from "jsonwebtoken"
import { User } from "../models/user.model.js";
export const verifyJWT = asyncHandler(async(req, _, next) => {
try {
const token = req.cookies?.accessToken || req.header("Authorization")?.replace("Bearer ", "")
// console.log(token);
if (!token) {
throw new ApiError(401, "Unauthorized request")
}
const decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)
// getting id from the decoded token payload and it is the user id.
const user = await User.findById(decodedToken?._id).select("-password -refreshToken")
if (!user) {
throw new ApiError(401, "Invalid Access Token")
}
req.user = user; // The user data is stored in req.user.
next()
} catch (error) {
throw new ApiError(401, error?.message || "Invalid access token")
}
})
Now We get the User data in “ req.user” section. I can use this data in any time to write my other controllers and sent the middleware name between the client (route) and response.
Steps to Re-Authenticate the User :
Now We need to Re-Authenticate the User, using the Stored Refresh Token in Database.
Firstly We goes to the cookies or the req. header section that “ Hey I want the stored refresh-token” , Then it gives me the refresh token.
Then We need to decode the Refresh Token, using the JWT secret code.
The the decoded token needed to check that is it the same refresh token which is stored in database.
If Yes then we need to fetch the User data using the payload data of the user in the refresh token. If No, Then return some http error.
After getting the user data, we re-generate the access and refresh token and sends them into cookies and the new refresh token is stored in the refresh token section in the database.
const refreshAccessToken = asyncHandler(async (req, res) => {
const incomingRefreshToken = req.cookies.refreshToken || req.body.refreshToken
if (!incomingRefreshToken) {
throw new ApiError(401, "unauthorized request")
}
try {
const decodedToken = jwt.verify(
incomingRefreshToken,
process.env.REFRESH_TOKEN_SECRET
)
const user = await User.findById(decodedToken?._id)
if (!user) {
throw new ApiError(401, "Invalid refresh token")
}
if (incomingRefreshToken !== user?.refreshToken) {
throw new ApiError(401, "Refresh token is expired or used")
}
const options = {
httpOnly: true,
secure: true
}
const {accessToken, newRefreshToken} = await generateAccessAndRefereshTokens(user._id)
return res
.status(200)
.cookie("accessToken", accessToken, options)
.cookie("refreshToken", newRefreshToken, options)
.json(
new ApiResponse(
200,
{accessToken, refreshToken: newRefreshToken},
"Access token refreshed"
)
)
} catch (error) {
throw new ApiError(401, error?.message || "Invalid refresh token")
}
})
// These are the code for Login and Logout User.
const loginUser = asyncHandler(async (req, res) =>{
// req body -> data
// username or email
//find the user
//password check
//access and referesh token
//send cookie
const {email, username, password} = req.body
console.log(email);
if (!username && !email) {
throw new ApiError(400, "username or email is required")
}
// Here is an alternative of above code based on logic discussed in video:
// if (!(username || email)) {
// throw new ApiError(400, "username or email is required")
// }
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
const {accessToken, refreshToken} = await generateAccessAndRefereshTokens(user._id)
const loggedInUser = await User.findById(user._id).select("-password -refreshToken")
const options = {
httpOnly: true,
secure: true
}
return res
.status(200)
.cookie("accessToken", accessToken, options)
.cookie("refreshToken", refreshToken, options)
.json(
new ApiResponse(
200,
{
user: loggedInUser, accessToken, refreshToken
},
"User logged In Successfully"
)
)
})
const logoutUser = asyncHandler(async(req, res) => {
await User.findByIdAndUpdate(
req.user._id,
{
$unset: {
refreshToken: 1 // this removes the field from document
}
},
{
new: true
}
)
const options = {
httpOnly: true,
secure: true
}
return res
.status(200)
.clearCookie("accessToken", options)
.clearCookie("refreshToken", options)
.json(new ApiResponse(200, {}, "User logged Out"))
})
The Refresh Token is mainly Used to Re-Authenticate the User.
Using access and refresh tokens together balances security and usability. Short-lived access tokens minimize risk, while refresh tokens enable seamless user experience without repeated logins. Re-authentication is only needed when both expire, keeping your MERN application both secure and user-friendly.
Subscribe to my newsletter
Read articles from Pratip Modak directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
