JWT Authentication in NodeJS: Implementing Access and Refresh Tokens
JWT-based authentication in NodeJS uses short-lived access tokens for API requests and long-lived refresh tokens to obtain new access tokens. By storing tokens securely in HTTP-only cookies and handling refreshes automatically, it provides scalable, stateless, and secure user session management.

In modern web applications, authentication plays a crucial role in securing user data and managing user sessions. One of the most popular techniques for implementing secure authentication is by using JWT-based (JSON Web Token) authentication. In this guide, we'll focus on implementing authentication using Access Tokens and Refresh Tokens in NodeJS.
Why JWT?
JWTs are stateless tokens that are digitally signed and provide a secure way to verify user identity without needing to store session data on the server. This makes JWT-based authentication lightweight, scalable, and a great fit for distributed systems like REST APIs and microservices.

Access Tokens vs. Refresh Tokens
- Access Tokens: Short-lived tokens (usually valid for 15 minutes to a few hours) used to authenticate API requests. Once they expire, the user cannot make requests until a new access token is obtained.
- Refresh Tokens: Long-lived tokens (usually valid for days or weeks) used to generate new access tokens. They provide a way to maintain a user session without needing to log in again after the access token expires.
Step-by-Step: JWT Access and Refresh Token Flow
Below is a basic authentication flow for most of the common applications:
1. User Login and Generating Tokens
During the login process, after the user provides valid credentials (like username and password), we issue both an access token and a refresh token.
Here’s an example of how to generate and send both tokens after successful login:
import { Request, Response } from "express";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import UserModel from "../../user/model";
export const loginUser = async (req: Request, res: Response) => {
try {
const { login_id, password } = req.body;
// Find the user by email or username
const user = await UserModel.findOne({
$or: [{ email_address: login_id }, { username: login_id }],
});
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// Verify password
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
return res.status(401).json({ error: "Invalid password" });
}
// Token payload with user information
const tokenDetails = {
userId: user._id,
username: user.username,
user_role: user.user_role,
};
// Generate access token (valid for 1 day)
const accessToken = jwt.sign(tokenDetails, process.env.SECRET_KEY || "", {
expiresIn: "1d",
});
// Generate refresh token (valid for 7 days)
const refreshToken = jwt.sign(
tokenDetails,
process.env.REFRESH_SECRET_KEY || "",
{ expiresIn: "7d" }
);
// Send tokens in response (or cookies)
res.cookie("accessToken", accessToken, {
secure: true,
httpOnly: true,
sameSite: "none",
});
res.cookie("refreshToken", refreshToken, {
secure: true,
httpOnly: true,
sameSite: "none",
});
res.status(200).json({ message: "Login successful", accessToken, refreshToken });
} catch (error) {
console.error("Error during login:", error);
res.status(500).json({ error: "Internal server error" });
}
};
In this code:
- After successfully verifying the user’s credentials, we generate an Access Token that expires in 1 day and a Refresh Token that expires in 7 days.
- Both tokens are sent back in HTTP-only cookies for security or can be included in the response for mobile applications.
If you are wondering why we are using HTTP-only cookies, check this article:

2. Refreshing Access Tokens
Once the access token expires, we don't want to force users to log in again. Instead, the refresh token is used to issue a new access token.
Here’s how we handle that:
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
export const refreshToken = (req: Request, res: Response) => {
try {
// Get refresh token from cookies or headers
const refreshToken = req.cookies.refreshToken || req.headers["refresh-token"];
if (!refreshToken) {
return res.status(400).json({ error: "Refresh token not provided" });
}
// Verify refresh token
jwt.verify(
refreshToken,
process.env.REFRESH_SECRET_KEY || "",
(err: any, decoded: any) => {
if (err) {
return res.status(403).json({ error: "Invalid refresh token" });
}
// Generate new access token
const accessToken = jwt.sign(
{ userId: decoded.userId, username: decoded.username },
process.env.SECRET_KEY || "",
{ expiresIn: "15m" } // Shorter lifespan (15 mins)
);
// Send the new access token in cookies or headers
res.cookie("accessToken", accessToken, {
secure: true,
httpOnly: true,
sameSite: "none",
});
res.status(200).json({ message: "Access token refreshed successfully" });
}
);
} catch (error) {
console.error("Error refreshing access token:", error);
res.status(500).json({ error: "Internal server error" });
}
};
In this refresh token flow:
- The user sends their refresh token via cookies or headers.
- The server verifies the refresh token. If valid, it generates a new access token (with a shorter lifespan, like 15 minutes).
- The new access token is sent to the user in an HTTP-only cookie or JSON response.
3. Token Expiry and Security Considerations
- Access Token Lifespan: Typically, access tokens should have short lifespans (e.g., 15 minutes to 1 day) to minimize the impact of token theft.
- Refresh Token Lifespan: Refresh tokens have longer lifespans (e.g., 7 days or more), but they should be stored securely (preferably in HTTP-only cookies to avoid XSS attacks).
- Token Blacklisting: In some applications, you might want to store refresh tokens in a database to handle logout or revoke refresh tokens if needed.
JWT-based authentication with access and refresh tokens allows for secure, scalable, and stateless user authentication in NodeJS. By issuing short-lived access tokens and long-lived refresh tokens, you can ensure user sessions remain secure while minimizing the need for frequent logins.
This token-based system is an excellent foundation for building modern, secure applications.
That's it for today! See ya 👋