Access Tokens and Refresh Tokens
Understanding authentication and authorization mechanisms in web applications
Access Tokens and Refresh Tokens
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).
sessionStorageorlocalStoragecan 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
- On login, the server issues both an Access Token (short-lived) and a Refresh Token (long-lived).
 - The client sends the Access Token with each API request.
 - 
    
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.
 
 - 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.