Skip to main content

Token Refresh

Why token refresh matters 🔄​

Both implicit tokens and Account Management tokens have limited lifespans. The new Elastic Path SDK (@epcc-sdk/sdks-shopper) handles token refresh automatically, but understanding the process helps you customize behavior for your specific needs.

info

With configureClient or createShopperClient, token refresh is handled automatically by default. This guide shows how to customize that behavior or implement manual refresh when needed.


1 · Understanding token expiration​

Implicit tokens expire after approximately 1 hour (3600 seconds).

Account Management tokens have configurable expiration times:

  • Default expiration can be set by store administrators using account_management_authentication_token_timeout_secs
  • Useful for implementing idle timeout security features

The SDK handles this automatically by default. You can customize this behavior through custom storage adapters.

The new SDK handles token refresh automatically when you use configureClient or createShopperClient:

import { configureClient } from "@epcc-sdk/sdks-shopper";

// The SDK automatically:
// - Fetches tokens on first API call
// - Refreshes before expiry
// - Retries on 401 errors
export const client = configureClient(
{ baseUrl: "https://useast.api.elasticpath.com" },
{
clientId: process.env.CLIENT_ID,
storage: "localStorage"
}
);

No manual refresh code needed! The SDK handles everything internally.

Customizing refresh behavior​

To customize token refresh behavior, you would implement a custom storage adapter that controls when and how tokens are refreshed. The SDK's automatic refresh behavior is built-in and not directly configurable through options.

3 · Manual token refresh​

If you need direct control over token refresh (e.g., for custom authentication flows):

import { createShopperClient } from "@epcc-sdk/sdks-shopper";

const { client, auth } = createShopperClient(
{ baseUrl: "https://useast.api.elasticpath.com" },
{
clientId: process.env.CLIENT_ID
// Note: To disable automatic token management,
// you would need to implement custom storage that
// controls when tokens are fetched/refreshed
}
);

// Manually get and refresh tokens
async function manualTokenManagement() {
// Get valid token (auto-refreshes if needed)
const token = await auth.getValidAccessToken();
console.log("Token obtained:", token);

// Get current token without refresh
const snapshot = await auth.getSnapshot();
console.log("Current token:", snapshot?.access_token);

// Force manual refresh
const newToken = await auth.refresh();
console.log("Token refreshed:", newToken);
}

Implementing custom refresh logic​

import { createShopperClient } from "@epcc-sdk/sdks-shopper";

const { client, auth } = createShopperClient(
{ baseUrl: "https://useast.api.elasticpath.com" },
{
clientId: process.env.CLIENT_ID,
storage: {
get: async (key) => {
// Custom storage retrieval
const stored = localStorage.getItem(key);
if (stored) {
const data = JSON.parse(stored);
// Check custom expiry logic
if (data.expiresAt && Date.now() > data.expiresAt - 300000) {
// Refresh 5 minutes before expiry
return null; // Force refresh
}
return data.token;
}
return null;
},
set: async (key, value) => {
// Store with custom expiry tracking
localStorage.setItem(key, JSON.stringify({
token: value,
expiresAt: Date.now() + 3600000, // 1 hour
refreshedAt: Date.now()
}));
},
remove: async (key) => {
localStorage.removeItem(key);
}
}
}
);

4 · Refreshing Account Management tokens​

Account Management tokens can be refreshed using the refresh token that's provided during initial authentication:

import { createAnAccessToken, client } from "@epcc-sdk/sdks-shopper";

// Setup Account token refresh
function setupAccountTokenRefresh() {
let refreshTimeout;

async function refreshAccountToken() {
const refreshToken = localStorage.getItem("ep_account_refresh_token");

if (!refreshToken) {
console.error("No refresh token available");
return null;
}

try {
const { access_token, refresh_token, expires_in } =
await createAnAccessToken({
grant_type: "refresh_token",
client_id: process.env.CLIENT_ID,
refresh_token: refreshToken,
});

// Store the new tokens
localStorage.setItem("ep_account_token", access_token);
localStorage.setItem("ep_account_refresh_token", refresh_token);

// Calculate expiration time
const expiryTime = Date.now() + expires_in * 1000;
localStorage.setItem("ep_account_token_expiry", expiryTime.toString());

// Schedule next refresh at 80% of token lifetime
const refreshTime = expires_in * 0.8 * 1000;
refreshTimeout = setTimeout(refreshAccountToken, refreshTime);

return access_token;
} catch (error) {
console.error("Account token refresh failed:", error);
// Clear tokens on refresh failure - user needs to re-login
localStorage.removeItem("ep_account_token");
localStorage.removeItem("ep_account_refresh_token");
localStorage.removeItem("ep_account_token_expiry");
return null;
}
}

// Clear previous timeouts on re-setup
if (refreshTimeout) clearTimeout(refreshTimeout);

// Initial check - if we have a refresh token, try to refresh
const hasRefreshToken = !!localStorage.getItem("ep_account_refresh_token");
if (hasRefreshToken) {
return refreshAccountToken();
}

return Promise.resolve(null);
}

// Initialize on app start
setupAccountTokenRefresh().then((token) => {
if (token) {
console.log("Account token refreshed");
}
});

5 · Handling both token types together​

When using both implicit and Account Management tokens, the SDK can manage both:

For a complete solution, combine both token refresh mechanisms:

import { configureClient } from "@epcc-sdk/sdks-shopper";

