Skip to content

Token Refresh Patterns — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Token Refresh Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.

Token refresh patterns enable applications to maintain user sessions beyond the lifetime of a short-lived access token by using a longer-lived refresh token to obtain new access tokens without requiring the user to re-authenticate.

What You'll Learn

By the end of this lesson, you will implement token refresh with rotation, build automatic refresh interceptors for API clients, manage refresh token lifecycle, and detect refresh token theft.

Why It Matters

Short-lived access tokens (15 minutes) limit the damage if a token is stolen. Refresh tokens make the short lifetime practical by allowing seamless token renewal. Doda Browser uses refresh tokens to maintain user sessions across its sync service while keeping access tokens short-lived for security.

Real-World Use

A user opens a banking app in the morning. The app obtains an access token (valid 15 min) and a refresh token (valid 7 days). The user checks their balance, then puts the phone down. 20 minutes later, they open the app again. The app automatically refreshes the access token using the refresh token. The user never sees a login prompt.

Token Refresh Flow

sequenceDiagram
    participant Client
    participant API
    participant Auth

    Client->>Auth: Login
    Auth-->>Client: Access Token (15m) + Refresh Token (7d)
    Client->>API: API call with Access Token
    API-->>Client: 200 OK
    Note over Client,API: 15 minutes later...
    Client->>API: API call with expired Access Token
    API-->>Client: 401 Unauthorized
    Client->>Auth: POST /refresh with Refresh Token
    Auth-->>Client: New Access Token + New Refresh Token (rotated)
    Client->>API: Retry API call with new Access Token
    API-->>Client: 200 OK

Refresh Token with Rotation

const express = require("express");
const jwt = require("jsonwebtoken");
const crypto = require("crypto");

const app = express();
app.use(express.json());

const ACCESS_SECRET = crypto.randomBytes(64).toString("hex");
const REFRESH_SECRET = crypto.randomBytes(64).toString("hex");
const tokenFamily = new Map();

function generateTokens(user) {
  const accessToken = jwt.sign(
    { sub: user.id, role: user.role, type: "access" },
    ACCESS_SECRET,
    { expiresIn: "15m" }
  );

  const refreshId = crypto.randomBytes(16).toString("hex");
  const refreshToken = jwt.sign(
    { sub: user.id, type: "refresh", refreshId, family: user.id },
    REFRESH_SECRET,
    { expiresIn: "7d" }
  );

  tokenFamily.set(refreshId, {
    refreshId,
    userId: user.id,
    createdAt: Date.now(),
    isUsed: false,
  });

  return { accessToken, refreshToken, expiresIn: 900 };
}

app.post("/api/auth/login", (req, res) => {
  const { email, password } = req.body;
  if (email !== "alice@example.com" || password !== "correct-password") {
    return res.status(401).json({ error: "Invalid credentials" });
  }
  const tokens = generateTokens({ id: 1, role: "user" });
  res.json(tokens);
});

app.post("/api/auth/refresh", (req, res) => {
  const { refreshToken } = req.body;
  if (!refreshToken) {
    return res.status(401).json({ error: "Refresh token required" });
  }

  try {
    const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
    const stored = tokenFamily.get(decoded.refreshId);

    if (!stored) {
      return res.status(401).json({ error: "Refresh token not found" });
    }

    if (stored.isUsed) {
      console.log(`[TOKEN THEFT] Refresh token ${decoded.refreshId} already used!`);
      const familyMembers = [...tokenFamily.values()]
        .filter(t => t.userId === decoded.sub && !t.isUsed);
      familyMembers.forEach(t => {
        t.isUsed = true;
        console.log(`[TOKEN THEFT] Invalidating sibling token: ${t.refreshId}`);
      });
      return res.status(401).json({ error: "Token reuse detected" });
    }

    stored.isUsed = true;
    const tokens = generateTokens({ id: decoded.sub, role: decoded.role || "user" });
    console.log(`[Refresh] Tokens rotated for user ${decoded.sub}`);
    res.json(tokens);

  } catch (err) {
    res.status(401).json({ error: "Invalid refresh token" });
  }
});

app.listen(3000);

Automatic Refresh Interceptor

// API client with automatic token refresh
class ApiClient {
  constructor(baseURL, authService) {
    this.baseURL = baseURL;
    this.authService = authService;
    this.accessToken = null;
    this.refreshPromise = null;
  }

  async request(endpoint, options = {}) {
    if (!this.accessToken) {
      throw new Error("Not authenticated");
    }

    const response = await fetch(`${this.baseURL}${endpoint}`, {
      ...options,
      headers: {
        ...options.headers,
        Authorization: `Bearer ${this.accessToken}`,
      },
    });

    if (response.status === 401) {
      const newToken = await this.refreshAccessToken();
      this.accessToken = newToken;
      return this.request(endpoint, options);
    }

    return response.json();
  }

  async refreshAccessToken() {
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    this.refreshPromise = this.authService.refresh()
      .then((token) => {
        this.refreshPromise = null;
        return token;
      })
      .catch((err) => {
        this.refreshPromise = null;
        this.accessToken = null;
        throw err;
      });

    return this.refreshPromise;
  }
}

Common Mistakes

  1. Not rotating refresh tokens leaves a stale token valid indefinitely if stolen.
  2. Failing to detect refresh token reuse allows attackers to maintain access after theft.
  3. Storing refresh tokens in localStorage exposes them to XSS attacks.
  4. Using the same secret for access and refresh tokens allows cross-use.
  5. Not implementing a maximum refresh chain length allows infinite session extension.
  6. Blocking the UI while refreshing tokens creates poor user experience.

Practice Questions

  1. What is refresh token rotation?

Each time the client refreshes, the server issues a new refresh token and invalidates the old one. If an attacker steals a refresh token, the next legitimate use will fail because the token was already rotated, alerting of theft.

  1. How do you detect refresh token theft?

Implement token family tracking. When a used refresh token is presented, invalidate all tokens in the same family. The legitimate user will be forced to re-authenticate, and the attacker loses access.

  1. What is the maximum safe lifetime for a refresh token?

7-30 days depending on security requirements. Banking apps: 7 days. Social media: 30 days. Enterprise apps: configurable by admin policy. Always allow absolute timeout regardless of refresh activity.

  1. Challenge: Implement a complete token refresh system with rotation, theft detection, maximum refresh chain (20 refreshes max), device binding (refresh token tied to device fingerprint), and automatic cleanup of expired token records.

FAQ

Can I use access tokens without refresh tokens?

Yes, but the user must re-authenticate every time the access token expires (every 15 minutes). Refresh tokens make the user experience practical.

How do mobile apps store refresh tokens securely?

iOS: Keychain (kSecClass). Android: EncryptedSharedPreferences. React Native: react-native-keychain. Flutter: flutter_secure_storage. Never use SharedPreferences or AsyncStorage.

What happens if the refresh token expires?

The user must re-authenticate. The client should redirect to the login screen. Implement a silent refresh attempt first, then fall back to full authentication.

Should refresh token rotation be synchronous?

Yes. If multiple API calls fail simultaneously with 401, only one refresh call should be made. The interceptor pattern with a shared promise handles this correctly.

Mini Project: Token Refresh Monitor

Build a CLI tool that simulates a client performing API calls with token refresh, tracks refresh events, and generates a report on token lifetime and refresh patterns.

import time
import jwt
import secrets
from datetime import datetime, timedelta

class TokenSimulator:
    def __init__(self):
        self.secret = secrets.token_hex(32)
        self.refresh_count = 0
        self.events = []

    def create_tokens(self):
        access = jwt.encode({
            "sub": "user_1", "type": "access",
            "exp": int(time.time()) + 15,
        }, self.secret, algorithm="HS256")
        refresh = jwt.encode({
            "sub": "user_1", "type": "refresh", "id": secrets.token_hex(8),
            "exp": int(time.time()) + 300,
        }, self.secret, algorithm="HS256")
        return access, refresh

    def simulate_session(self, duration_minutes=10):
        access, refresh = self.create_tokens()
        start = time.time()
        end = start + duration_minutes * 60
        now = start

        while now < end:
            try:
                jwt.decode(access, self.secret, algorithms=["HS256"])
                self.events.append({"time": datetime.utcnow(), "event": "API call OK"})
            except jwt.ExpiredSignatureError:
                try:
                    jwt.decode(refresh, self.secret, algorithms=["HS256"])
                    access, refresh = self.create_tokens()
                    self.refresh_count += 1
                    self.events.append({"time": datetime.utcnow(), "event": f"Token refreshed (#{self.refresh_count})"})
                except jwt.ExpiredSignatureError:
                    self.events.append({"time": datetime.utcnow(), "event": "Session expired"})
                    break
            now += 20

        print(f"=== Session Report ===")
        print(f"Duration: {duration_minutes} minutes simulated")
        print(f"API calls: {len(self.events)}")
        print(f"Refreshes: {self.refresh_count}")
        print(f"Active session: {'Yes' if self.events and self.events[-1]['event'] != 'Session expired' else 'No'}")

sim = TokenSimulator()
sim.simulate_session(5)

What's Next

Learn about multi-factor authentication for layered security, then explore passwordless authentication for modern login experiences.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro