Skip to content
Ashish.
All posts
Diagram illustrating the Token Broker pattern isolating OIDC tokens from micro-frontend shards.

Building Secure Micro-Frontends with OIDC and Token Mediation

This article covers secure micro-frontend architecture using OIDC protocols and token mediation strategies for Single Page Applications.

By Ashish SrivastavaPart 3 of Frontend Security Architecture

Building Secure Micro-Frontends with OIDC and Token Sharing

Splitting a Single Page Application (SPA) into independent shards introduces a critical security challenge: maintaining a unified session without exposing credentials. If you naively store an OIDC access token in localStorage and allow every shard to read it, a single Cross-Site Scripting (XSS) vulnerability in one module grants an attacker full access to all data. The goal is not just encryption, but minimizing the blast radius of a compromised script.

The Mechanism of Trust: PKCE and the Authorization Code Flow

In a traditional server-side application, the client secret protects the token exchange. In a pure client-side micro-frontend, there is no server secret to hide. If you use the implicit flow (which is deprecated), the token is exposed in the URL fragment, making it visible to browser history and network logs. The correct mechanism is the Authorization Code Flow with Proof Key for Code Exchange (PKCE).

PKCE mitigates the risk of authorization code interception attacks by introducing a dynamic challenge. When the billing micro-frontend redirects the user to the Identity Provider (IdP), it generates a random code_verifier and a derived code_challenge. The IdP stores the challenge. Later, when the IdP redirects back with the authorization code, the billing module must present the original code_verifier. Without this verifier, an attacker who intercepted the authorization code cannot exchange it for an access token.

// Example: Generating the PKCE challenge
const generateCodeVerifier = () => {
  return Array.from({ length: 32 }, () => 
    Math.random().toString(36).substring(2)
  ).join('');
};
 
const generateCodeChallenge = (verifier) => {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  return window.crypto.subtle.digest('SHA-256', data)
    .then(hash => {
      // RFC 7636 compliant Base64URL encoding
      const base64Url = btoa(String.fromCharCode(...new Uint8Array(hash)))
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
      return base64Url;
    });
};

This mechanism ensures that even if the authorization code is stolen during the redirect, it is useless without the ephemeral verifier generated by the specific micro-frontend instance.

The Token Boundary Problem

Once the user is authenticated, the micro-frontend architecture faces the "token mediation" dilemma. The most common mistake is storing the access token in a shared localStorage item or a non-HttpOnly cookie accessible by all subdomains.

If app.example.com and shop.example.com share a cookie with SameSite=None, an XSS attack in app can read that cookie via document.cookie and send it to a malicious server. This is a total compromise. The browser's SameSite attribute mitigates some of this, but it does not solve the internal XSS threat within the same domain.

The secure mechanism is to treat the token as a resource that should never be directly accessible to the rendering logic of the micro-frontend. Instead of passing the token to the global scope, we isolate the token storage.

Consider a scenario where billing and analytics are loaded via Module Federation. If analytics is compromised, it should not be able to read the billing token. The solution is to restrict the token's scope and visibility. We use HttpOnly cookies for the refresh token (which exchanges for access tokens) and ensure the access token is only available to the specific shard that requested it, or strictly controlled via a broker.

However, cookies have their own constraints. If the micro-frontends are on different subdomains (e.g., app.example.com and shop.example.com), a cookie set with Domain=.example.com IS accessible to all subdomains by default. The Same-Origin Policy restricts access only if the Domain attribute is not explicitly set to the parent domain. This forces a design decision: do we force all micro-frontends to live on a single subdomain, or do we implement a cross-origin token exchange?

The Broker Pattern: A Secure Vault

To handle multiple subdomains and prevent XSS token leakage, we implement a "Token Broker." This is a dedicated, isolated subdomain (e.g., auth.example.com) or a hidden iframe that holds the long-lived refresh token and the OIDC session state.

