Skip to main content

Command Palette

Search for a command to run...

Understanding Refresh Tokens, Access Tokens, and Cookies in Authentication

Updated
9 min read
Understanding Refresh Tokens, Access Tokens, and Cookies in Authentication
P

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:

  1. Extract User Data from Request:

    • Retrieve the username, fullname, email, and password from the request body.
  2. 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 400 error indicating that all fields are required.

  3. Check if the User Already Exists:

    • Query the database to check if a user with the same username or email already exists.

    • If a user is found, throw a 409 error indicating that a user with the same email or username already exists.

  4. 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 400 An error indicating that the avatar image is required is also missing.

  5. 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.

  6. 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 400 an error indicating that the avatar image is required.

  7. Create the User in the Database:

    • Create a new user in the database using the provided fullname, avatar, coverImage, email, password, and username.

    • Convert the username to lowercase before storing it.

  8. Fetch the Created User:

    • Query the database to fetch the newly created user by their _id, excluding the password and refreshToken fields from the response.
  9. 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.
  10. Send Success Response:

    • If the user was successfully created and retrieved, send a 201 status response with the user data and a success message.
// 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.

  1. Receive User Credentials:

    • Extract email, username, and password from the request body.

    • Example:

        const { email, username, password } = req.body;
      
  2. Validate Input:

    • Check if either username or email is provided. If neither is provided, throw an error with a 400 status code and the message "username or email is required."

    • Example:

        if (!username && !email) {
            throw new ApiError(400, "username or email is required");
        }
      
  3. Find the User:

    • Search the database for a user whose username or email matches the provided input.

    • Example:

        const user = await User.findOne({
            $or: [{ username }, { email }]
        });
      
    • If the user is not found, throw an error with a 404 status code and the message "User does not exist."

  4. Validate Password:

    • Check if the provided password is correct by comparing it with the stored password using the isPasswordCorrect method.

    • Example:

        const isPasswordValid = await user.isPasswordCorrect(password);
      
    • If the password is invalid, throw an error with a 401 status code and the message "Invalid user credentials."

  5. Generate Access and Refresh Tokens:

    • If the password is valid, generate an accessToken and a refreshToken using the generateAccessAndRefreshToken function.

    •       // 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);
      
  6. Retrieve Logged-In User Data:

    • Fetch the user’s data from the database without including the password and refreshToken fields.

    • Example:

        const loggedInUser = await User.findById(user._id).select("-password -refreshToken");
      
  7. Configure Cookie Options:

    • Set cookie options such as httpOnly (to prevent client-side scripts from accessing the cookies) and secure (to ensure cookies are only sent over HTTPS in production).

    • Example:

        const options = {
            httpOnly: true,
            secure: process.env.NODE_ENV === 'production'
        };
      
  8. Send Cookies and Response:

    • Send the accessToken and refreshToken as cookies in the response.

    • Include the logged-in user data, accessToken, and refreshToken in the response body.

    • Return a 200 status 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

  1. Extract the Refresh Token:

    • Attempt to retrieve the refreshToken from the cookies or the request body.

    • Example:

        const incomingRefreshToken = req.cookie.refreshToken || req.body.refreshToken;
      
  2. Validate the Presence of the Refresh Token:

    • If the incomingRefreshToken is not provided, throw an error with a 401 status code and the message "Refresh token is required."

    • Example:

        if (!incomingRefreshToken) {
            throw new ApiError(401, "Refresh token is required");
        }
      
  3. Verify the Refresh Token:

    • Use the jwt.verify method to decode and verify the incomingRefreshToken using the secret key (REFRESH_TOKEN_SECRET).

    • Example:

        const decodedToken = jwt.verify(
            incomingRefreshToken,
            process.env.REFRESH_TOKEN_SECRET,
        );
      
  4. Find the User Associated with the Token:

    • Use the decoded token’s _id to 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 401 status code and the message "Invalid refresh token or user."

  5. Validate the Refresh Token Against the User:

    • Compare the incomingRefreshToken with the user.refreshToken stored in the database.

    • Example:

        if (incomingRefreshToken !== user.refreshToken) {
            throw new ApiError(401, "Refresh Token is invalid or expired or used");
        }
      
  6. Set-Cookie Options:

    • Define the cookie options with httpOnly set to true to prevent client-side access and secure set to true to ensure the cookies are sent over HTTPS.

    • Example:

        const options = {
            httpOnly: true,
            secure: true
        };
      
  7. Generate New Tokens:

    • Generate a new accessToken and newRefreshToken by calling the generateAccessAndRefreshToken function using the user's _id.

    • Example:

        const { accessToken, newRefreshToken } = await generateAccessAndRefreshToken(user._id);
      
  8. Send the New Tokens as Cookies and in the Response:

    • Set the new accessToken and newRefreshToken as cookies in the response.

    • Send a JSON response containing the new accessToken and newRefreshToken with a 200 status 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"
                )
            );
      
  9. Handle Errors:

    • If an error occurs during the process, catch the error and throw a new error with a 401 status 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:

  1. The client sends a logout request to the server.

  2. The server invalidates the refresh token in the database.

  3. 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!

More from this blog

Untitled Publication

8 posts