Access Token & Refresh Token in User Session Management

Access Token & Refresh Token in User Session Management

ยท

4 min read

Access tokens and refresh tokens are both used in authentication and authorization processes to help balance security and user experience. Their main purpose is to allow users to stay logged in, so they don't have to log in again and again.

Access Token: These digital assets allow users to access resources without repeated login requests. They are usually short-lived, lasting a few minutes or hours, and may have an expiration date embedded in them. Access tokens can be used for passwordless authentication and accessing shared resources. They contain data, such as information about the user, permissions, and timeframes, that passes from a server to a user's device.

Refresh Token: These tokens are typically long-lived and allow clients to request new access tokens when the current ones expire. The refresh token should be stored in the user database while the user logs in.

How it works

When the user logs in, the backend server stores the user data in the database and assigns two types of tokens:

  • Access Token

  • Refresh Token

Then while returning the response to the frontend, the server should set these 2 tokens in the cookies so that the frontend can access them to manage user sessions.

Suppose there is an ongoing user session. When the session ends, the frontend hits an endpoint and sends a request to the authorization server with the refresh token. The server matches the token coming from the frontend and the token stored in the current user's object in the database and if they match then server again assigns the new access token & refresh token and returns them with responses in the cookies so the user can again start their new session without login.

Using Access and Refresh Tokens

  1. Install the JSON web token library

     npm i jsonwebtoken
    
  2. Write methods to generate tokens within the user model

     import mongoose, { Schema } from "mongoose";
     import jwt from "jsonwebtoken";
    
     const userSchema = new Schema({
     // user data
     })
    
     // generating access 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,
         }
       );
     };
    
     // generataing refresh token
     userSchema.methods.generateRefreshToken = function () {
       return jwt.sign(
         {
           _id: this._id,
         },
         process.env.REFRESH_TOKEN_SECRET,
         {
           expiresIn: process.env.REFRESH_TOKEN_EXPIRY,
         }
       );
     };
    
     export const User = mongoose.model("User", userSchema);controller
    
  3. Now get these tokens in the login controller. When the user logs in, assign them to the user, and return with the cookies.

     // in user.controller.js
    
     import { User } from "../models/user.model.js";
    
     // function for token generation
     const getTokens = async (userId) => {
       try {
         const user = await User.findById(userId);
         const accessToken = await user.generateAccessToken();
         const refreshToken = await user.generateRefreshToken();
         user.refreshToken = refreshToken;
         await user.save({ validateBeforeSave: false });
         return { accessToken, refreshToken };
       } catch {
         throw new Error(500, "Something went wrong while generating tokens");
       }
     };
    
     // controller for user login
     const loginUser = async (req, res) => {
       const { username, email, password } = req.body;
       const user = await User.findOne({
         $or: [{ username }, { email }],
       });
       const { accessToken, refreshToken } = await getTokens(user._id);
       const loggedInUser = await User.findById(user._id).select(
         "-password -refreshToken"
       );
    
       // defining the options for the cookie
       const options = {
         httpOnly: true,
         secure: true,
       };
    
       // returning the response with cookies
       return res
         .status(200)
         .cookie("refreshToken", refreshToken) // using cookie middleware to store refresh token
         .cookie("accessToken", accessToken, options)
         .json({
           data: {
             user: loggedInUser,
             accessToken,
             refreshToken,
           },
           message: "User logged in successfully",
         });
     };
    
     export { loginUser };
    

Managing sessions with tokens

Now suppose after login, the user session ended in 1 hour; now the frontend will hit the endpoint /refresh-token with refresh token payload to regenerate the access token for the new session.

// in user.controller.js

import { User } from "../models/user.model.js";
import jwt from "jsonwebtoken";

// function for token generation
const getTokens = async (userId) => {
  try {
    const user = await User.findById(userId);
    const accessToken = await user.generateAccessToken();
    const refreshToken = await user.generateRefreshToken();
    user.refreshToken = refreshToken;
    await user.save({ validateBeforeSave: false });
    return { accessToken, refreshToken };
  } catch {
    throw new Error(500, "Something went wrong while generating tokens");
  }
};
const refreshAccessToken = async (req, res) => {
    // extract refresh token from the request
    const incomingRefreshToken = req.cookies.refreshToken || req.body.refreshToken

    if (!incomingRefreshToken) {
        throw new Error(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 Error(401, "Invalid refresh token")
        }

        if (incomingRefreshToken !== user?.refreshToken) {
            throw new Error(401, "Refresh token is expired or used")
        }

        const options = {
            httpOnly: true,
            secure: true
        }

        const {accessToken, newRefreshToken} = await getTokens(user._id)

        return res
        .status(200)
        .cookie("accessToken", accessToken, options)
        .cookie("refreshToken", newRefreshToken, options)
        .json(
            data:{accessToken, refreshToken: newRefreshToken},
            message: "Access token refreshed"
            )
        )
    } catch (error) {
        throw new ApiError(401, error?.message || "Invalid refresh token")
    }

}

Now the user's access token and refresh tokens are regenerated and the user will be able to continue working in a new session without needing to log in again.

So in short, the Access tokens provide temporary access to resources, while refresh tokens allow clients to request new access tokens when the current ones expire.

Thanks for reading! ๐Ÿ‘‹

ย