Olympus Docs
CookbookSessions

Cross-origin cookies, what works and what doesn't

Cookie attributes for SPAs on separate origins

Your SPA is at app.your-domain.com. Olympus is at ciam.your-domain.com. Same parent domain, different subdomain. Cookies don't automatically share. Here's how to navigate.

The challenge

Browser sets cookie at ciam.your-domain.com (Kratos session). SPA at app.your-domain.com wants to use that session.

Default cookie behavior: scoped to setting-domain only. app.X can't see cookies set by ciam.X.

Set the session cookie at .your-domain.com (note the leading dot). All subdomains see it.

# kratos.yml
session:
  cookie:
    domain: your-domain.com
    same_site: Lax
    secure: true

ciam.X sets, app.X reads. Both can be in their own subdomain.

Solution 2: SameSite=Lax (default)

SameSite=Lax means cookie is sent on top-level navigations, NOT on cross-site fetch.

So:

  • User navigates from app.X to ciam.X → cookie sent (top-level GET).
  • SPA at app.X does fetch('https://ciam.X/api') → cookie NOT sent.

This affects OAuth2 flows that use fetch. Workaround: use redirects, not fetch, for OAuth2 endpoints.

Solution 3: BFF on same origin

Your SPA at app.X. Your BFF (backend-for-frontend) also at app.X/api/*. BFF holds the Kratos session.

SPA (app.X) ──cookie session_id──► BFF (app.X/api) ──tokens──► Olympus

SPA never directly touches Olympus. No cross-origin cookies needed.

This is the cleanest. See BFF whoami pattern.

SameSite=None

For cases where you MUST send cookie cross-origin (iframe-based UIs, etc.):

Set-Cookie: session=X; SameSite=None; Secure

Required: Secure flag. SameSite=None without Secure is rejected by modern browsers.

Trade-off: re-introduces CSRF risk. Pair with CSRF tokens or SPA-style headers.

Cookies in iframes

If you embed Hera in an iframe at a different origin (NOT recommended), cookies blocked unless:

  • SameSite=None; Secure.
  • Third-party cookie permissions (Safari blocks by default; Chrome reducing).

For embedded auth, OAuth2 popup or redirect is more reliable than iframe.

Cookies on different sites (apex)

app.com and auth.com are completely separate apex domains. No cookie sharing possible.

Use OAuth2: app.com redirects to auth.com, gets code, exchanges, stores its own session cookie at app.com.

Set-Cookie: kratos_session=value;
  Path=/;
  HttpOnly;
  Secure;
  SameSite=Lax;
  Max-Age=86400;
  Domain=your-domain.com  ← optional, for subdomain sharing
  • HttpOnly: JS can't read.
  • Secure: HTTPS only.
  • SameSite=Lax: CSRF protection.
  • Max-Age: explicit lifetime.

Domain pinning

Should you scope cookies to .your-domain.com or ciam.your-domain.com?

  • .your-domain.com: all subdomains see. Risk: a vulnerable subdomain (e.g., legacy.your-domain.com) leaks cookies.
  • ciam.your-domain.com: only that subdomain. More secure but less convenient.

Recommendation: scope to the most specific subdomain that needs the cookie.

For Olympus: Kratos's session is read by Hera (same origin). No need to broaden domain. Use ciam.your-domain.com scope.

CORS and cookies

If your SPA at app.X uses fetch to ciam.X:

fetch("https://ciam.your-domain.com/sessions/whoami", {
  credentials: "include",  // ← required to send cookies
});

Server must respond:

Access-Control-Allow-Origin: https://app.your-domain.com
Access-Control-Allow-Credentials: true

Wildcards (*) NOT allowed with credentials.

Logout cookies

On logout, server should set:

Set-Cookie: kratos_session=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT

Browser deletes. Kratos's logout endpoint handles this.

For multiple domains (multi-tenant white-label): each domain's cookies must be cleared independently. Front-channel logout helps.

Cookies pre-load

Some browsers (Firefox, Safari) may not send first-time cross-origin cookies even on top-level nav (privacy mode). Edge case but real.

Mitigation: same-origin BFF avoids the problem entirely.

Debugging

In DevTools:

  • Application → Cookies → see what's set.
  • Network → request headers → see what was sent.

If cookie isn't being sent:

  1. Check cookie attribs (Secure, SameSite, Domain).
  2. Check the request is what you think (same-origin, cross-origin, top-level nav).
  3. Check no extension is blocking (Privacy Badger, uBlock).

Modern alternatives

  • CHIPS (Cookies Having Independent Partitioned State): experimental Chrome feature. Cookies partitioned per top-level site.
  • DBSC: device-bound credentials (different from cookies).

Both early, not relied upon yet.

On this page