How Auth0‑Auth‑JS Handles Token Expiration and When to Re‑exchange Tokens

The Auth0‑Auth‑JS SDK automatically detects expired access tokens by comparing the stored expiresAt Unix timestamp against the current time, and automatically refreshes them using a stored refresh token; you only need to manually re‑exchange tokens when the SDK throws a TokenByRefreshTokenError due to a missing or invalid refresh token.

The Auth0‑Auth‑JS SDK manages OAuth 2.0 token lifecycles internally, eliminating the need for manual expiration checks in most scenarios. By storing metadata alongside every access token, the SDK determines exactly when to use a refresh token grant versus when to force a full re‑authentication. This article examines the source code in auth0/auth0-auth-js to explain how the expiresAt field drives automatic refresh logic and identifies the specific conditions that require manual token re‑exchange.

How the SDK Stores Token Expiration Metadata

Every token received from Auth0 is stored with an expiresAt property representing the absolute expiration time as a Unix timestamp in seconds. This metadata enables the SDK to evaluate token validity without external API calls.

TokenSet and ConnectionTokenSet Types

The core token types are defined in packages/auth0-server-js/src/types.ts. The TokenSet interface (lines 49‑60) includes:

export interface TokenSet {
  accessToken: string;
  refreshToken?: string;
  expiresAt: number;  // Unix timestamp in seconds
  scope?: string;
  // ...
}

The ConnectionTokenSet type follows an identical pattern for connection‑specific credentials. When the SDK receives a token response from Auth0, it immediately calculates expiresAt from the expires_in value and stores it alongside the access token.

Automatic Token Expiration Detection

When your application requests an access token via getAccessToken(), the SDK performs an internal validity check before returning cached credentials.

The getAccessToken() Validation Flow

In packages/auth0-server-js/src/server-client.ts (lines 96‑100 and 400‑406), the SDK executes the following logic:

  1. Load session state from the configured StateStore using #stateStore.get.
  2. Locate the matching TokenSet for the requested audience and scope.
  3. Compare expiresAt against current time using Date.now() / 1000.
if (tokenSet && tokenSet.expiresAt > Date.now() / 1000) {
  return tokenSet;  // Token still valid, return from cache
}

If the current time exceeds expiresAt, the SDK immediately initiates a refresh token grant rather than returning the expired access token.

Connection-Specific Token Handling

The same expiration logic applies to connection‑specific tokens via getAccessTokenForConnection() (lines 548‑571). Whether fetching tokens for a social connection or enterprise IdP, the SDK evaluates expiresAt and attempts automatic refresh using the stored session refresh token.

The Automatic Refresh Token Grant

When getAccessToken() detects an expired token, the SDK attempts to acquire a new access token without requiring manual intervention.

When the SDK Triggers a Refresh

If the expiresAt check fails, the SDK verifies the presence of a refresh token in stateData?.refreshToken (lines 408‑412). If absent, it throws TokenByRefreshTokenError with the message "The access token has expired and a refresh token was not provided."

When a refresh token exists, the SDK:

  1. Constructs TokenByRefreshTokenOptions preserving the original audience and scope.
  2. Calls authClient.getTokenByRefreshToken() to exchange the refresh token.
  3. Receives a new access token with updated expiresAt and optional new refresh token.

Token Rotation Handling

If Auth0 rotates the refresh token during the grant, the SDK captures the new value in updateStateData() (lines 44‑46 of packages/auth0-server-js/src/state/utils.ts):

stateData.refreshToken = tokenEndpointResponse.refreshToken ?? stateData.refreshToken;

This ensures subsequent refresh attempts use the latest rotated token.

State Persistence

After successfully refreshing, updateStateData() (lines 24‑48) merges the new token response into the stored session. The SDK then persists the updated state via #stateStore.set (lines 422‑425) and returns the fresh TokenSet to the caller.

When to Manually Re‑exchange Tokens

Despite automatic handling, two specific scenarios require your application to initiate a manual re‑exchange (full re‑authentication).

Missing Refresh Token Scenarios

If the initial authentication did not request the offline_access scope, the session contains no refreshToken. When getAccessToken() discovers an expired access token and finds stateData?.refreshToken is undefined, it throws TokenByRefreshTokenError. Your application must catch this error and redirect the user to the authorization endpoint:

