Token Refresh
Why token refresh matters 🔄​
Both implicit tokens and Account Management tokens have limited lifespans. Implementing a robust refresh strategy ensures users don't experience disruptions when tokens expire. This guide focuses specifically on refresh strategies and complements the Token Management guide.
This guide assumes you're familiar with the basics of token management. Make sure you've read the Token Management guide before implementing these refresh strategies.
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
2 · Refresh strategies for Implicit tokens​
Option A: Proactive refresh (before expiration)​
This approach refreshes the token before it expires, preventing any 401 errors:
import { createAnAccessToken, client } from "@epcc-sdk/sdks-shopper";
// Setup token management with auto-refresh before expiry
function setupTokenRefresh() {
let refreshTimeout;
async function refreshToken() {
const { access_token, expires_in } = await createAnAccessToken({
grant_type: "implicit",
client_id: process.env.CLIENT_ID,
});
// Store the new token
localStorage.setItem("ep_implicit_token", access_token);
// Calculate expiration time for tracking
const expiryTime = Date.now() + expires_in * 1000;
localStorage.setItem("ep_implicit_token_expiry", expiryTime.toString());
// Update SDK client headers
client.setConfig({
headers: { Authorization: `Bearer ${access_token}` },
});
// Schedule next refresh at 90% of token lifetime
const refreshTime = expires_in * 0.9 * 1000; // convert to ms
refreshTimeout = setTimeout(refreshToken, refreshTime);
console.log(
`Token refreshed. Next refresh in ${Math.floor(refreshTime / 1000 / 60)} minutes`,
);
return access_token;
}
// Clear previous timeouts on re-setup
if (refreshTimeout) clearTimeout(refreshTimeout);
// Initial token fetch
return refreshToken();
}
// Initialize on app start
setupTokenRefresh().then(() => {
console.log("Token refresh system initialized");
});
Option B: Reactive refresh (after 401 errors)​
This approach catches 401 errors, refreshes the token, and retries the failed request:
import { createAnAccessToken, client } from "@epcc-sdk/sdks-shopper";
// Setup a 401 interceptor that automatically refreshes the token
function setup401Interceptor() {
let isRefreshing = false;
let refreshPromise = null;
client.setConfig({
onError: async (err) => {
// Handle 401 Unauthorized errors
if (err.status === 401) {
// Prevent multiple refresh calls
if (!isRefreshing) {
isRefreshing = true;
try {
refreshPromise = createAnAccessToken({
grant_type: "implicit",
client_id: process.env.CLIENT_ID,
});
const { access_token, expires_in } = await refreshPromise;
// Update token in storage
localStorage.setItem("ep_implicit_token", access_token);
const expiryTime = Date.now() + expires_in * 1000;
localStorage.setItem(
"ep_implicit_token_expiry",
expiryTime.toString(),
);
// Update client authorization header
client.setConfig({
headers: { Authorization: `Bearer ${access_token}` },
});
// Retry the original request with new token
return err.retry();
} catch (refreshError) {
console.error("Token refresh failed:", refreshError);
// Handle refresh failure (redirect to login, etc)
throw err;
} finally {
isRefreshing = false;
refreshPromise = null;
}
} else if (refreshPromise) {
// Wait for the ongoing refresh to complete
await refreshPromise;
return err.retry();
}
}
// For other errors, just propagate
throw err;
},
});
}
// Initialize on app start
setup401Interceptor();
3 · 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");
}
});
4 · Combined refresh strategy​
For a complete solution, combine both token refresh mechanisms:
// Initialize both refresh mechanisms on app start
function initializeTokenRefresh() {
// Set up the implicit token refresh
setupTokenRefresh();
// Set up the 401 interceptor as a fallback
setup401Interceptor();
// Periodically check and refresh the account token if needed
setInterval(
async () => {
const refreshToken = localStorage.getItem("ep_account_refresh_token");
if (refreshToken) {
try {
await refreshAccountToken(refreshToken);
} catch (error) {
console.warn("Account token refresh failed, will retry later");
}
}
},
1000 * 60 * 60,
); // Check every hour
}
5 · Comprehensive Token Service​
Here's a complete token service that handles both token types together with refresh capabilities:
import { createAnAccessToken, client } from "@epcc-sdk/sdks-shopper";
// Comprehensive token management service
const TokenService = {
// Store tokens securely
setImplicitToken(token, expiresIn) {
localStorage.setItem("ep_implicit_token", token);
const expiryTime = Date.now() + expiresIn * 1000;
localStorage.setItem("ep_implicit_token_expiry", expiryTime.toString());
},
setAccountTokens(token, refreshToken, expiresIn) {
localStorage.setItem("ep_account_token", token);
localStorage.setItem("ep_account_refresh_token", refreshToken);
const expiryTime = Date.now() + expiresIn * 1000;
localStorage.setItem("ep_account_token_expiry", expiryTime.toString());
},
// Check token validity
isImplicitTokenExpired(bufferSeconds = 300) {
// 5 minutes buffer
const expiryTime = localStorage.getItem("ep_implicit_token_expiry");
if (!expiryTime) return true;
// Check if token will expire within buffer time
return Date.now() > parseInt(expiryTime) - bufferSeconds * 1000;
},
isAccountTokenExpired(bufferSeconds = 300) {
const expiryTime = localStorage.getItem("ep_account_token_expiry");
if (!expiryTime) return true;
return Date.now() > parseInt(expiryTime) - bufferSeconds * 1000;
},
// Get stored tokens
getImplicitToken() {
return localStorage.getItem("ep_implicit_token") ?? "";
},
getAccountToken() {
return localStorage.getItem("ep_account_token") ?? "";
},
getAccountRefreshToken() {
return localStorage.getItem("ep_account_refresh_token") ?? "";
},
// Clear tokens on logout
clearAllTokens() {
localStorage.removeItem("ep_implicit_token");
localStorage.removeItem("ep_implicit_token_expiry");
localStorage.removeItem("ep_account_token");
localStorage.removeItem("ep_account_refresh_token");
localStorage.removeItem("ep_account_token_expiry");
},
// Refresh tokens when needed
async refreshImplicitToken() {
try {
const { access_token, expires_in } = await createAnAccessToken({
grant_type: "implicit",
client_id: process.env.CLIENT_ID,
});
this.setImplicitToken(access_token, expires_in);
return access_token;
} catch (error) {
console.error("Failed to refresh implicit token:", error);
throw error;
}
},
async refreshAccountToken() {
const refreshToken = this.getAccountRefreshToken();
if (!refreshToken) {
throw new Error("No refresh token available");
}
try {
const { access_token, refresh_token, expires_in } =
await createAnAccessToken({
grant_type: "refresh_token",
client_id: process.env.CLIENT_ID,
refresh_token: refreshToken,
});
this.setAccountTokens(access_token, refresh_token, expires_in);
return access_token;
} catch (error) {
console.error("Failed to refresh account token:", error);
// Clear account tokens on failure
localStorage.removeItem("ep_account_token");
localStorage.removeItem("ep_account_refresh_token");
localStorage.removeItem("ep_account_token_expiry");
throw error;
}
},
// Initialize SDK with automatic token refresh
setupTokenRefresh() {
// Set up request interceptor to add both tokens and refresh when needed
client.interceptors.request.use(async (config) => {
// Check and refresh implicit token if needed
if (this.isImplicitTokenExpired()) {
await this.refreshImplicitToken();
}
// Add implicit token to headers
config.headers.Authorization = `Bearer ${this.getImplicitToken()}`;
// Check and refresh account token if needed
const accountToken = this.getAccountToken();
if (accountToken) {
if (this.isAccountTokenExpired() && this.getAccountRefreshToken()) {
try {
await this.refreshAccountToken();
} catch (error) {
console.error(
"Account token refresh failed during request:",
error,
);
}
}
// Add the latest account token to headers
config.headers["EP-Account-Management-Authentication-Token"] =
this.getAccountToken();
}
return config;
});
// Set up 401 interceptor as fallback
client.setConfig({
onError: async (err) => {
if (err.status === 401) {
try {
// Try refreshing the implicit token
await this.refreshImplicitToken();
// Retry the request
return err.retry();
} catch (refreshError) {
console.error("Token refresh failed on 401:", refreshError);
throw err;
}
}
// For other errors, just propagate
throw err;
},
});
},
};
// Initialize the token service
TokenService.setupTokenRefresh();
6 · Handling edge cases​
Handling multiple tabs​
For applications running in multiple browser tabs, use localStorage
events to synchronize token refreshes:
// In tab that refreshes the token
window.addEventListener("storage", (event) => {
if (event.key === "ep_implicit_token") {
// Token was updated in another tab
client.setConfig({
headers: { Authorization: `Bearer ${event.newValue}` },
});
}
});
// After refreshing token in one tab
localStorage.setItem("ep_implicit_token", newToken);
// This will trigger the storage event in other tabs
Handling network interruptions​
Add retry logic for token refresh attempts when network connectivity is unstable:
async function refreshTokenWithRetry(maxRetries = 3, delay = 1000) {
let retries = 0;
while (retries < maxRetries) {
try {
const { access_token, expires_in } = await createAnAccessToken({
grant_type: "implicit",
client_id: process.env.CLIENT_ID,
});
// Token refresh succeeded
return { access_token, expires_in };
} catch (error) {
retries++;
if (retries >= maxRetries) {
// Max retries reached, rethrow the error
throw error;
}
// Wait before retrying
await new Promise((resolve) => setTimeout(resolve, delay * retries));
}
}
}
Ensuring critical operations have fresh tokens​
For important operations like checkout, explicitly ensure a fresh token before proceeding:
async function ensureFreshTokenForCheckout() {
// Check if token is close to expiry (within 5 minutes)
if (TokenService.isImplicitTokenExpired(300)) {
await TokenService.refreshImplicitToken();
}
// Check if account token is close to expiry
if (
TokenService.getAccountToken() &&
TokenService.isAccountTokenExpired(300)
) {
await TokenService.refreshAccountToken();
}
// Proceed with checkout
return proceedWithCheckout();
}
Summary​
Implementing robust token refresh strategies is essential for maintaining seamless user experiences in your Elastic Path storefront. This guide has covered:
- Proactive refresh - Refreshing tokens before they expire
- Reactive refresh - Handling 401 errors with automatic token refresh
- Account token refresh - Using refresh tokens to maintain authenticated sessions
- Combined strategies - Implementing a comprehensive token service
- Edge case handling - Managing multiple tabs and network interruptions
By implementing these strategies in conjunction with the token storage patterns from the Token Management guide, you'll create a resilient authentication system that minimizes disruptions for your users.
For complete implementations, check out the authentication examples in the Composable Frontend repository.