DEV Community

Cover image for Understanding Access Tokens and Refresh Tokens in a MERN App
Shriyansh Lohia
Shriyansh Lohia

Posted on

Understanding Access Tokens and Refresh Tokens in a MERN App

Hello fellow Devs, I hope you are doing great.
This is my first article on Dev.to, but I have written some on Medium.

Let's start with what this article is not:

  • It is not a coding tutorial.
  • You will see very little code, so sorry.

This article is about understanding the working of access tokens and refresh tokens in a MERN app, how they work, and how we use them.


The Article Contains

  • What is JWT?
  • What are the access and refresh tokens?
  • Why do we need them?
  • What if we don't use them?
  • Why do we need 2 tokens?
  • How do they work?
  • How does token creation work?
  • How does token re-generation work based on the refresh token?

What is JSON Web Token(JWT)
JSON Web Token, mostly referred to as JWT, is a standard (not a library) created by IETF (not OAuth) and it is used to generate tokens for security purposes in your application. JWTs are used for validation, access control, payments, admin control, etc.

These tokens are generated from some payload of the user, e.g. { email, username, id }. Along with this payload, you also use a secret key, which you should store in your .env file.

If you don’t know what your secret should be, run this command:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
Copy-paste the above command and store the string in your secret file.

Coming back to JWT, with payload and signature, a JWT also has a header, which contains the type (JWT) and the encryption algorithm, like HMAC SHA256 or RSA.

Your token looks like this:

xxxx.yyyy.zzzz = Header.Payload.Signature

For now, don’t focus too much on the signature; just know that it is there for verification to make sure the message is not compromised.


What are the Access Token and Refresh Token?
Access Token → This is the token used to access authenticated resources, make particular requests, add or edit resources, etc. But an access token usually expires quickly (10–15 minutes) because if it gets compromised, it could be misused.

Refresh Token → This is like the access token but with a longer lifetime (e.g. 7 days). It allows users to stay logged in without needing to re-enter credentials every 15 minutes. When the access token expires, the refresh token is used to generate a new one.


Why We Need Them?
You must have encountered problems like:

  • A user should be logged in before adding a project.
  • Only admins should be able to see details of other users. In such cases, you need a way to verify that the request is coming from the correct user.

That’s where JWT comes in. You can generate a token that includes the user’s role or permissions, and then verify it on each request before allowing access.


What If We Don’t Use Them?
If we don’t use tokens, we would have to:

  • Keep sending usernames and passwords with every request (very insecure).
  • Maintain session data in the backend for every logged-in user (not scalable for large apps).
  • Expose ourselves to higher risks of session hijacking. In short, without access and refresh tokens, authentication becomes messy, insecure, and harder to manage.

Why do we need 2 tokens?
You might be thinking: Why not just use one token that never expires or expires after a few days?

The problem with a single long-lived token is security.

  • If that token gets stolen, the attacker can use it for as long as it’s valid. If it’s valid for 7 days, that’s 7 days of unauthorised access.
  • On the other hand, if you make the token short-lived (like 10 minutes), the user would have to log in again every 10 minutes, which is terrible for user experience.

It is a bearer token, i.e. if a hacker has the token, he becomes the owner and can use it in any way he likes, just like cash. Whoever holds the note can spend it.

The solution is to use two tokens:

  • Access Token (Short-lived), used for actual requests. Even if it gets compromised, the attacker can only use it for a few minutes.
  • Refresh Token (Long-lived), stored securely in an HTTP-only cookie, and used to silently request a new access token without bothering the user.

This way, you get the best of both worlds:
Good security (because access tokens expire quickly).
Good user experience (because refresh tokens let you stay logged in).


How They Work?
Here’s the general flow:

  1. User logs in with their credentials.
  2. Backend verifies credentials and creates an access token and a refresh token.
  3. Access token is sent in the response (usually in the body or headers).
  4. The refresh token is sent as an httpOnly cookie for security.
  5. For every API request, the access token is attached in the header (Authorisation: Bearer ).
  6. When the access token expires, the frontend automatically uses the refresh token to request a new one.

How Token Creation Works?
Flow Diagram for Token Generation
This is a simple flow diagram that shows how the browser receives the data back for verification.

Example code:

import jwt from "jsonwebtoken";

interface UserPayload {
  id: string;
  username: string;
}

function generateAccessToken(payload: UserPayload): string {
  return jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: "10m" });
}

function generateRefreshToken(payload: UserPayload): string {
  return jwt.sign(payload, process.env.JWT_SECRET as string, { expiresIn: "7d" });
}

import { Request, Response } from "express";

export const SignUp = async (req: Request, res: Response) => {
  try {
    // Your Business Logic (create user, save to DB, etc.)
    const newUser = { _id: "12345", username: "testUser" }; // Example only

    const accessToken = generateAccessToken({
      id: newUser._id.toString(),
      username: newUser.username,
    });

    const refreshToken = generateRefreshToken({
      id: newUser._id.toString(),
      username: newUser.username,
    });

    res.cookie("jwt", refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict",
    });

    return res.status(201).json({
      token: accessToken,
    });
  } catch (error) {
    console.log(error);
    return res.status(500).json({ message: "Something went wrong" });
  }
};
Enter fullscreen mode Exit fullscreen mode

How Token Re-Generation Works Based on the Refresh Token?
Flow Diagram for Token Re-Generation Works Based on the Refresh Token

Whenever the frontend (like Axios) receives a 401 Unauthorised error, it makes an API call to the backend with the refresh token. The backend verifies the refresh token, and if it’s valid, generates a new access token.

Example:

import jwt from "jsonwebtoken";
import { Request, Response } from "express";

export const RefreshToken = async (req: Request, res: Response) => {
  const token = req.cookies.jwt;
  if (!token) {
    return res.status(401).json({ message: "No refresh token found" });
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET as string) as {
      id: string;
      username: string;
    };

    const newAccessToken = jwt.sign(payload, process.env.JWT_SECRET as string, {
      expiresIn: "10m",
    });

    return res.status(201).json({
      token: newAccessToken,
    });
  } catch (err) {
    return res.status(403).json({
      message: "Refresh Token has expired or is not valid",
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

That’s the basic idea of Access Tokens and Refresh Tokens in a MERN app.
I hope you liked this article, and if you do, then please share it with someone who is wrapping their head around this topic.

Till then, enjoy your cold coffee. Have fun.

Top comments (0)