Access Tokens and Refresh Tokens

Understanding authentication and authorization mechanisms in web applications

By Hank Kim

Access Tokens and Refresh Tokens

Understanding authentication and authorization mechanisms in web applications

Access Tokens and Refresh Tokens

Access Token & Refresh Token

Access Token

  • A short-lived credential used to authenticate API requests.
  • Attached to requests, usually in the Authorization: Bearer <token> header.
  • Lifetime is intentionally short (minutes to ~1 hour) to limit risk if stolen.
  • Commonly stored in in-memory state (e.g., Zustand store, React state).

    • sessionStorage or localStorage can be used, but those are more exposed to XSS attacks.

Refresh Token

  • A long-lived credential used to obtain new access tokens when they expire.
  • Typically has a longer lifetime (days to weeks).
  • Because it can mint new access tokens, it must be protected carefully.
  • Usually stored in HttpOnly, Secure, SameSite cookies:

    • HttpOnly → JavaScript cannot read it (protects against XSS).
    • Secure → Sent only over HTTPS.
    • SameSite → Mitigates CSRF attacks.

Token Lifecycle

  1. On login, the server issues both an Access Token (short-lived) and a Refresh Token (long-lived).
  2. The client sends the Access Token with each API request.
  3. When the Access Token expires:

    • The client calls a refresh endpoint with the Refresh Token (sent automatically via cookie).
    • The server validates the Refresh Token and issues a new Access Token.
  4. If the Refresh Token has expired or is invalid → the user must log in again.

Implementation Example

authStore.ts

import { create } from "zustand";

interface AuthState {
  accessToken: string | null;
  setAccessToken: (token: string | null) => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  accessToken: null,
  setAccessToken: (token) => set({ accessToken: token }),
}));

api.ts

import axios from "axios";
import { useAuthStore } from "./authStore";

const api = axios.create({
  baseURL: "http://localhost:4000",
  withCredentials: true, // include Refresh Token cookie
});

// Request interceptor: attach Access Token
api.interceptors.request.use((config) => {
  const token = useAuthStore.getState().accessToken;
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Response interceptor: handle 401 and refresh flow
let isRefreshing = false;
let refreshQueue: (() => void)[] = [];

api.interceptors.response.use(
  (res) => res,
  async (err) => {
    const originalRequest = err.config;
    if (err.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        return new Promise((resolve) => {
          refreshQueue.push(() => resolve(api(originalRequest)));
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        const res = await axios.get("http://localhost:4000/auth/refresh", {
          withCredentials: true,
        });
        const newToken = res.data.accessToken;
        useAuthStore.getState().setAccessToken(newToken);

        refreshQueue.forEach((cb) => cb());
        refreshQueue = [];
        return api(originalRequest);
      } finally {
        isRefreshing = false;
      }
    }
    return Promise.reject(err);
  }
);

export default api;

App.tsx

import { useEffect } from "react";
import api from "./api";
import { useAuthStore } from "./authStore";

function App() {
  const setAccessToken = useAuthStore((s) => s.setAccessToken);

  useEffect(() => {
    // Attempt refresh at app start (cookie contains Refresh Token)
    api.get("/auth/refresh").then((res) => {
      setAccessToken(res.data.accessToken);
    });
  }, []);

  return <div>My App</div>;
}

Security Considerations

  • Access Token → short-lived, stored in memory.
  • Refresh Token → long-lived, stored in HttpOnly + Secure + SameSite cookies.
  • Always use HTTPS to avoid token sniffing.
  • Rotate or invalidate refresh tokens on logout or suspicious activity.
  • If refresh fails, force the user to log out.
  • Handle concurrent refresh requests carefully (e.g., with a refresh queue).

Summary

  • Access Token: short-lived, used for API authentication.
  • Refresh Token: long-lived, used to renew access tokens.
  • Access Token is typically stored in memory; Refresh Token is stored in secure cookies.
  • Together, they enable secure session management with minimal user friction.

Tags: Security