Solving OIDC Silent Token Renewal, CSP Iframe Issues & API Call Loops in React

Creative Software logomark
Iyani Kalupahana
January 9, 2026

If you’re using OpenID Connect (OIDC) with a React frontend and have struggled with silent token renewal, expired tokens triggering infinite API calls, or Content Security Policy (CSP) blocking iframes, this article is for you.

After wrestling with all these issues, I finally managed to build a stable and secure solution and here’s a breakdown of the journey and how you can apply it in your own app.

The Problems I Faced

1. Silent Token Renewal Didn’t Work

OIDC offers a way to silently renew access tokens via an iframe, but this method fails with applications that have a strict CSP, particularly with frame-ancestors ‘none’.

As a consequence, tokens expire unnoticed and users are logged out without any notification.

2. Infinite API Loops After Token Expiry

Expired tokens weren’t detected early enough. When the frontend made API requests with an expired token, the server responded with 401s and our interceptor logic tried to retry the same request again and again. This led to an infinite loop of failing requests, degraded performance, and confused users.

3. Lost Redirect Path After Login

After being redirected to the login page due to an expired token, users were not returned to the original page they were on.

The Final Solution

To address these problems, I made the following structural changes:

  • Issue: Implemented Silent renew not working
    Fix: Added dedicated silentrenew.html page and fix the CSP issue
  • Issue: Token expiration API loop
    Fix: Moved token validation outside of interceptors
  • Issue: CSP iframe blocked
    Fix: Updated server CSP policy to allow 'self' for iframes
  • Issue: Lost redirect path
    Fix: Used state parameter or redirectTo query in login flow

Key Fixes Explained

1. silentrenew.html:

Silent token renewals happen in a hidden iframe. For this, you need a static HTML file that executes the silent callback:

<!-- silentrenew.html -->
<script src="https://unpkg.com/oidc-client-ts/dist/umd/oidc-client-ts.min.js"></script>
<script>
  new Oidc.UserManager().signinSilentCallback();
</script>

Make sure to add this page’s URL to your Identity Provider’s Redirect URIs and update your CSP:

Redirect URIs:

https://your-app-url/login-redirect
https://your-app-url/silentrenew.html

Post-Logout Redirect URI:

https://your-app-url/login

Enable Offline Access:

AllowOfflineAccess: true

Configure the Content Security Policy (CSP) in the Identity server project:

Content-Security-Policy: frame-ancestors 'self'

2. Validate Tokens Before Making API Calls

A common mistake is to do token renewal inside your axios request or response interceptors.

While it seems convenient, this causes major issues:

  • Token renewal might be triggered in parallel by multiple requests.
  • Expired tokens may still be sent before renewal completes.
  • 401 responses can trigger infinite retry loops.

Solution:
Move token validation logic outside the interceptor, into a centralized place that runs before any API call is made.

function getValidAccessToken() {
  const user = userManager.configuration.getUser() //Calls to get the new token
  if (!user || user.expired) {
    return null
  }
  return user.access_token
}

const token = await getValidAccessToken()
if (!token) {
  redirectToLoginWithState()
} else {
  axios.defaults.headers.Authorization = `Bearer ${token}`
}

Why I Removed Token Logic From Axios Interceptors

Interceptors are great but not for token renewal.

Before:

axios.interceptors.response.use(async (response) => {
  if (response.status === 401) {
    await renewToken()
    return axios(response.config) // ❌ Risk of loops
  }
})

Problems:

  • If many API calls are made at once with an expired token, all trigger renewToken().
  • Initial requests are made again before the new token becomes available.

3. Managing Expiry with Events

Lifecycle events such as addUserLoaded and addSilentRenewError are provided by the OIDC client, making them useful for tasks such as token maintenance.

import { User, UserManager, UserManagerSettings, WebStorageStateStore } from 'oidc-client-ts'

const configuration = new UserManager(config)

configuration.events.addUserLoaded(user => {
  storeTokenGlobally(user.access_token, user.expires_at)
})

configuration.events.addSilentRenewError(() => {
  // Fallback: maybe trigger login redirect after some time
})

4. Handling Login Redirects Cleanly

When redirecting to login, I now preserve the user’s current route using query parameters or state:

const targetPath = window.location.pathname + window.location.search
const loginUrl = `/login?redirectTo=${encodeURIComponent(targetPath)}`
window.location.replace(loginUrl)

After successful login, the app reads the redirectTo value and sends the user back where they left off.

Bonus: Environment Setup

Be sure to include the offline_access scope if you want to use refresh tokens or extended silent renewal capabilities:

REACT_APP_IDENTITY_SERVER_SCOPE='openid offline_access'

Make sure to have the following default settings as well.

const config: UserManagerSettings = {
  authority: appConfig.identityServer.authority,
  client_id: appConfig.identityServer.client_id,
  redirect_uri: `${window.location.origin}/login-redirect`,
  client_secret: appConfig.identityServer.clientSecret,
  scope: appConfig.identityServer.scope,
  response_type: 'code',
  post_logout_redirect_uri: `${window.location.origin}/login`,
  automaticSilentRenew: true,
  silent_redirect_uri: `${window.location.origin}/silentrenew.html`,
  accessTokenExpiringNotificationTimeInSeconds: 120,
  includeIdTokenInSilentRenew: true,
  revokeTokensOnSignout: false,
}


Results

With these changes:

✅ Silent token renew works consistently
✅ Infinite API loops are eliminated
✅ Redirects are handled gracefully
✅ CSP is respected and secure

Share this post
Creative Software logomark
Iyani Kalupahana
January 9, 2026
5 min read