// Configure client with custom auth handling for both tokens
export const client = configureClient(
{ baseUrl: "https://useast.api.elasticpath.com" },
{
clientId: process.env.CLIENT_ID,
storage: "localStorage",
// The SDK automatically manages the implicit token
// You handle the Account Management token separately
interceptors: {
request: async (config) => {
// The SDK already added the implicit token
// Add Account Management token if available
const accountToken = localStorage.getItem("ep_account_token");
if (accountToken) {
config.headers["EP-Account-Management-Authentication-Token"] = accountToken;
}
return config;
}
}
}
);

// Separate function to refresh Account Management token
async function refreshAccountToken() {
const refreshToken = localStorage.getItem("ep_account_refresh_token");
if (!refreshToken) return;

try {
// Call your Account Management token refresh endpoint
const response = await fetch("/api/auth/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken })
});

const { access_token, refresh_token: newRefreshToken } = await response.json();
localStorage.setItem("ep_account_token", access_token);
localStorage.setItem("ep_account_refresh_token", newRefreshToken);
} catch (error) {
console.error("Account token refresh failed:", error);
// Clear tokens and redirect to login
localStorage.removeItem("ep_account_token");
localStorage.removeItem("ep_account_refresh_token");
}
}

6 · Monitoring token state​

To monitor token state and handle authentication events in your application:

import { createShopperClient } from "@epcc-sdk/sdks-shopper";

const { client, auth } = createShopperClient(
{ baseUrl: "https://useast.api.elasticpath.com" },
{
clientId: process.env.CLIENT_ID,
storage: "localStorage"
}
);

// Check authentication state
async function checkAuthState() {
// Get current token snapshot
const snapshot = await auth.getSnapshot();
const isAuthenticated = !!snapshot?.access_token;
console.log("Authenticated:", isAuthenticated);

if (!isAuthenticated) {
// Handle unauthenticated state
window.location.href = "/login";
}
}

// Periodically check auth state if needed
setInterval(checkAuthState, 60000); // Check every minute

7 · Advanced scenarios​

Force token refresh​

For critical operations, force a fresh token:

import { createShopperClient } from "@epcc-sdk/sdks-shopper";

const { client, auth } = createShopperClient(
{ baseUrl: "https://useast.api.elasticpath.com" },
{ clientId: process.env.CLIENT_ID }
);

// Ensure fresh token before checkout
async function startCheckout() {
// Force a token refresh
await auth.refresh();

// Proceed with checkout
return initiateCheckout();
}

Handling multiple tabs​

When using localStorage as the storage option, tokens are naturally shared across tabs since localStorage is synchronized across all tabs of the same origin:

import { configureClient } from "@epcc-sdk/sdks-shopper";

// Using localStorage enables natural cross-tab token sharing
export const client = configureClient(
{ baseUrl: "https://useast.api.elasticpath.com" },
{
clientId: process.env.CLIENT_ID,
storage: "localStorage"
}
);

If you need to react to token changes from other tabs, you can use the browser's native storage event:

// Listen for token changes from other tabs
window.addEventListener('storage', (event) => {
if (event.key === '_store_ep_credentials' || event.key?.startsWith('ep_')) {
console.log('Token updated in another tab');
// Your custom handling here
}
});

Handling network interruptions​

The SDK includes built-in retry logic for failed requests. To implement custom retry behavior, you can wrap API calls with your own retry logic:

async function callWithRetry(apiCall, maxRetries = 3, delay = 1000) {
let lastError;

for (let i = 0; i < maxRetries; i++) {
try {
return await apiCall();
} catch (error) {
lastError = error;
console.log(`Attempt ${i + 1} failed:`, error.message);

if (i < maxRetries - 1) {
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
}
}
}

throw lastError;
}

// Usage
const products = await callWithRetry(() => getByContextAllProducts());

Server-side token refresh (Next.js example)​

For SSR applications, handle token refresh on the server:

// app/api/auth/refresh/route.ts
import { createShopperClient } from "@epcc-sdk/sdks-shopper";
import { cookies } from "next/headers";

export async function POST() {
const cookieStore = cookies();
const currentToken = cookieStore.get("ep_token");

const { client, auth } = createShopperClient(
{ baseUrl: process.env.EPCC_BASE_URL },
{
clientId: process.env.EPCC_CLIENT_ID,
storage: {
get: async () => currentToken?.value,
set: async (key, value) => {
cookieStore.set("ep_token", value, {
httpOnly: true,
secure: true,
sameSite: "lax",
maxAge: 3600 // 1 hour
});
},
remove: async () => {
cookieStore.delete("ep_token");
}
}
}
);

try {
const newToken = await auth.refresh();
return Response.json({ success: true });
} catch (error) {
return Response.json({ error: "Refresh failed" }, { status: 401 });
}
}

Summary​

The new Elastic Path SDK (@epcc-sdk/sdks-shopper) dramatically simplifies token refresh:

  1. Automatic by default - The SDK handles token refresh automatically with configureClient
  2. Customizable behavior - Override refresh timing, retry logic, and storage
  3. Event-driven - React to token events for custom workflows
  4. Cross-tab sync - Automatic synchronization when using localStorage
  5. SSR-friendly - Works with server-side rendering and custom storage adapters

For most use cases, simply using configureClient with default settings provides a complete token refresh solution. Only implement custom refresh logic when you have specific requirements beyond the SDK's automatic handling.

For complete implementations, check out the authentication examples in the Composable Frontend repository.