Cookie:
A small piece of data stored by the browser, sent back with every request to the server. It helps the server remember information about the client, like login status.
- When you visit a website, the server can give your browser a cookie.
- Your browser stores it and automatically sends it back to the server every time you visit that site again.
- Cookies are often used to remember you, like keeping you logged in or storing preferences.
Access Token:
A short-lived token sent by the server to the client after login. The client uses it to prove authentication when making API requests. It usually expires quickly for security reasons.
- When you log in, the server gives you this token.
- You send it with every request to protected routes (like “get user profile” or “update post”).
- It usually expires quickly (like in 15 minutes) for security.
Refresh Token:
A longer-lived token stored safely (usually in a cookie with httpOnly
) used to get a new access token when the old one expires. It helps keep the user logged in without asking for credentials repeatedly.
- It’s longer-lasting than the access token.
- It’s stored securely in a cookie (usually
httpOnly
). - When your access token expires, your app sends the refresh token to the server to get a new access token without logging in again.
Why Is the Refresh Token Saved in a Cookie?
The refresh token is saved in a cookie—specifically an HTTP-only cookie—to enhance security and protect against XSS (Cross-Site Scripting) attacks.
Here’s why:
HTTP-only flag
When a cookie is set with httpOnly: true, it cannot be accessed by JavaScript running in the browser (e.g., document.cookie).
This prevents attackers from stealing the refresh token even if they manage to inject malicious scripts.Automatically sent with requests
Cookies are automatically included in HTTP requests by the browser—no need to manually attach the refresh token on the frontend.
This makes the refresh flow seamless and consistent.Reduces surface area for attacks
If you store the refresh token in localStorage or sessionStorage, it's accessible to JavaScript—making it easier for XSS attacks to extract it.
Cookies (with proper flags like httpOnly, secure, and sameSite) reduce that risk.Built-in browser handling
The browser takes care of sending the cookie only to the origin you specify (sameSite, domain, path), giving you finer control over exposure.
Example Setup
src\app.js
//...
import cookieParser from 'cookie-parser';
import cors from 'cors';
app.use(cookieParser());
app.use(
cors({
origin: ['http://localhost:5173'],
credentials: true, // ✅ this is mandatory for cookies
}),
);
//...
src\app\modules\Auth\auth.route.js
import { AuthControllers } from './auth.controller';
//...
router.post('/login', AuthControllers.loginUser);
router.post('/refresh-token', AuthControllers.refreshToken);
//..
src\app\modules\Auth\auth.controller.js
import { AuthServices } from './auth.service';
//..
const loginUser = catchAsync(async (req, res) => {
const result = await AuthServices.loginUser(req.body);
const { refreshToken, accessToken } = result;
res.cookie('refreshToken', refreshToken, {
secure: config.NODE_ENV === 'production',
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000, // cookie lasts 7 days
});
sendResponse(res, {
statusCode: httpStatus.OK,
success: true,
message: 'User is logged in succesfully!',
data: {
accessToken,
},
});
});
const refreshToken = catchAsync(async (req, res) => {
const { refreshToken } = req.cookies;
const result = await AuthServices.refreshToken(refreshToken); //verifies the current refresh token. generates and returns new access token upon succesful verification
sendResponse(res, {
statusCode: httpStatus.OK,
success: true,
message: 'Access token is retrieved succesfully!',
data: result,
});
});
export const AuthControllers = {
loginUser,
changePassword,
refreshToken,
};
//..
Explanation of the provided example in simple terms
-
Middleware setup (
src\app.js
)
app.use(cookieParser());
app.use(
cors({
origin: ['http://localhost:5173'],
credentials: true, // ✅ this is mandatory for cookies
}),
);
-
cookieParser()
lets your server read cookies sent by the browser. -
cors()
allows your frontend (http://localhost:5173
) to talk to this backend, andcredentials: true
lets cookies be sent in cross-origin requests, which is necessary for storing refresh tokens.
-
Routes (
auth.route.js
)
router.post('/login', AuthControllers.loginUser);
router.post('/refresh-token', AuthControllers.refreshToken);
-
/login
route handles user login. -
/refresh-token
route is called when the client wants a new access token using the refresh token.
-
Login controller (
auth.controller.js
)
const loginUser = catchAsync(async (req, res) => {
//takes login credential (email, password), verifies it and generates refreshToken and accessToken
const result = await AuthServices.loginUser(req.body);
const { refreshToken, accessToken } = result;
res.cookie('refreshToken', refreshToken, {
secure: config.NODE_ENV === 'production',
httpOnly: true,
maxAge: 7 * 24 * 60 * 60 * 1000, // cookie lasts 7 days
});
sendResponse(res, {
statusCode: httpStatus.OK,
success: true,
message: 'User is logged in succesfully!',
data: {
accessToken,
},
});
});
- When a user logs in, your service returns two tokens: an
accessToken
and arefreshToken
. - The refresh token is saved in an HTTP-only cookie (
res.cookie(...)
), so the browser stores it but JavaScript cannot read it (safer). - The access token is sent back in the JSON response, so the frontend can use it to authenticate API calls.
- Refresh token controller
const refreshToken = catchAsync(async (req, res) => {
const { refreshToken } = req.cookies;
// uses jwt.verify for the verification. expample - jwt.verify( refreshToken, config.jwt_refresh_secret )
// verifies the current refresh token. generates and returns new access token upon successful verification
const result = await AuthServices.refreshToken(refreshToken);
sendResponse(res, {
statusCode: httpStatus.OK,
success: true,
message: 'Access token is retrieved succesfully!',
data: result,
});
});
- When the frontend detects the access token expired, it calls
/refresh-token
. - The server reads the refresh token from the cookie (
req.cookies
). - It verifies the refresh token and issues a new access token.
- The new access token is sent back to the client to continue making authenticated requests.
Summary:
Cookies store the refresh token securely. The frontend uses the access token for requests. When access token expires, frontend calls /refresh-token
endpoint, sending the refresh token automatically via cookie, and gets a new access token without asking the user to log in again.
AuthService example code
import jwt from 'jsonwebtoken';
import { User } from '../user/user.model';
// util function to generate tokens
const createToken = (jwtPayload, secret, expiresIn) => {
return jwt.sign(jwtPayload, secret, { expiresIn });
};
const loginUser = async (payload) => {
const user = await User.findById(payload.id);
const jwtPayload = {
userId: user.id,
role: user.role,
};
const accessToken = createToken(
jwtPayload,
config.jwt_access_secret,
config.jwt_access_expires_in
);
const refreshToken = createToken(
jwtPayload,
config.jwt_refresh_secret,
config.jwt_refresh_expires_in
);
return {
accessToken,
refreshToken,
};
};
const refreshToken = async (token) => {
const decoded = jwt.verify(token, config.jwt_refresh_secret);
const jwtPayload = {
userId: decoded.userId,
role: decoded.role,
};
const accessToken = createToken(
jwtPayload,
config.jwt_access_secret,
config.jwt_access_expires_in
);
return {
accessToken,
};
};
export const AuthServices = {
loginUser,
refreshToken,
};
Top comments (0)