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
- 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).
- 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.
- 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
sandboxattribute can tighten that further, and warns that combiningallow-scriptsandallow-same-originfor a same-origin embedded document lets it remove its own sandbox.
Figure 1. Where credentials live in a shell-and-fragments system
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.
What a cookie is allowed to do
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).
Domainomitted: 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 ascom.HttpOnly: JavaScript cannot read the cookie throughdocument.cookie. It is still sent with JavaScript-initiatedfetch()calls.Secure: sent only overhttps:(localhost excepted).SameSite:Strictsends the cookie only on same-site requests;Laxalso sends it on cross-site top-level navigations with a safe method;Nonesends it everywhere and requiresSecure. Some browsers default toLaxwhen the attribute is missing, so do not rely on the default. The draft specification warns thatLaxis "defense in depth" and "does not offer a robust defense against CSRF".__Host-prefix: the cookie must beSecure, havePath=/and must not have aDomainattribute. 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
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
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
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
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
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
- Deploy behind a flag for a small share of sessions.
- 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
401rates per fragment and renewal counts.
Common mistakes
- Treating "same site" as "same origin".
- Adding
Domainto fix a 401. - Putting tokens in
localStoragein a page shared by many teams' code. - Letting each fragment renew tokens.
- Using
postMessagewith"*"or without checkingevent.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;Domainwidens it to subdomains. HttpOnlyhides 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
authFetchandgetUser, 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.
Related reading
In this course:
- The Same-Origin Policy and CORS, what a page may read and send.
- CORS in Practice: Preflights, Credentials and Debugging, credentials and the custom-header defence.
Elsewhere on the site:
- System Design: Security, the back-end side of authentication and authorisation.
Official documentation
- MDN: Set-Cookie header and MDN: Site (glossary)
- IETF draft: Cookies, RFC 6265bis
- RFC 10017: OAuth 2.0 for Browser-Based Applications and RFC 9700: OAuth 2.0 Security Best Current Practice
- RFC 7636: Proof Key for Code Exchange and RFC 6749: OAuth 2.0
- OWASP: HTML5 Security Cheat Sheet
- MDN: Window.postMessage() and Martin Fowler: Micro Frontends