When a micro-frontend (like billing) needs to make an API call, it does not fetch the token itself. Instead, it sends a secure postMessage request to the broker. The broker validates the request (checking the origin and the micro-frontend's signature), then issues a short-lived access token scoped specifically to that micro-frontend's needs.

// In the Micro-Frontend (Billing)
const brokerWindow = document.getElementById('auth-broker').contentWindow;
 
brokerWindow.postMessage(
  { type: 'REQUEST_TOKEN', scope: 'billing:read' },
  'https://auth.example.com'
);
 
window.addEventListener('message', (event) => {
  if (event.source !== brokerWindow) return;
  
  if (event.data.type === 'TOKEN_GRANTED') {
    // Use the token immediately for the API call
    fetch('/api/billing', {
      headers: { 'Authorization': `Bearer ${event.data.token}` }
    });
    // Token is NOT stored in localStorage
  }
});

The broker uses postMessage with a strict target origin. This prevents other scripts on the page from intercepting the token response. Even if the billing module is compromised, the attacker cannot retrieve the refresh token from the broker because the broker never exposes it to the DOM. It only returns short-lived access tokens. This limits the window of opportunity for an attacker.

Integrating with Module Federation

Webpack Module Federation allows us to expose these micro-frontends dynamically. The challenge is ensuring that the authentication context is available to the remote modules without leaking it.

We configure the exposes object in the billing module to include a function that requests the token from the broker, rather than exposing the token string directly.

// In the Remote Module (billing)
// module.exports = {
//   './BillingComponent': './src/BillingComponent'
// };
 
// Security Wrapper
export const getBillingData = async () => {
  // Use the postMessage pattern defined in the Broker section
  const brokerWindow = document.getElementById('auth-broker').contentWindow;
  const token = await new Promise((resolve, reject) => {
    const handler = (event) => {
      if (event.source === brokerWindow && event.data.type === 'TOKEN_GRANTED') {
        resolve(event.data.token);
        window.removeEventListener('message', handler);
      }
    };
    brokerWindow.postMessage(
      { type: 'REQUEST_TOKEN', scope: 'billing:read' },
      'https://auth.example.com'
    );
    window.addEventListener('message', handler);
    // Timeout logic should be added here in production
  });
 
  const response = await fetch('/api/billing', {
    headers: { 'Authorization': `Bearer ${token}` }
  });
  return response.json();
};

This pattern ensures that the token is transient. It is fetched, used, and discarded. It never resides in the module's closure or the global window object longer than necessary.

Conclusion

Building secure micro-frontends is not about adding more layers of encryption; it is about architectural isolation. By enforcing the PKCE mechanism for initial authentication and replacing direct token storage with a Token Broker pattern, you prevent a single XSS vulnerability from cascading into a total account takeover. The trade-off is slightly higher latency due to the token request round-trip, but this is negligible compared to the security guarantee of never exposing the full credential set to the application logic.

While the Broker pattern adds complexity, the alternative—storing tokens in localStorage across micro-frontends—is a fundamental security flaw that no amount of input validation can fix. In a micro-frontend environment, the token is the key to the entire building; you must never leave it lying on the floor where any module can pick it up.

The industry standard is shifting toward this "zero-trust" model for frontend tokens, treating the browser as an untrusted environment where the only trusted component is the isolated broker. As micro-frontend architectures grow, the distinction between "app" and "auth" becomes less about code separation and more about data boundary enforcement.

Common Pitfalls

  1. Storing tokens in localStorage: This makes tokens instantly accessible to any XSS payload running on the page.
  2. Improper scope validation: Failing to validate the requested scope in the broker allows modules to request tokens they shouldn't have access to.
  3. Missing origin checks: Neglecting to validate the origin in postMessage events allows cross-site attackers to inject malicious token requests.

Practical Takeaways

  1. Isolate the Vault: Treat the authentication broker as a separate, untrusted environment that holds the master keys.
  2. Least Privilege: Request only the specific scopes needed for a single API call, never broad access.
  3. Ephemeral Tokens: Never store access tokens persistently; fetch them on demand and discard them immediately after use.

FAQ

Q: Can I use the Implicit Flow for micro-frontends? A: No. The Implicit Flow is deprecated because it exposes tokens in the URL fragment, making them vulnerable to logging and history-based attacks. Always use Authorization Code Flow with PKCE.

Q: How do I handle token refresh across different subdomains? A: Use a shared Domain=.example.com cookie for the refresh token, but ensure it is HttpOnly and Secure. The micro-frontends should then use the Broker pattern to exchange this refresh token for a new access token via postMessage.

Q: Is the Token Broker pattern too complex for small teams? A: While it adds architectural complexity, the alternative (storing tokens in localStorage) introduces a catastrophic security risk. For any application handling sensitive data, the Broker pattern is the recommended baseline for security.

Related posts