Skip to content

    Frontend System Design

    Sharing Authentication Across Micro-Frontends

    Decide who owns sign-in in a micro-frontend shell, how cookies and tokens reach each fragment, and where OAuth with PKCE fits, with the trade-offs named.

    By Published 12 min read

    • frontend-system-design
    • micro-frontends
    • authentication
    • cookies
    • oauth
    Share

    Standard lessonadvancedFrontend System Design

    Before this: Cross-App Communication in Micro-Frontends, Token Storage and Secure Cookies (coming soon)

    What you'll learn: By the end of this lesson you will be able to say which part of a micro-frontend system should own sign-in, work out from origins and sites which fragments can see which cookies, compare a same-origin backend-for-frontend cookie session, a parent-domain cookie and an in-memory token in the shell, place the OAuth 2.0 authorization code flow with PKCE in that picture, and design the small contract through which fragments ask for authenticated calls without ever holding credentials.

    Think of an office building. Reception checks your identity once and gives you a badge, and every floor has a door reader that trusts it. Nobody expects each floor to run its own front desk. A micro-frontend system has the same shape: several teams ship fragments of one page, and the user expects to sign in once. The question is who plays reception, and what the doors actually check.

    The running example is an online shop with a shell (the host page that loads everything else) at app.example.com, and three fragments owned by three teams: catalogue, cart and account. A gateway at api.example.com serves the data. The names are illustrative. If you have not read how browsers decide what a page may read, start with The Same-Origin Policy and CORS, because everything below rests on it.

    The core idea

    1. One component owns sign-in. Usually the shell, with a backend it talks to. Fragments never run a login of their own. The article that popularised the style agrees: authentication "should be owned by the container application" (Cam Jackson, martinfowler.com, 2019).
    2. Fragments ask for authenticated calls; they do not hold credentials. The less code that can read a secret, the fewer places it can leak from.
    3. What protects the session is the browser's rules, not team conventions. Origins, sites and cookie attributes decide who can read or receive a credential. A convention such as "fragments must not read the token" is not a control if their code runs inside the same page.

    Which boundary do you actually have?

    Two words matter, and they are not synonyms. An origin is scheme, host and port together. A site is the registrable domain (a public suffix plus the label before it), and MDN notes it ignores the port and, by default, the scheme. https://app.example.com and https://cart.example.com are different origins but the same site. Cookies follow site and host rules, scripts follow origin rules.

    Request or page Same origin as https://app.example.com? Same site?
    https://app.example.com/cart Yes Yes
    https://cart.example.com No (different host) Yes (example.com)
    https://app.example.com:8443 No (different port) Yes (port ignored)
    https://shop.example.net No No

    Now the composition model decides the boundary:

    • Fragments loaded as JavaScript into the shell's page (build-time packages, runtime module loading) execute with the shell's origin. RFC 10017 says it plainly: malicious JavaScript "has the same privileges as the legitimate application code". Any fragment's code, or any library it ships, can reach whatever the shell's code can reach in that page.
    • Fragments in iframes from another origin are separated by the same-origin policy. MDN's sandbox attribute can tighten that further, and warns that combining allow-scripts and allow-same-origin for a same-origin embedded document lets it remove its own sandbox.

    Figure 1. Where credentials live in a shell-and-fragments system

    A shell owns the session; fragments call through it to a backend for frontendCatalogue fragment to Shell (authFetch); Cart fragment to Shell (authFetch); Shell to Backend for frontend (cookie session); Backend for frontend to Identity provider (code flow); Backend for frontend to Product APIs (bearer token)BROWSER PAGESERVER SIDEIdentity provider: OAuth and OIDCIdentityproviderOAuth and OIDCCatalogue fragment: no credentialsCataloguefragmentno credentialsCart fragment: no credentialsCart fragmentno credentialsShell: owns sign-inShellowns sign-inBackend for frontend: holds tokensBackend forfrontendholds tokensProduct APIs: resource serversProduct APIsresource serversauthFetchauthFetchcookie sessioncode flowbearer token

    How to read this: fragments never talk to the identity provider or hold a token. They call a function the shell gives them, and the backend for frontend keeps the tokens. This is one of three patterns compared below.

    The rules below come from MDN's Set-Cookie page and the IETF cookie specification (RFC 6265bis, still a draft at the time of writing, version 22 of December 2025).

    • Domain omitted: the cookie is host-only and returns only to the host that set it. Domain=example.com: it goes to that domain and all its subdomains. It cannot be a public suffix such as com.
    • HttpOnly: JavaScript cannot read the cookie through document.cookie. It is still sent with JavaScript-initiated fetch() calls.
    • Secure: sent only over https: (localhost excepted).
    • SameSite: Strict sends the cookie only on same-site requests; Lax also sends it on cross-site top-level navigations with a safe method; None sends it everywhere and requires Secure. Some browsers default to Lax when the attribute is missing, so do not rely on the default. The draft specification warns that Lax is "defense in depth" and "does not offer a robust defense against CSRF".
    • __Host- prefix: the cookie must be Secure, have Path=/ and must not have a Domain attribute. Browsers that do not support prefixes accept the cookie anyway, so the prefix is a safeguard, not a guarantee.
    • Cookies do not isolate by port. The draft notes that cookies for a host are shared across all its ports, unlike the same-origin policy.

    A minimal illustration of the two choices for the shop (Illustration 1, minimal):

    Set-Cookie: __Host-session=opaque-id; Secure; HttpOnly; Path=/; SameSite=Lax
    Set-Cookie: session=opaque-id; Secure; HttpOnly; Domain=example.com; Path=/; SameSite=Lax

    The first is host-only: only app.example.com receives it. The second goes to every subdomain of example.com and cannot use the __Host- prefix. Both values are placeholders for an unguessable identifier, never a readable token.

    Three ways to give fragments a session

    Pattern A: same-origin backend for frontend (BFF) with a cookie session. RFC 10017 (OAuth 2.0 for Browser-Based Applications, a Best Current Practice published in August 2026) describes a confidential server-side component that performs the OAuth flow, keeps the tokens, hands the browser only a cookie, and proxies calls to the APIs. The document calls this the most secure of its three patterns and "strongly recommended" for applications that handle sensitive or personal data. It says the BFF "MUST enable the HttpOnly flag" on its cookies. Fragments call /api/... on their own origin and the browser attaches the cookie. No token is ever in JavaScript.

    What it does not do: an HttpOnly cookie stops a script from reading the value, but any fragment script can still make requests that carry it. Same-origin code is trusted code.

    Pattern B: parent-domain cookie. Each fragment is served from its own subdomain, and the session cookie sets Domain=example.com so every subdomain receives it. It works without a shared origin, but the cookie reaches every subdomain, including forgotten ones. RFC 10017 notes that SameSite "falls short" when a BFF is hosted alongside other applications within the same site, because a subdomain takeover of b.example.com can enable CSRF against a.example.com's BFF. You then need a real CSRF defence; the RFC suggests requiring a custom request header, which forces a preflight (see CORS in Practice).

    Pattern C: token in the shell's memory. The shell, as an OAuth client in the browser, holds an access token in a variable and gives fragments an authFetch wrapper. RFC 10017 ranks the pure browser client last ("vulnerable to all attack scenarios"). Its middle option is a token-mediating backend: a server keeps the refresh token and the browser gets only access tokens. In-memory storage "limits the exposure of the tokens to the current execution context only", but tokens vanish on reload.

    Why not localStorage? The trade-off is stated in several places. RFC 10017: "localStorage does not protect against unauthorized access from malicious JavaScript", and none of the storage options "can fully mitigate token exfiltration when the attacker can execute malicious code". OWASP's HTML5 Security Cheat Sheet: "Do not store session identifiers in local storage as the data is always accessible by JavaScript." Its advantage is persistence across reloads and tabs; in a page where many teams' code shares one origin, the exposure usually outweighs that.

    Pattern Token visible to fragment code? Persists across reload Main weakness Needs
    A. Same-origin BFF, cookie No Yes (cookie) A fragment can still make requests as the user Same-origin routing, CSRF care
    B. Parent-domain cookie No Yes Cookie reaches every subdomain CSRF defence, subdomain hygiene
    C. In-memory token, token-mediating backend Reachable by shell-origin code Short-lived Any script in the page can call authFetch or read globals Renewal logic
    localStorage token Yes, to all same-origin code Yes Exposed to any malicious script Avoid for credentials

    Figure 2. Choosing a pattern

    flowchart TD accTitle: Choosing a session pattern for micro-frontends accDescr: A decision tree. With no server available, keep tokens in shell memory. With a server, use a same-origin backend for frontend if all fragments share one origin; otherwise a parent-domain cookie with CSRF defences if all hosts are yours, or sandboxed iframes with a narrow message contract if not. Q0{"Can you run a server
    for the shell?"} -->|"no"| C["Pattern C
    token in shell memory,
    PKCE in the browser"] Q0 -->|"yes"| Q1{"Can every fragment be
    served under one origin?"} Q1 -->|"yes"| A["Pattern A
    same-origin BFF,
    host-only cookie"] Q1 -->|"no, separate hosts"| Q2{"Are all hosts
    under your control?"} Q2 -->|"yes"| B["Pattern B
    parent-domain cookie
    plus CSRF defence"] Q2 -->|"no"| I["Sandboxed iframe
    and a narrow
    postMessage contract"]

    How to read this: the tree is a starting point; the answer still depends on how sensitive the data is.

    OAuth, OIDC and PKCE in the picture

    OAuth 2.0 (RFC 6749) lets a client obtain an access token from an authorization server without ever seeing the user's password. Its client types matter here: a confidential client can keep credentials secret (a server), a public client cannot (RFC 6749 lists browser-based apps as public). OpenID Connect is "a simple identity layer on top of the OAuth 2.0 protocol": it adds an ID token (a JWT with claims such as iss, sub, aud, exp) that tells the client who signed in.

    In the authorization code flow the client sends the user to the authorization server, gets a code on a redirect, and exchanges it for tokens at the token endpoint. PKCE (RFC 7636) protects the code: the client creates a random code_verifier (43 to 128 characters), sends code_challenge = BASE64URL(SHA256(code_verifier)) with the request, and later proves it holds the verifier. RFC 9700, the OAuth security best current practice (January 2025), says public clients MUST use PKCE, recommends it for confidential clients too, and says clients SHOULD NOT use the implicit grant. RFC 7636 says a client that can use S256 MUST use it; plain is for the rare case where it cannot.

    Figure 3. Sign-in through the shell with code flow and PKCE

    sequenceDiagram accTitle: Authorization code flow with PKCE started from the shell accDescr: The user opens the shell, which has no session. The shell creates a code verifier and challenge, redirects to the identity provider, the user signs in, the provider redirects back to the shell with a code, and the shell exchanges the code and verifier for tokens. Fragments then render for the signed-in user. participant U as User participant S as Shell participant I as Identity provider participant F as Fragments U->>S: opens app.example.com S->>S: create code_verifier,
    code_challenge (S256) S->>I: redirect with challenge I->>U: sign-in page U->>I: credentials I->>S: redirect back with code S->>I: code + code_verifier I-->>S: tokens S->>F: render with session API

    How to read this: only the shell takes part in the flow. In Pattern A the BFF acts for it and the browser receives only a cookie.

    One consequence is design judgement rather than a quote: RFC 9700 requires exact string matching of redirect URIs, so register one callback owned by the shell. A callback per fragment multiplies the places to protect.

    The contract fragments see

    Make the contract small, typed and free of secrets (Illustration 2, realistic: the shop's shell hands this to the catalogue, cart and account fragments):

    export interface SessionApi {
      /** Who is signed in, for display. Never contains a token. */
      getUser(): { id: string; displayName: string } | null;
      /** fetch that the shell has configured to authenticate the call. */
      authFetch(input: string, init?: RequestInit): Promise<Response>;
      /** Runs the listener when the session ends; returns an unsubscribe function. */
      onSessionEnded(listener: () => void): () => void;
    }

    A fragment never learns whether authFetch uses a cookie or a token, so the shell can change patterns without touching fragments. For a cross-origin iframe, the shell can answer a message. MDN's postMessage guidance: name an exact targetOrigin, verify event.origin on receipt, and validate the contents:

    const CART_ORIGIN = "https://cart.example.com";
    
    window.addEventListener("message", (event) => {
      if (event.origin !== CART_ORIGIN) return;
      if (event.data?.type !== "session:get") return;
      event.source?.postMessage({ type: "session:value", user: getUser() }, CART_ORIGIN);
    });

    Only the user summary crosses the boundary, never a token.

    Expiry: many fragments, one renewal

    Illustration 3, failure or edge case. In Pattern C the access token expires while three fragments have calls in flight. If each fragment reacts to its own 401 by renewing, the backend sees three renewals at once. The wrapper below renews once, and a call that failed with an old token retries only if nobody has renewed since. A Node run of this code with a fake fetch (three concurrent calls, one expired token) made one request to the renewal endpoint and three successful responses.

    let accessToken = null;
    let renewing = null;
    
    function renewToken() {
      renewing ??= fetch("/auth/token", { method: "POST", credentials: "same-origin" })
        .then(async (r) => {
          if (!r.ok) throw new Error("no session");
          accessToken = (await r.json()).accessToken;
        })
        .finally(() => { renewing = null; });
      return renewing;
    }
    
    export async function authFetch(input, init = {}) {
      if (!accessToken) await renewToken();
      const used = accessToken;
      const send = (token) =>
        fetch(input, { ...init, headers: { ...init.headers, Authorization: `Bearer ${token}` } });
      let response = await send(used);
      if (response.status === 401) {
        if (accessToken === used) await renewToken();  // nobody renewed since we sent
        response = await send(accessToken);
      }
      return response;
    }

    /auth/token is an illustrative path on a token-mediating backend. If renewal fails, the shell should end the session and notify fragments through onSessionEnded. RFC 9700 says refresh tokens for public clients MUST be sender-constrained or use rotation.

    Figure 4. One renewal for many calls

    sequenceDiagram accTitle: Single renewal shared by concurrent calls accDescr: Two fragments send requests with an expired token and receive 401. The first starts a renewal; the second waits for the same promise. After the new token arrives both retry and succeed. participant C as Catalogue participant A as Cart participant S as Shell authFetch participant B as Backend C->>S: authFetch (token t1) A->>S: authFetch (token t1) S->>B: request with t1 B-->>S: 401 (expired) S->>B: renew (one call) Note over S: second 401 shares the same renewal B-->>S: token t2 S->>B: retry with t2 B-->>S: 200

    How to read this: the renewal is requested once however many fragments hit the expiry; each retry uses the new token.

    In practice: moving the cart to its own subdomain

    NOTE

    This is a realistic scenario, not a case history. It is a composite built from common production patterns. It describes no specific company or product, and every name and number in it is an illustrative assumption.

    The situation

    The shop's shell serves its fragments from app.example.com, with a BFF at the same origin holding the OAuth tokens. The cart team wants to deploy on its own schedule and plans to serve its UI as an iframe from cart.example.com.

    Starting architecture

    Figure 5. Before the move

    Before: all fragments and the BFF on one origin with a host-only cookieShopper to Shell and fragments (page); Shell and fragments to BFF (__Host- cookie); BFF to Product APIs (bearer token)ORIGIN APP.EXAMPLE.COMShopperShopperShell and fragments: app.example.comShell andfragmentsapp.example.comBFF: same origin, /apiBFFsame origin, /apiProduct APIsProduct APIspage__Host- cookiebearer token

    How to read this: one origin, one host-only cookie, no token in JavaScript.

    Options considered

    Option What it fixes What it costs
    Keep the cart on the shell origin Cookie works unchanged Cart code runs with the shell's trust
    Cart iframe with a Domain=example.com cookie Independent deploys Cookie reaches every subdomain; no __Host-
    Cart iframe, postMessage for the user summary, cart calls through the BFF Smaller trust, tokens stay server side A message contract and CORS

    The decision

    Judgement, labelled as such: keep the cookie host-only, serve cart APIs through the BFF, give the iframe only a user summary by postMessage, and sandbox the iframe without allow-same-origin if the cart needs no storage of its own. Record the rule that no session cookie may set Domain without an owner's review.

    Rollout and verification

    1. Deploy behind a flag for a small share of sessions.
    2. Check which requests carry the cookie, and test sign-out and expiry with the cart open.

    The 2 a.m. failure

    After launch, many shoppers see "please sign in" inside the cart. The iframe on cart.example.com calls its own endpoints, and the host-only cookie of app.example.com is not sent there. An engineer "fixes" it with Domain=example.com, which works but widens the cookie to every subdomain and drops the __Host- guarantee.

    First three checks: which host each failing request went to, whether it carried a cookie, and whether the cookie had a Domain. Mitigation: route the iframe's calls through the BFF origin, and revert the Domain change. Follow-up: add a test that fails if a session cookie sets Domain, and a runbook line on who may change cookie scope.

    Architect's notes

    • Decide the owner of sign-in first. One owner (shell plus BFF) is cheaper than four teams negotiating.
    • Prefer a host-only cookie and a same-origin BFF unless composition forces separate hosts.
    • Treat shell-origin code as trusted. Use sandboxed iframes for code you cannot vouch for.
    • Keep the fragment contract small and secret-free so you can change patterns later.
    • Measure 401 rates per fragment and renewal counts.

    Common mistakes

    • Treating "same site" as "same origin".
    • Adding Domain to fix a 401.
    • Putting tokens in localStorage in a page shared by many teams' code.
    • Letting each fragment renew tokens.
    • Using postMessage with "*" or without checking event.origin.
    • Registering a redirect URI per fragment.

    Key takeaways

    • One component, normally the shell with a BFF, owns sign-in.
    • Origin and site are different; cookies use host and site rules, scripts use origin rules.
    • A host-only cookie with the __Host- prefix is the tightest cookie scope; Domain widens it to subdomains.
    • HttpOnly hides a cookie from scripts but does not stop scripts using it.
    • RFC 10017 ranks BFF above a token-mediating backend above a pure browser client.
    • Public clients must use PKCE with S256; do not use the implicit grant (RFC 9700).
    • Prefer in-memory tokens to localStorage; neither stops a malicious script in your own origin.
    • Give fragments authFetch and getUser, never a token.

    Practice exercises

    Exercise 1, Starter (30 min): For five URLs, say whether each is same-origin, same-site or neither relative to https://app.example.com, and whether a host-only cookie would be sent.

    Exercise 2, Intermediate (2 hours): Test authFetch with a fake fetch: three concurrent expired calls, one renewal.

    Exercise 3, Advanced (half day): Write a threat model for Patterns A and B with one hostile fragment: what it can do, and one control each.

    In this course:

    Elsewhere on the site:

    Official documentation

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments