Refresh Tokens: Issue, Rotate, Revoke

Learn how to issue, rotate, and revoke refresh tokens in OAuth 2 and OpenID Connect. This hands-on lesson covers core concepts, step-by-step implementation, troubleshooting, and what to study next.

Focus: refresh tokens: issue, rotate, revoke

Sponsored

Your access token just expired in the middle of a critical API call, and now you're facing a hard decision: force the user to log in again, or silently fetch a new token in the background? That moment is exactly what refresh tokens are built for. In this lesson, you'll master the complete lifecycle of refresh tokens—how to issue them, rotate them for security, and revoke them when needed—so your applications can keep working seamlessly while staying secure.

The problem this lesson solves

Access tokens are short-lived by design—typically 15 minutes to an hour. They reduce the risk of a leaked token being used for extended periods, but they also introduce a usability problem: every hour, the user would be forced to re-authenticate, which destroys the user experience and breaks background tasks, integrations, and mobile apps that need to keep running without user interaction.

The solution OAuth 2.0 provides is the refresh token: a long-lived credential that allows your client to obtain new access tokens without prompting the user. But with that power comes responsibility. Refresh tokens are sensitive—if stolen, they grant access to the user's resources for a long time. That's why you must implement rotation (each refresh issues a new refresh token) and revocation (killing tokens when they're no longer needed). Without these practices, a single leaked refresh token could be a security nightmare.

Core concept / mental model

Think of refresh tokens and access tokens as a keycard and a visitor badge. The visitor badge (access token) gives you access to the building's rooms—but it expires at the end of the day. The keycard (refresh token) is your permanent pass: it can be used to get a new badge whenever the old one expires. If you lose the keycard, the security desk can invalidate it (revoke), and you can also issue a new keycard when you use the old one (rotation).

Here's the mental model in a few sentences:

  • Access token: short-lived, used to call protected APIs. Goes in the Authorization: Bearer header.
  • Refresh token: long-lived, used only at the token endpoint to get new access tokens. Never sent to APIs.
  • Token endpoint: the server-side endpoint where your client exchanges the refresh token for a new access token.

Token lifecycle (in words)

  1. Authorization: The user logs in and authorizes your app.
  2. Token issuance: The authorization server returns an access token and a refresh token.
  3. Access token use: Your app calls APIs with the access token until it expires.
  4. Refresh: When the access token expires, your app sends the refresh token to the token endpoint.
  5. Rotation (optional but recommended): The server responds with a new access token and a new refresh token; the old refresh token is invalidated.
  6. Revocation: If the user logs out or the token is compromised, your app calls the revocation endpoint to invalidate the refresh token.

This lifecycle is the heartbeat of silent re-authentication in OAuth 2.0 and OpenID Connect.

How it works step by step

Let's break down the actual token request/response flow, so you know exactly what happens under the hood.

1. Initial token issuance

When the user authorizes your app (via the authorization code flow, for example), the token endpoint returns a JSON body that looks like this:

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "def50200...",
  "scope": "read write"
}

The expires_in tells you how long the access token is valid (3600 seconds = 1 hour). The refresh_token is your long-lived key. Store it securely—never in browser local storage for a public client (SPA); use an HttpOnly, Secure cookie for web apps.

2. Refresh request

When the access token expires (you can detect this via a 401 response or by a timer), your client makes a POST request to the token endpoint with grant_type=refresh_token:

POST /oauth/token HTTP/1.1
Host: authserver.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=def50200...&client_id=my-client&client_secret=my-secret

The server validates the refresh token, checks the client, and returns a new access token (and, if rotation is enabled, a new refresh token).

3. Rotation (if enabled)

With rotation, the server marks the old refresh token as used and returns a new one. This means if an attacker steals a refresh token and uses it, the legitimate client's next refresh will fail because the old token is no longer valid. Rotation is the single most effective way to detect token theft.

4. Revocation

To revoke a refresh token, you call the revocation endpoint (defined in RFC 7009):

POST /oauth/revoke HTTP/1.1
Host: authserver.example.com
Content-Type: application/x-www-form-urlencoded

token=def50200...&token_type_hint=refresh_token&client_id=my-client

The server invalidates the token, and any subsequent use will return an error.

Hands-on walkthrough

Let's implement a minimal refresh token client in Python. We'll use requests and assume a typical OAuth server (you can test against a dummy endpoint or your own server).

First, make sure you have requests installed:

pip install requests

Now, write a Python script that issues a refresh token, uses it to get a new access token, and finally revokes it.

import requests

# Configuration
TOKEN_ENDPOINT = "https://auth.example.com/oauth/token"
REVOKE_ENDPOINT = "https://auth.example.com/oauth/revoke"
CLIENT_ID = "my-client"
CLIENT_SECRET = "my-secret"

# Step 1: Get a refresh token (in a real flow, you'd use the authorization code)
# For demo, we assume we already have a refresh token
tokens = {
    "access_token": "old_access_token",
    "refresh_token": "def50200...",
    "expires_in": 3600
}

def refresh_access_token(refresh_token):
    """Exchange a refresh token for a new access token."""
    data = {
        "grant_type": "refresh_token",
        "refresh_token": refresh_token,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    }
    resp = requests.post(TOKEN_ENDPOINT, data=data)
    resp.raise_for_status()
    return resp.json()

# Step 2: Refresh the access token
new_tokens = refresh_access_token(tokens["refresh_token"])
print("New access token:", new_tokens["access_token"])
if "refresh_token" in new_tokens:
    # Rotation: update the stored refresh token
    new_refresh_token = new_tokens["refresh_token"]
    print("New refresh token (rotation):", new_refresh_token)
else:
    # No rotation, keep the same refresh token
    new_refresh_token = tokens["refresh_token"]

# Step 3: Revoke the refresh token (e.g., on logout)
def revoke_token(token):
    data = {
        "token": token,
        "token_type_hint": "refresh_token",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
    }
    resp = requests.post(REVOKE_ENDPOINT, data=data)
    resp.raise_for_status()
    print("Refresh token revoked.")

revoke_token(new_refresh_token)

Expected output (assuming a valid server):

New access token: eyJhbGciOi...
New refresh token (rotation): def50201...
Refresh token revoked.

Handling refresh token expiry or reuse detection

If the refresh token is invalid, expired, or already used (with rotation), the server returns an error like invalid_grant. Your client should catch that and fall back to a full re-authentication.

# Robust refresh with error handling
def safe_refresh(refresh_token):
    try:
        data = {
            "grant_type": "refresh_token",
            "refresh_token": refresh_token,
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
        }
        resp = requests.post(TOKEN_ENDPOINT, data=data)
        if resp.status_code == 400 and resp.json().get("error") == "invalid_grant":
            print("Refresh token invalid or expired — need full login.")
            return None
        resp.raise_for_status()
        return resp.json()
    except requests.RequestException as e:
        print(f"Network error: {e}")
        return None

Pro tip: Always use a dedicated token refresh library or SDK when available (e.g., oauthlib, authlib). They handle edge cases like clock skew and token reuse detection.

Compare options / when to choose what

Not all services implement refresh tokens the same way. Here's a quick comparison of common strategies:

Strategy How it works Pros Cons
No rotation Same refresh token reused until expiry Simple to implement If leaked, attacker has unlimited access until expiry
Rotation Each refresh returns a new refresh token, old one invalidated Detects theft; shortens window of misuse More complex; must handle race conditions
Revocation endpoint Explicitly kill tokens via API Immediate invalidation on logout/compromise Requires server support and client integration
Reuse detection (with rotation) If a used refresh token is presented again, revoke the entire token family Strongest security; detects replay attacks Most complex; requires family tracking on server

For most production apps, rotation + reuse detection is the gold standard. For low-risk apps, simple rotation without reuse detection might suffice. Avoid no-rotation unless you're in a controlled environment.

When to choose what

  • Public clients (SPAs, mobile): Use rotation and secure storage (HttpOnly cookies, Keychain/Keystore). Never store refresh tokens in JavaScript-accessible storage.
  • Confidential clients (server apps): Rotation is still recommended; revocation on logout is a must.
  • High-security environments (finance, healthcare): Implement reuse detection and consider shorter refresh token lifetimes.

Troubleshooting & edge cases

401 on API call after refresh

If your app gets a 401 even after refreshing, it's likely because: - You're sending the old access token (race condition) — always use the latest one. - The access token is valid but the API expects different scope. Check the scope returned with the token.

Fix: Implement a token refresh queue that ensures only one refresh happens at a time, and cache the new access token immediately.

Refresh token reuse after rotation

If a refresh token is used twice, and your server has reuse detection, it will revoke the entire token family. This can break your app if a background job retries a refresh with the old token.

Fix: Always update the stored refresh token from the response. If you get an invalid_grant, force re-login.

Refresh token expiry

Refresh tokens can also expire (often 30 days, depending on server config). If your app doesn't refresh before that, the user must log in again.

Fix: Monitor for invalid_grant and proactively re-authenticate before expiry when possible (e.g., silent iframe or iframe-based re-auth for SPAs).

Race condition in rotation

Two parallel requests might try to use the same refresh token simultaneously. With rotation, one will succeed; the other will fail with invalid_grant.

Fix: Use a mutex or lock in your client to serialize refresh calls. In Python, you can use threading.Lock() or an async lock.

import threading

lock = threading.Lock()

def refresh_with_lock():
    with lock:
        # perform refresh
        pass

What you learned & what's next

You've mastered the full lifecycle of refresh tokens: how to issue them during authorization, rotate them on every use to increase security, and revoke them explicitly on logout or compromise. You now understand the core mental model (keycard vs. badge), the exact HTTP flows, and common pitfalls like token reuse detection and race conditions.

Next step: In the next lesson, you'll explore token introspection and userinfo — how to validate tokens and fetch user claims in OpenID Connect. You'll also learn how to handle token revocation across multiple clients and auth servers. Get ready to put your refresh token skills into a full token management strategy.

Practice recap

In a sandbox OAuth server (or using an online service like OAuth 2.0 Playground), obtain a refresh token, then manually call the token endpoint to refresh it. Observe the new access token and, if rotation is supported, the new refresh token. Then call the revocation endpoint and verify that subsequent refresh attempts fail with invalid_grant.

Common mistakes

  • Storing refresh tokens in browser localStorage or sessionStorage — they're accessible to XSS. Use HttpOnly, Secure cookies for SPAs, or secure storage in mobile apps.
  • Not rotating refresh tokens — a single leaked token gives an attacker long-lived access. Always rotate on every refresh.
  • Failing to update stored refresh token after rotation — using the old token triggers reuse detection and revokes the entire token family.
  • Ignoring invalid_grant errors — treating them as temporary failures instead of falling back to full re-authentication.
  • Not handling concurrent refresh requests — parallel requests with the same refresh token cause race conditions and unexpected revocations.

Variations

  1. OAuth 2.0 can be combined with OpenID Connect to also obtain an ID token, which contains user profile claims — often used together with access tokens in modern auth.
  2. Some providers (e.g., Entra ID) support token families where multiple refresh tokens belong to the same family, enabling revocation of all descendants.
  3. Instead of classic refresh tokens, some modern auth systems (e.g., device flow with rotating user codes) use alternative long-lived mechanisms — but refresh tokens remain the OAuth 2.0 standard.

Real-world use cases

  • A mobile banking app silently refreshes the user's access token every 15 minutes without interrupting the session, and rotates the refresh token to detect fraud.
  • An enterprise SaaS uses refresh tokens with revocation to instantly kill a user's session when they change their password or an admin disables their account.
  • A CI/CD pipeline obtains access tokens for deployment APIs using refresh tokens, and revokes them after the pipeline completes to limit exposure.

Key takeaways

  • Refresh tokens are long-lived credentials used exclusively at the token endpoint to obtain new access tokens — never send them to APIs.
  • Rotation (issuing a new refresh token on every refresh) is essential for security and helps detect token theft.
  • Revocation endpoints allow you to invalidate refresh tokens immediately on logout or breach.
  • Always store refresh tokens securely — HttpOnly cookies for web, Keychain/Keystore for mobile, and never in client-side storage for public clients.
  • Handle invalid_grant errors by falling back to a full login flow, as they signal expired, revoked, or already-used tokens.
  • Implement refresh token locking to avoid race conditions in concurrent refresh requests.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.