import { TokenByRefreshTokenError } from '@auth0/auth0-auth-js';

try {
  const token = await auth0.getAccessToken();
} catch (err) {
  if (err instanceof TokenByRefreshTokenError) {
    // No refresh token available; force re-login
    await auth0.loginWithRedirect({
      appState: { returnTo: window.location.pathname }
    });
  }
}

Handling Invalid or Revoked Refresh Tokens

If the refresh token itself is expired, revoked, or invalid, the token endpoint returns an error. The SDK propagates this as TokenByRefreshTokenError with code token_by_refresh_token_error. In this case, the refresh grant has failed and the user must re‑authenticate to obtain a fresh authorization code and new tokens.

Practical Implementation Examples

Automatic Expiration Handling

The standard pattern lets the SDK manage expiration entirely. Simply call getAccessToken() before each API request:

async function fetchUserData() {
  // SDK checks expiresAt and refreshes automatically if needed
  const { accessToken } = await auth0.getAccessToken();
  
  const response = await fetch('https://api.example.com/user', {
    headers: { Authorization: `Bearer ${accessToken}` }
  });
  
  return response.json();
}

Defensive Error Handling for Production

Implement error boundaries to handle cases where automatic refresh is impossible:

async function safeGetToken() {
  try {
    return await auth0.getAccessToken();
  } catch (error) {
    if (error instanceof TokenByRefreshTokenError) {
      // Log and redirect to login
      console.error('Token expired without refresh capability');
      await auth0.loginWithRedirect();
      return null;
    }
    throw error;  // Re-throw unexpected errors
  }
}

Connection-Specific Tokens with Expiration Checks

For downstream identity providers, use the same automatic flow:

async function getEnterpriseToken(connectionName: string) {
  const tokenSet = await auth0.getAccessTokenForConnection({
    connection: connectionName,
    loginHint: 'user@enterprise.com'
  });
  
  // tokenSet.expiresAt reflects the new expiration after any refresh
  return tokenSet.accessToken;
}

Summary

  • The SDK stores expiresAt as a Unix timestamp in TokenSet and ConnectionTokenSet types defined in packages/auth0-server-js/src/types.ts.
  • Automatic detection occurs in getAccessToken() (lines 400‑406), which compares expiresAt against Date.now() / 1000 before returning cached tokens.
  • Automatic refresh uses the stored refresh token via getTokenByRefreshToken() (lines 414‑420) and updates state through updateStateData() in packages/auth0-server-js/src/state/utils.ts.
  • Manual re‑exchange is required only when TokenByRefreshTokenError is thrown, indicating either a missing refresh token (no offline_access scope) or a failed refresh grant (revoked/invalid token).
  • Refresh token rotation is handled automatically by preserving the new refresh token from the token endpoint response back into the session store.

Frequently Asked Questions

What happens if I don't request the offline_access scope during login?

If you omit offline_access, Auth0 does not issue a refresh token. When the access token expires, getAccessToken() throws TokenByRefreshTokenError because stateData?.refreshToken is undefined (lines 408‑412). Your application must catch this error and redirect the user through a new login flow to obtain fresh tokens.

How does the SDK handle refresh token rotation?

When Auth0 returns a new refresh token during a successful refresh grant, updateStateData() in packages/auth0-server-js/src/state/utils.ts (lines 44‑46) stores the new value using the nullish coalescing operator: tokenEndpointResponse.refreshToken ?? stateData.refreshToken. This ensures the session always contains the latest rotated token for subsequent requests.

Can I check token expiration manually before calling getAccessToken()?

While you could inspect the session store directly, the SDK does not expose the raw expiresAt value through public APIs for manual checking. The intended pattern is to call getAccessToken() and allow the SDK to handle the expiration check internally. This abstraction prevents race conditions and ensures atomic refresh operations.

What specific error should I catch to handle re-authentication scenarios?

Catch TokenByRefreshTokenError imported from @auth0/auth0-auth-js. This error indicates that the access token has expired and automatic refresh is impossible—either because no refresh token was stored or because the refresh grant failed. When caught, redirect the user to loginWithRedirect() to initiate a fresh authentication flow.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →