Understanding Refresh Tokens, Access Tokens, and Cookies in Authentication

MERN STACK DEVELOPER
In modern web applications, authentication and session management are critical components. Two key concepts in this domain are Access Tokens and Refresh Tokens. Alongside, cookies play a vital role in maintaining user sessions. In this blog, we'll explore these concepts, how they work together in a login/logout flow, and how to implement them effectively in your applications. I'll also walk you through some code snippets to simplify the explanation.
1. What are Access Tokens?
An Access Token is a short-lived token that grants access to resources or APIs. It contains user information and permissions (claims) and is usually encoded in a JWT (JSON Web Token) format. Access tokens are typically stored in memory or local storage and are included in the Authorization header of API requests.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NmRhNjNjYjhkNGJhZTEzMmI4ZGI1MmUiLCJlbWFpbCI6InBpY2hrYXRlMTA4QGdtYWlsLmNvbSIsImZ1bGxuYW1lIjoiUGljaGthdGUiLCJ1c2VybmFtZSI6InBpY2hrYXRlMTIiLCJpYXQiOjE3MjU1OTAzODEsImV4cCI6MTcyNTY3Njc4MX0.ucPlcK5bhgQpKiohOUDylhfe1r_GPJyiWugAJRbLkg8
2. What are Refresh Tokens?
A Refresh Token is a longer-lived token used to obtain a new access token without requiring the user to re-authenticate. It's usually stored more securely, such as in an HTTP-only cookie, and is exchanged for a new access token when the old one expires.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NmRhNjNjYjhkNGJhZTEzMmI4ZGI1MmUiLCJpYXQiOjE3MjU1OTAzODEsImV4cCI6MTcyNjQ1NDM4MX0.p3Q_A3NoUiSoZRlG4ee-0TQlK76QrlJ3Ipcl1UvnMdg
3.Understanding Cookies
Cookies are small pieces of data stored on the user's browser. They can be used to store session information, such as a refresh token. When set as HTTP-only, cookies can't be accessed via JavaScript, enhancing security.
Why Use Cookies?
Security: HTTP-only cookies protect sensitive information from XSS attacks.
Convenience: Automatically sent with every request to the same domain.
4. The Authentication Flow
Let's break down the authentication flow using access tokens, refresh tokens, and cookies:
Registration
The user registers by providing necessary details (e.g., username, email, password). These details are stored in the database, and no tokens are issued at this stage.
Algorithm for registerUser Function:
Extract User Data from Request:
- Retrieve the
username,fullname,email, andpasswordfrom the request body.
- Retrieve the
Validate Input Fields:
Check if any of the fields (
username,fullname,email,password) are empty or consist of only whitespace.If any field is empty, throw a
400error indicating that all fields are required.
Check if the User Already Exists:
Query the database to check if a user with the same
usernameoremailalready exists.If a user is found, throw a
409error indicating that a user with the same email or username already exists.
Check for Avatar Image:
Retrieve the local path of the avatar image from the uploaded files.
If the avatar image is not provided, throw a
400An error indicating that the avatar image is required is also missing.
Check for Cover Image (Optional):
Retrieve the local path of the cover image from the uploaded files if it exists.
If a cover image is provided, store its path.
Upload Images to Cloudinary:
Upload the avatar image to Cloudinary and retrieve the URL of the uploaded image.
Upload the cover image to Cloudinary if it exists and retrieve the URL. If not, set the cover image URL to an empty string.
If the avatar image fails to upload, throw a
400an error indicating that the avatar image is required.
Create the User in the Database:
Create a new user in the database using the provided
fullname,avatar,coverImage,email,password, andusername.Convert the
usernameto lowercase before storing it.
Fetch the Created User:
- Query the database to fetch the newly created user by their
_id, excluding thepasswordandrefreshTokenfields from the response.
- Query the database to fetch the newly created user by their
Check if User Creation was Successful:
- If the user is not found (which would be unexpected), throw an error indicating that something went wrong during user creation.
Send Success Response:
- If the user was successfully created and retrieved, send a
201status response with the user data and a success message.
- If the user was successfully created and retrieved, send a
// Function for registering the user
const registerUser = asyncHandler(async (req, res) => {
//Get the data from the user
const {username,fullname,email,password} = req.body
//check for empty field
if (
[username,fullname,email,password].some((field)=>
field?.trim() === "")
) {
throw new ApiError(400,"All fields are required.")
}
//check if user already exist
const existedUser = await User.findOne({
$or:[{ username },{ email }]
})
if (existedUser) {
throw new ApiError(409,"User with same email or username already exist.")
}
//upload avatar and coverImage and also check for avatar image
const avatarLocalPath = req.files?.avatar[0]?.path;
// const coverImageLocalPath = req.files?.coverImage[0]?.path;
let coverImageLocalPath;
if (req.files?.coverImage?.length > 0) {
coverImageLocalPath = req.files.coverImage[0].path;
}
if (!avatarLocalPath) {
throw new ApiError(400,"Avatar image is required")
}
//upload avatar and coverImage on cloudinary
const avatar = await uploadOnCloudinary(avatarLocalPath);
const coverImage = await uploadOnCloudinary(coverImageLocalPath);
if(!avatar){
throw new ApiError(400,"Avatar image is required")
}
//create user
const user = await User.create({
fullname,
avatar: avatar.url,
coverImage:coverImage?.url || "",
email,
password,
username: username.toLowerCase(),
})
//check if the user is present by its id
const createdUser = await User.findById(user._id).select(
"-password -refreshToken"
)
if (!createdUser) {
throw new ApiError("Something when wrong while creating the User.")
}
//send the response
return res.status(201).json(
new ApiResponse(200,createdUser,"Successfully Registered the user")
)
});
Login User
We will understand how exactly the Access Tokens and Refresh Tokens Are Generated and Used to Log In.
Receive User Credentials:
Extract
email,username, andpasswordfrom the request body.Example:
const { email, username, password } = req.body;
Validate Input:
Check if either
usernameoremailis provided. If neither is provided, throw an error with a400status code and the message "username or email is required."Example:
if (!username && !email) { throw new ApiError(400, "username or email is required"); }
Find the User:
Search the database for a user whose
usernameoremailmatches the provided input.Example:
const user = await User.findOne({ $or: [{ username }, { email }] });If the user is not found, throw an error with a
404status code and the message "User does not exist."
Validate Password:
Check if the provided password is correct by comparing it with the stored password using the
isPasswordCorrectmethod.Example:
const isPasswordValid = await user.isPasswordCorrect(password);If the password is invalid, throw an error with a
401status code and the message "Invalid user credentials."
Generate Access and Refresh Tokens:
If the password is valid, generate an
accessTokenand arefreshTokenusing thegenerateAccessAndRefreshTokenfunction.// Define Function to generate the refresh and access token const generateAccessAndRefreshToken = async (userId) => { try { const user = await User.findById(userId); const accessToken = await user.generateAccessToken(); const refreshToken = await user.generateRefreshToken(); console.log("Generated Refresh Token:", refreshToken); user.refreshToken = refreshToken; await user.save({ validateBeforeSave: false }); // Log the user object after saving to confirm refreshToken is saved const updatedUser = await User.findById(userId); console.log("Updated User with Refresh Token:", updatedUser); return { accessToken, refreshToken }; } catch (error) { throw new ApiError(500, "Something went wrong while generating the tokens!"); } };Example:
const { accessToken, refreshToken } = await generateAccessAndRefreshToken(user._id);
Retrieve Logged-In User Data:
Fetch the user’s data from the database without including the
passwordandrefreshTokenfields.Example:
const loggedInUser = await User.findById(user._id).select("-password -refreshToken");
Configure Cookie Options:
Set cookie options such as
httpOnly(to prevent client-side scripts from accessing the cookies) andsecure(to ensure cookies are only sent over HTTPS in production).Example:
const options = { httpOnly: true, secure: process.env.NODE_ENV === 'production' };
Send Cookies and Response:
Send the
accessTokenandrefreshTokenas cookies in the response.Include the logged-in user data,
accessToken, andrefreshTokenin the response body.Return a
200status code with a success message indicating the user has logged in successfully.
Example:
return res
.status(200)
.cookie("accessToken", accessToken, options)
.cookie("refreshToken", refreshToken, options)
.json(
new ApiResponse(
200,
{
user: loggedInUser, accessToken, refreshToken
},
"User logged In Successfully"
)
);
Token Refresh
When the access token expires, the client sends the refresh token (stored in the cookie) to the server. The server verifies it and issues a new access token.
Algorithm for refreshAccessToken Function
Extract the Refresh Token:
Attempt to retrieve the
refreshTokenfrom the cookies or the request body.Example:
const incomingRefreshToken = req.cookie.refreshToken || req.body.refreshToken;
Validate the Presence of the Refresh Token:
If the
incomingRefreshTokenis not provided, throw an error with a401status code and the message "Refresh token is required."Example:
if (!incomingRefreshToken) { throw new ApiError(401, "Refresh token is required"); }
Verify the Refresh Token:
Use the
jwt.verifymethod to decode and verify theincomingRefreshTokenusing the secret key (REFRESH_TOKEN_SECRET).Example:
const decodedToken = jwt.verify( incomingRefreshToken, process.env.REFRESH_TOKEN_SECRET, );
Find the User Associated with the Token:
Use the decoded token’s
_idto find the user in the database.Example:
const user = await User.findById(decodedToken?._id);If the user is not found, throw an error with a
401status code and the message "Invalid refresh token or user."
Validate the Refresh Token Against the User:
Compare the
incomingRefreshTokenwith theuser.refreshTokenstored in the database.Example:
if (incomingRefreshToken !== user.refreshToken) { throw new ApiError(401, "Refresh Token is invalid or expired or used"); }
Set-Cookie Options:
Define the cookie options with
httpOnlyset totrueto prevent client-side access andsecureset totrueto ensure the cookies are sent over HTTPS.Example:
const options = { httpOnly: true, secure: true };
Generate New Tokens:
Generate a new
accessTokenandnewRefreshTokenby calling thegenerateAccessAndRefreshTokenfunction using the user's_id.Example:
const { accessToken, newRefreshToken } = await generateAccessAndRefreshToken(user._id);
Send the New Tokens as Cookies and in the Response:
Set the new
accessTokenandnewRefreshTokenas cookies in the response.Send a JSON response containing the new
accessTokenandnewRefreshTokenwith a200status code and the message "Access token refreshed successfully."Example:
return res .status(200) .cookie("accessToken", accessToken, options) .cookie("refreshToken", newRefreshToken, options) .json( new ApiResponse( 200, { accessToken, refreshToken: newRefreshToken }, "Access token refreshed successfully" ) );
Handle Errors:
If an error occurs during the process, catch the error and throw a new error with a
401status code and a message indicating the issue with the refresh token.Example:
catch (error) { throw new ApiError(401, error?.message || "Invalid refresh Token"); }
Logout
To log out, the client sends a logout request. The server then invalidates the refresh token, effectively logging the user out. The cookie is also cleared on the client side.
Algorithm:
The client sends a logout request to the server.
The server invalidates the refresh token in the database.
The server clears t/he cookie on the client side.
5. Conclusion
In this blog, we’ve delved into the critical role that access tokens, refresh tokens, and cookies play in modern web authentication. By understanding these concepts, you can build more secure and user-friendly applications that ensure seamless experiences for your users.
Key Takeaways:
Access Tokens
Refresh Tokens
Cookies
By implementing the strategies discussed, such as securely handling and rotating tokens, you can enhance the security of your authentication flow. Additionally, the provided code snippets offer practical guidance on how to integrate these concepts into your projects.
What’s Next? As you build and secure your applications, consider exploring advanced topics like OAuth 2.0, JWT best practices, and secure cookie handling. These areas will deepen your understanding and enable you to implement even more robust authentication mechanisms.
Thank you for following along, and happy coding!



