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

> Learn how Auth0-Auth-JS handles token expiration automatically and when you need to re-exchange tokens. Discover clear guidance for your application.

- Repository: [Auth0/auth0-auth-js](https://github.com/auth0/auth0-auth-js)
- Tags: deep-dive
- Published: 2026-02-25

---

**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`](https://github.com/auth0/auth0-auth-js/blob/main/packages/auth0-server-js/src/types.ts). The `TokenSet` interface (lines 49‑60) includes:

```typescript
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`](https://github.com/auth0/auth0-auth-js/blob/main/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`.

```typescript
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`](https://github.com/auth0/auth0-auth-js/blob/main/packages/auth0-server-js/src/state/utils.ts)):

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/auth0/auth0-auth-js/blob/main/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`](https://github.com/auth0/auth0-auth-js/blob/main/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`](https://github.com/auth0/auth0-auth-js/blob/main/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.