Skip to content

JWT Refresh Tokens — Long-Lived Credentials for Seamless Session Renewal

DodaTech Updated 2026-06-28 4 min read

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

JWT refresh tokens are long-lived credentials issued alongside access tokens, enabling clients to obtain new access tokens without requiring the user to re-authenticate.

What You'll Learn

How refresh tokens complement access tokens, refresh token rotation, secure storage strategies, and handling refresh token revocation.

Why It Matters

Short-lived access tokens (15 minutes) are secure but require frequent renewal. Refresh tokens solve this by providing a long-lived credential that lives on the device and silently obtains new access tokens.

Real-World Use

Auth0 uses refresh token rotation for mobile apps. Google issues refresh tokens that last until revoked. Durga Antivirus Pro uses 7-day refresh tokens with rotation for its partner API.

sequenceDiagram
    participant Client
    participant Auth as Auth Server
    participant API as Resource Server

    Client->>Auth: Login
    Auth->>Client: Access Token (15m) + Refresh Token (7d)
    Client->>API: Request + Access Token
    API->>Client: 401 Expired
    Client->>Auth: Refresh + Refresh Token
    Auth->>Client: New Access Token + New Refresh Token
    Client->>API: Request + New Access Token
    API->>Client: 200 OK

Refresh Token Rotation

With rotation, each refresh request returns both a new access token AND a new refresh token, invalidating the previous refresh token. This prevents a stolen refresh token from being reused.

Event Valid Refresh Tokens
Initial login RT-1
First refresh RT-2 (RT-1 invalidated)
Attacker tries RT-1 Rejected (already used)
Legitimate refresh with RT-2 RT-3 issued (RT-2 invalidated)

Code Example: Refresh Token with Rotation

import jwt, secrets, datetime, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET = "your-secret"

# Store: refresh_token_hash -> token_data
refresh_store = {}

@app.route("/api/auth/login", methods=["POST"])
def login():
    # Validate credentials (simplified)
    data = request.get_json()
    if data.get("password") != "correct":
        return jsonify({"error": "Invalid credentials"}), 401

    # Generate access token
    access_token = jwt.encode({
        "token_type": "access",
        "sub": data["username"],
        "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
    }, SECRET, algorithm="HS256")

    # Generate refresh token
    refresh_token = secrets.token_urlsafe(32)
    token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
    refresh_store[token_hash] = {
        "user": data["username"],
        "family": secrets.token_hex(16),
        "active": True
    }

    return jsonify({
        "access_token": access_token,
        "refresh_token": refresh_token,
        "expires_in": 900
    })

@app.route("/api/auth/refresh", methods=["POST"])
def refresh():
    data = request.get_json()
    old_refresh = data.get("refresh_token")
    if not old_refresh:
        return jsonify({"error": "Refresh token required"}), 400

    old_hash = hashlib.sha256(old_refresh.encode()).hexdigest()
    stored = refresh_store.get(old_hash)

    if not stored or not stored["active"]:
        # Possible token theft — revoke all tokens in family
        revoke_family(stored.get("family") if stored else None)
        return jsonify({"error": "Refresh token revoked"}), 401

    # Rotate: invalidate old, issue new
    stored["active"] = False

    new_access = jwt.encode({
        "token_type": "access",
        "sub": stored["user"],
        "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
    }, SECRET, algorithm="HS256")

    new_refresh = secrets.token_urlsafe(32)
    new_hash = hashlib.sha256(new_refresh.encode()).hexdigest()
    refresh_store[new_hash] = {
        "user": stored["user"],
        "family": stored["family"],
        "active": True
    }

    return jsonify({
        "access_token": new_access,
        "refresh_token": new_refresh,
        "expires_in": 900
    })

def revoke_family(family_id):
    """Revoke all refresh tokens in a family (defense against theft)."""
    for key, data in list(refresh_store.items()):
        if data.get("family") == family_id:
            data["active"] = False

Common Mistakes

1. Not Rotating Refresh Tokens

Without rotation, a stolen refresh token works until it expires. Rotation limits the window of opportunity.

2. Storing Refresh Tokens in localStorage

XSS Attacks can read localStorage. Use httpOnly cookies or secure device storage.

3. Making Refresh Tokens Permanent

Refresh tokens must expire. 7-30 days is standard. Permanent refresh tokens cannot be revoked if compromised.

4. Not Detecting Token Theft

When an already-rotated refresh token is presented, the server should revoke all tokens in that family (token theft detection).

5. Sending Refresh Tokens with API Requests

Refresh tokens are only for the refresh endpoint. Never send them to regular API endpoints.

Practice Questions

  1. Why are refresh tokens needed if we have access tokens?
  2. What is refresh token rotation and why is it important?
  3. How should refresh tokens be stored on the client?
  4. What happens when a refresh token expires?
  5. How do you detect refresh token theft?

Answers:

  1. Access tokens are short-lived (15 min) for security. Refresh tokens provide long-lived sessions without frequent logins.
  2. Each refresh returns a new refresh token, invalidating the old one. A stolen refresh token can only be used once.
  3. httpOnly cookies (web) or secure device storage/Keychain/Keystore (mobile). Never localStorage.
  4. The server returns 401 on refresh. The user must re-authenticate to obtain a new refresh token.
  5. When an already-rotated (inactive) refresh token is presented, revoke all tokens in the same family and alert the user.

Challenge: Implement refresh token rotation with family tracking and automatic theft detection. When an old refresh token is presented after rotation, revoke all tokens for that family.

FAQ

How long should refresh tokens live?

7-30 days for most apps. 90 days for internal tools. Shorter for high-security applications.

Can refresh tokens be revoked?

Yes. Maintain a server-side store of valid refresh tokens. Deleting or marking one as inactive revokes it.

Should refresh tokens be JWTs?

Not typically. Refresh tokens are often opaque strings that reference server-side state, enabling immediate revocation.

What is a token family?

A token family groups all refresh tokens derived from the same initial login. If one token in a family is compromised, the entire family can be revoked.

How do mobile apps handle refresh tokens?

Store in iOS Keychain or Android Keystore. These are encrypted device-specific storage that survives app restarts.

Mini Project

Build a Flask refresh token service with rotation: login issues access + refresh tokens, refresh endpoint rotates both, and automatic detection and response to token theft.

What's Next

Now learn about JWT Token Expiry — how expiry times work and strategies for handling them.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro