KAHIBARO
Discord Login Register

13.9. Refresh Tokens

Why Refresh Tokens Exist

In many applications you want users to stay logged in for a long time, but you also want to reduce the risk if an attacker steals an access token.

An access token is usually:

If you keep access tokens valid for hours or days:

Refresh tokens solve this by splitting responsibilities:

So a user can stay logged in for days or weeks, but each individual access token is only valid for minutes.

Key idea: Use short lived access tokens for API calls and long lived refresh tokens only for refreshing access tokens, not for direct access to protected resources.


How Refresh Tokens Work

Basic Flow

A typical login and refresh flow looks like this:

  1. User logs in
    • Client sends credentials to /auth/login.
    • Server verifies them.
    • Server returns:
      • an access token (short expiry, for example 15 minutes)
      • a refresh token (long expiry, for example 7 to 30 days)
  2. Client uses access token
    • For each API request, client sends the access token in a header:
      • Authorization: Bearer <access_token>.
    • When the server receives the request, it validates the access token and processes the request.
  3. Access token expires
    • After its expiry time, the server starts rejecting requests with that access token, usually with 401 Unauthorized or 403 Forbidden.
  4. Client uses refresh token
    • Before or after the access token expires, the client calls /auth/refresh with the refresh token.
    • If the refresh token is valid and not expired or revoked, the server issues:
      • a new access token
      • often a new refresh token too
  5. User logs out or token is revoked
    • Client calls /auth/logout and sends the refresh token (and optionally access token).
    • Server marks the refresh token as revoked, or deletes it from storage.
    • Client removes any tokens stored locally.

In formula-like form:

$$
\text{User credentials} \xrightarrow{\text{/auth/login}}
(\text{access\_token},\, \text{refresh\_token})
$$

Later:

$$
\text{refresh\_token} \xrightarrow{\text{/auth/refresh}}
(\text{new\_access\_token},\, \text{new\_refresh\_token})
$$

Example API Endpoints

A very common minimal set of endpoints:

EndpointMethodPurpose
/auth/loginPOSTGet access + refresh tokens
/auth/refreshPOSTGet new access (and refresh)
/auth/logoutPOSTInvalidate the refresh token

Example JSON response from /auth/login:

json
{
  "access_token": "eyJhbGciOiJIUzI1...",
  "access_token_expires_in": 900,
  "refresh_token": "a1b2c3d4e5f6...",
  "refresh_token_expires_in": 2592000,
  "token_type": "bearer"
}

Time values are in seconds:

Access Tokens vs Refresh Tokens

Although both are "tokens", they should be treated differently.

Comparison

FeatureAccess TokenRefresh Token
PurposeCall protected APIsGet new access tokens
LifetimeShort (minutes)Long (days or weeks)
Where usedEvery API requestOnly to refresh tokens
Risk if stolenModerate, limited by short expiryHigh, can keep generating access tokens
Storage locationIn memory, or short term storageMore protected storage, often HttpOnly cookie
Should be JWT?Often yesCan be JWT or opaque random string
RevocationHard if pure JWT, easier if storedMust be revocable

Never use refresh tokens directly to access protected data.
They must only be used to obtain new access tokens.

Opaque vs JWT Refresh Tokens

Two popular design choices:

  1. Opaque refresh tokens
    • Random strings, for example b7146e06-76a9-4e99-88de-....
    • Server stores them in a database table:
      • id, user_id, token_hash, expires_at, revoked_at, user_agent, ip_address.
    • On /auth/refresh, server:
      • looks up the token
      • checks if still valid
      • issues new tokens
  2. JWT refresh tokens
    • Structured, signed tokens with claims like:
      • sub: user id
      • exp: expiry
      • type: "refresh"
    • Still often stored or tracked server side for revocation or rotation.

For beginners, opaque refresh tokens stored in a database are simpler and safer to reason about, because revocation is straightforward.


Token Rotation

Token rotation means: when the client uses a refresh token, the server:

So at any time, only the most recent refresh token is valid for that session.

Why Rotate?

Without rotation:

With rotation:

This makes it easier to detect suspicious behavior and to cut off stolen tokens early.

Rule: A refresh token should be single use. Once used to get new tokens, it must be invalidated and replaced.

Simple Rotation Workflow

Let us say the user has refresh_token_A.

  1. Client calls /auth/refresh with refresh_token_A.
  2. Server:
    • Validates refresh_token_A.
    • Marks refresh_token_A as used or revoked.
    • Creates refresh_token_B.
    • Returns new access token and refresh_token_B.
  3. Client replaces its stored refresh token with refresh_token_B.

Next time, the client must send refresh_token_B, not A.

Pseudo table for a token row:

ColumnValue
id123
user_id42
token_hashhash of refresh_token_A
is_revokedtrue
replaced_by_id124

New row:

ColumnValue
id124
user_id42
token_hashhash of refresh_token_B
is_revokedfalse
replaced_by_idnull

Detecting Token Theft with Rotation

Scenario:

Different possibilities:

  1. Legitimate client uses A first
    • A is exchanged for B, and A is revoked.
    • When attacker later uses A, server sees:
      • A already used or revoked.
    • This is a strong signal that A was stolen, so the server can:
      • revoke B too
      • log the event
      • ask the user to reauthenticate.
  2. Attacker uses A first
    • Attacker gets B.
    • When user later uses A, server sees the same suspicious behavior.

In both cases, rotation helps detect that someone tried to use the same refresh token multiple times.

You can implement a simple rule:

If a refresh token is used after it was already rotated, treat it as a possible token theft and revoke the entire session.


Implementing Refresh Tokens in Practice

Typical Database Table

For opaque refresh tokens, you will often have a table like refresh_tokens:

ColumnTypeDescription
idUUID / integerInternal token id
user_idUUID / integerThe user this token belongs to
token_hashstringHash of the refresh token value
created_atdatetimeWhen token was created
expires_atdatetimeWhen token expires
revoked_atdatetime/nullWhen token was revoked, if any
replaced_byid/nullNew token id after rotation
user_agentstring/nullOptional, client information
ip_addressstring/nullOptional, IP at creation

Notice that you store a hash, not the raw token, similar to passwords.

Login Endpoint Example (Pseudocode)

Here is high level pseudocode, independent of any specific language or framework:

python
def login(username: str, password: str):
    user = find_user_by_username(username)
    if not user or not verify_password(password, user.password_hash):
        raise UnauthorizedError("Invalid credentials")
    access_token = create_access_token(user_id=user.id, expires_in=15 * 60)
    refresh_token_value = generate_secure_random_string()
    refresh_token_hash = hash_token(refresh_token_value)
    save_refresh_token(
        user_id=user.id,
        token_hash=refresh_token_hash,
        expires_in_days=30
    )
    return {
        "access_token": access_token,
        "access_token_expires_in": 15 * 60,
        "refresh_token": refresh_token_value,
        "refresh_token_expires_in": 30 * 24 * 60 * 60,
        "token_type": "bearer"
    }

Refresh Endpoint with Rotation (Pseudocode)

python
def refresh(refresh_token_value: str):
    refresh_token_hash = hash_token(refresh_token_value)
    token = find_refresh_token_by_hash(refresh_token_hash)
    if not token:
        raise UnauthorizedError("Invalid refresh token")
    if token.revoked_at is not None:
        # Possible reuse of old token
        revoke_token_family(token)
        raise UnauthorizedError("Refresh token has been revoked")
    if token.expires_at < now():
        raise UnauthorizedError("Refresh token has expired")
    user = find_user_by_id(token.user_id)
    # Create new tokens
    new_access_token = create_access_token(user_id=user.id, expires_in=15 * 60)
    new_refresh_token_value = generate_secure_random_string()
    new_refresh_token_hash = hash_token(new_refresh_token_value)
    new_token = save_refresh_token(
        user_id=user.id,
        token_hash=new_refresh_token_hash,
        expires_in_days=30
    )
    # Rotate: revoke the old refresh token and link it
    token.revoked_at = now()
    token.replaced_by = new_token.id
    save(token)
    return {
        "access_token": new_access_token,
        "access_token_expires_in": 15 * 60,
        "refresh_token": new_refresh_token_value,
        "refresh_token_expires_in": 30 * 24 * 60 * 60,
        "token_type": "bearer"
    }

The function revoke_token_family(token) could revoke all tokens in the same chain so that both the attacker and the user are forced to log in again.


Storing Refresh Tokens on the Client

How you store refresh tokens on the client side is critical for security.

Common Storage Options

Client TypeStorage PlaceNotes
Web browserHttpOnly secure cookieProtected from JavaScript, but watch CSRF
Single page appHttpOnly cookie plus CSRF protectionPopular pattern
Mobile appSecure storage (Keychain, Keystore)Platform specific secure storage
Desktop appEncrypted credentials storeFor example OS keyring

For web apps, a common approach:

Then, to refresh:

  1. Frontend calls /auth/refresh with credentials: "include" so the cookie is sent.
  2. Backend reads the only refresh token from the cookie.
  3. Backend returns a new access token.

Avoid storing refresh tokens in localStorage or sessionStorage, because JavaScript can access those and XSS vulnerabilities can steal them.


Security Best Practices for Refresh Tokens

Refresh tokens are powerful, so they need extra care.

1. Use HTTPS Everywhere

Always send refresh tokens only over HTTPS.

2. Limit Lifetime

Give refresh tokens a reasonable lifetime, for example:

Balance convenience with risk. A very long lived token is almost like a password.

3. Tie Tokens to a Device or Client

Store some additional metadata:

Then you can:

Example sessions list:

DeviceLocationLast usedAction
Chrome on WindowsBerlin, Germany2026-08-27 09:14Log out
Safari on iPhoneParis, France2026-08-26 21:03Log out

Each line corresponds to a refresh token record or a group of them.

4. Invalidate on Logout

When user logs out, do not only delete tokens on the client side. Also on the server side:

Logout pseudocode:

python
def logout(refresh_token_value: str):
    refresh_token_hash = hash_token(refresh_token_value)
    token = find_refresh_token_by_hash(refresh_token_hash)
    if token:
        token.revoked_at = now()
        save(token)
    return {"detail": "Logged out"}

For cookie based tokens, you might:

5. Do Not Mix Token Types

If you use JWTs for tokens, add a type claim.

Example JWT payload for an access token:

json
{
  "sub": "42",
  "type": "access",
  "exp": 1693151220,
  "scope": "read:tasks write:tasks"
}

For a refresh token:

json
{
  "sub": "42",
  "type": "refresh",
  "exp": 1695743220
}

Then in your backend:

Never accept a refresh token in place of an access token to call normal API endpoints.

6. Use Strong Randomness

If tokens are opaque strings, generate them using a cryptographically secure random generator.

For example, in Python:

python
import secrets
def generate_secure_random_string(length: int = 64) -> str:
    return secrets.token_urlsafe(length)

This reduces the chance of guessing or brute forcing tokens.

7. Protect Against Brute Force

If an attacker tries many possible refresh tokens for a user:

Common Patterns and Scenarios

Sliding Sessions with Refresh Tokens

A sliding session extends the user's session as long as they keep using the app.

Example rule:

So the user can stay logged in as long as they are active, but if they are idle for more than 30 days, the refresh token expires and they must log in again.

You can implement a maximum session length by adding another rule:

Remember Me Checkbox

Often there is a "Remember me" checkbox on the login page.

You can map it to different token lifetimes:

Example:

python
def login(username: str, password: str, remember_me: bool):
    # ... validate user ...
    refresh_days = 30 if remember_me else 1
    # create tokens with that lifetime

Multiple Devices

Users may log in on many devices:

You can handle this by:

To log out from "all devices", you can:

To log out from a single device, you can:

Example: End to End Scenario

Imagine a user "Alice" logging into a task management app.

  1. Login
    • Alice enters username and password.
    • Backend returns:
      • Access token valid 15 minutes.
      • Refresh token valid 14 days.
    • Frontend:
      • Keeps access token in memory.
      • Stores refresh token in an HttpOnly cookie.
  2. Normal usage
    • For the next 10 minutes, Alice adds tasks.
    • Every request includes the access token.
    • Backend processes requests, everyone is happy.
  3. Access token expires
    • 15 minutes pass.
    • Next request responds with 401 Unauthorized because the access token is expired.
    • Frontend intercepts the 401 and triggers a silent refresh:
      • Calls /auth/refresh with the cookie.
    • Backend:
      • Reads the refresh token from the cookie.
      • Validates and rotates it.
      • Returns a new access token and a new refresh token.
    • Frontend:
      • Updates its in memory access token.
      • Browser cookie is updated with the new refresh token.
    • Frontend retries the original API request with the new access token, and it works.
  4. She stops using the app
    • Alice closes the browser.
    • After 2 days, she opens it again.
    • Access token is gone from memory, but cookie still holds the refresh token.
    • On first API call, app notices there is no access token, so it:
      • Calls /auth/refresh directly using the cookie.
    • Backend returns a new access token.
    • Alice does not see a login screen. This feels like "remember me".
  5. Device lost
    • Alice loses her phone.
    • She logs into the app from her laptop and opens "Active sessions".
    • Backend shows active refresh tokens with device info.
    • She clicks "Log out" on the phone session.
    • Backend revokes that phone's refresh token.
    • Even if someone finds or steals the phone, they cannot refresh tokens anymore.

This scenario shows how refresh tokens create a session like experience on top of short lived access tokens.


Summary

Refresh tokens are a core building block for secure authentication:

In your backend, you will:

Once you understand refresh tokens, you can build robust, practical authentication flows for real world backend applications.

Views: 4

Comments

Please login to add a comment.

Don't have an account? Register now!