Skip to content

Token Refresh

When the access token expires, oidc-js uses the refresh token to obtain a new access token without requiring the user to log in again.

By default, OidcClient proactively refreshes the access token before it expires using a short setInterval poll. Every tick compares the current time against expiresAt - expiryBuffer and triggers a refresh when the token is about to expire. This means the token is always fresh — no 401s, no interceptors needed, no fallback flash.

This is enabled by default. You can configure the behavior with two options:

const config = {
issuer: "https://auth.example.com",
clientId: "my-app",
redirectUri: "http://localhost:5173/callback",
expiryBuffer: 60, // refresh 60s before expiry (default: 30)
autoRefresh: true, // enabled by default
autoRefreshInterval: 10, // polling interval in seconds (default: 10)
};

The polling interval is lightweight — one number comparison per tick, no re-renders or network calls until a refresh is actually needed. The approach is drift-proof and sleep-proof: after a laptop wakes from sleep, the very first tick catches the expiration.

Set autoRefresh: false to rely on reactive mechanisms instead (interceptors, RequireAuth re-mount):

<AuthProvider config={{ ...config, autoRefresh: false }}>
<App />
</AuthProvider>

RequireAuth handles refresh reactively. When a user navigates to a protected route with an expired token:

  1. Checks tokens.expiresAt using isExpiredAt() from core (with an optional buffer)
  2. If expired, calls actions.refresh()
  3. If refresh succeeds, renders the children with new tokens
  4. If refresh fails, redirects to login
<RequireAuth>
<Dashboard />
</RequireAuth>

You can set an expiryBuffer (in seconds) on the provider config to refresh the token early, accounting for clock skew and network latency. The default buffer is 30 seconds.

<AuthProvider config={{ ...config, expiryBuffer: 60 }}>
<App />
</AuthProvider>

You can also trigger a refresh manually:

import { useAuth } from "oidc-js-react";
function RefreshButton() {
const { actions } = useAuth();
async function handleRefresh() {
try {
await actions.refresh();
} catch (error) {
console.error("Refresh failed:", error);
}
}
return <button onClick={handleRefresh}>Refresh Token</button>;
}

The tokens object from useAuth includes expiry information:

const { tokens } = useAuth();
// Unix timestamp in seconds when the access token expires
console.log(tokens.expiresAt);

Use the helper functions from oidc-js-core to work with token expiry:

import { isExpiredAt, timeUntilExpiry } from "oidc-js-core";
const { tokens } = useAuth();
// Check if expired (includes a default 30-second buffer)
const expired = isExpiredAt(tokens.expiresAt);
// Check with a custom buffer (in seconds)
const expiringSoon = isExpiredAt(tokens.expiresAt, 120);
// Get seconds remaining until expiry
const secondsLeft = timeUntilExpiry(tokens.expiresAt);

To receive a refresh token, include offline_access in your scopes:

const config = {
issuer: "https://auth.example.com",
clientId: "my-app",
redirectUri: "http://localhost:5173/callback",
scopes: ["openid", "profile", "email", "offline_access"],
};

Without offline_access, the IdP may not issue a refresh token, and actions.refresh() will throw.