Understanding Access and Refresh Tokens in Authentication using Node, Express, Typescript

Akash MauryaAkash Maurya
5 min read

Access Token

An Access Token is a short-lived token used to:

  • Authenticate users and grant access to protected routes and resources.

  • Extract user details for authorization purposes.

  • Ensure security by expiring after a short duration (e.g., 15 minutes).

Refresh Token

A Refresh Token is a long-lived token used to:

  • Generate a new access token when the existing one expires.

  • Maintain user sessions without requiring frequent logins.

  • Improve user experience by reducing unnecessary re-authentication.

How Access and Refresh Tokens Work

  1. User Login: When a user logs in, both access and refresh tokens are generated with specific expiry times.

  2. Token Storage:

    • The access token is stored in memory or local storage (for web) and secure storage (for mobile).

    • The refresh token is stored in an HTTP-only secure cookie (for web) or secure storage (for mobile).

  3. Accessing Protected Routes: The access token is included in requests (usually in the Authorization header as Bearer <token>) to authenticate users.

  4. Token Expiry & Refresh: If the access token expires while the user is active, the refresh token is used to generate a new access token without requiring the user to log in again.

Why Use Both Access and Refresh Tokens?

Problem Without a Refresh Token:

If you only use an access token, when it expires, the user is logged out and must log in again. This can disrupt the user experience.

Solution with a Refresh Token:

By using a refresh token, the system can seamlessly generate a new access token without logging the user out, improving security and usability.

Token Strategy for Web and Mobile Applications

PlatformAccess TokenRefresh Token
WebMemory / HTTP-only cookieHTTP-only secure cookie
MobileSecure Storage (Keychain for iOS, EncryptedSharedPreferences for Android)Secure Storage

Security Best Practices

  • Use HTTP-only cookies for refresh tokens to prevent XSS (Cross-Site Scripting) attacks.

  • Store access tokens in memory rather than local storage to reduce vulnerability to XSS attacks.

  • Implement token rotation to enhance security (generate a new refresh token upon usage).

  • Use short-lived access tokens (e.g., 15 minutes) and longer-lived refresh tokens (e.g., 7 days).

  • Revoke refresh tokens if the user logs out or changes credentials.

Code to Generate Access and Refresh Tokens

import jwt from "jsonwebtoken";

const generateAccessToken = (userId: string, role: string) => {
  return jwt.sign({ userId, role }, process.env.ACCESS_TOKEN_SECRET!, {
    expiresIn: "15m",
  });
};

const generateRefreshToken = (userId: string, tokenVersion: number) => {
  return jwt.sign({ userId, tokenVersion }, process.env.REFRESH_TOKEN_SECRET!, {
    expiresIn: "7d",
  });
};

login Controller

import { Request, Response } from "express";
import bcrypt from "bcryptjs";
import { generateAccessToken, generateRefreshToken } from "../utils/jwt";
import UserModel from "../models/User";

export const login = async (req: Request, res: Response) => {
  try {
    const { email, password } = req.body;
    const user = await UserModel.findOne({ email });

    if (!user) return res.status(400).json({ message: "User not found" });

    const isValidPassword = await bcrypt.compare(password, user.password);
    if (!isValidPassword)
      return res.status(400).json({ message: "Invalid credentials" });

    const accessToken = generateAccessToken(user._id.toString(), user.role);
    const refreshToken = generateRefreshToken(user._id.toString(), user.tokenVersion);

    // Store refresh token in HTTP-only cookie (for web)
    res.cookie("refreshToken", refreshToken, {
      httpOnly: true,
      secure: true, // Only in HTTPS
      sameSite: "strict",
    });

    return res.json({ accessToken });
  } catch (error) {
    return res.status(500).json({ message: "Internal Server Error" });
  }
};

refreshToken Controller

import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import UserModel from "../models/User";
import { generateAccessToken, generateRefreshToken } from "../utils/jwt";

export const refreshToken = async (req: Request, res: Response) => {
  const token = req.cookies.refreshToken || req.body.refreshToken;
  if (!token) return res.status(401).json({ message: "Unauthorized" });

  try {
    const payload: any = jwt.verify(token, process.env.REFRESH_TOKEN_SECRET!);
    const user = await UserModel.findById(payload.userId);

    if (!user || user.tokenVersion !== payload.tokenVersion)
      return res.status(401).json({ message: "Unauthorized" });

    const newAccessToken = generateAccessToken(user._id.toString(), user.role);
    const newRefreshToken = generateRefreshToken(user._id.toString(), user.tokenVersion);

    res.cookie("refreshToken", newRefreshToken, {
      httpOnly: true,
      secure: true,
      sameSite: "strict",
    });

    return res.json({ accessToken: newAccessToken });
  } catch (err) {
    return res.status(401).json({ message: "Invalid token" });
  }
};

logout Controller

export const logout = (req: Request, res: Response) => {
  res.clearCookie("refreshToken");
  return res.json({ message: "Logged out successfully" });
};

Middleware for Protecting Routes

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

export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ message: "Unauthorized" });

  try {
    const payload: any = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!);
    req.user = payload;
    next();
  } catch (err) {
    return res.status(401).json({ message: "Invalid token" });
  }
};

Additional Security Considerations

  1. Token Revocation:

    • Invalidate refresh tokens on password change or logout.

    • Store tokenVersion in the database, and increment it when needed.

  2. Rate Limiting & Throttling:

    • Use express-rate-limit to prevent brute-force attacks.
  3. CSRF Protection:

    • For web apps, use CSRF tokens or same-site cookies.
  4. Secure Environment Variables:

    • Store secrets in .env and never commit them to Git.

Final Thoughts

  • This approach ensures security, efficiency, and scalability.

  • Using access tokens for short sessions and refresh tokens for re-authentication reduces attack risks.

  • Following best practices for storage ensures safety across both web and mobile clients.

Conclusion

Access and refresh tokens play a crucial role in secure authentication. By implementing the right storage strategy and security best practices, you can enhance user experience and protect applications from common security threats like XSS and token theft.

By following this approach, your application can efficiently manage authentication while keeping user data secure.

1
Subscribe to my newsletter

Read articles from Akash Maurya directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Akash Maurya
Akash Maurya

I am a Software Associate Engineer at XRG Consulting Pvt. Ltd., where I contribute to backend development, user authentication, employee onboarding, and database optimization. I have a deep understanding of TypeScript, Prisma, and OAuth/OIDC authentication and work extensively on server-side performance improvements. As a MERN Stack Developer, I specialize in building scalable, high-performance applications and optimizing infrastructure for seamless deployment. With hands-on experience in MongoDB, Express.js, React, and Node.js, I design robust backends, create efficient frontend architectures, and streamline deployment workflows using Docker and CI/CD pipelines. I am passionate about writing clean, maintainable code and implementing DevOps best practices to enhance system reliability and scalability. Always eager to learn, I thrive in fast-paced environments where I can leverage my problem-solving skills to build cutting-edge solutions. 🔹 Key Skills: MERN Stack | Node.js | TypeScript | MongoDB | Prisma | Docker | DevOps | OAuth & OIDC | Cloud Infrastructure | CI/CD | API Development | Scalable Systems