Auth0 Anonymous Sessions
BetaA first-party client gets a real OAuth access token for an unidentified visitor — no login required — then hands off to a genuine authenticated identity with zero extra code on the redirect. See also: PostHog Anonymous to Known, which solves the other half of this problem — analytics attribution, no API access involved.
The catalog is a plain public endpoint — no session, no token, nothing to present. Try adding an item to cart from here and the storefront API rejects it outright: that endpoint requires a real bearer token, even an anonymous one.
Catalog — no access token
no Authorization header sentLoading catalog…
Step 2: Create an anonymous session
Goes through our own server (a same-origin relay to POST /anonymous/token) rather than straight from this browser — this tenant's beta build never sends CORS headers on the real response, only on the preflight, so a browser can't read it directly. Auth0 issues a genuine anonymous OAuth access token either way; the cookie for transparent handoff comes later, at login.
Backend API log
Browse the catalog or add an item to cart to see calls land here
Strengths
- Issues a real OAuth-scoped access token (sub: anon@xxx) instead of exposing an unauthenticated open API — anonymous callers go through the exact same JWKS signature verification as authenticated ones, on the exact same /api/anon-sessions/cart endpoint (confirmed by deliberately tampering with a token and watching the route reject it, not just decode it).
- The Post-Login Action receives the anonymous session server-side, so migrating cart data doesn't require trusting anything the client claims about its own prior anonymous activity.
- app_metadata carries only an identifier (cart_id), a session_id, and timestamps — never the cart's actual contents. This isn't a workaround, it's Auth0's own confirmed design intent for the feature: metadata/app_metadata is for identifiers, not general data storage. The real cart lives in our own Firestore store, keyed by that same cart_id, and is dereferenced only for display.
- cart_id is generated client-side and is deliberately unrelated to any Auth0 sub — the one thing genuinely continuous across the whole visit, since the anon@ sub used for interactive shopping and the anon@ sub the Post-Login Action actually sees are, by design, two different identities (see risks).
- Each conversion appends a new entry to app_metadata.cart_sessions rather than overwriting the last one, so running the flow twice with the same user produces a real history rather than clobbering itself — same pattern as Auth0's own "keep all linked sessions" use-case example, generalised to a full session record.
Risks & Considerations
- Confirmed directly with Auth0's product team: the actual response to POST /anonymous/token and /anonymous/logout never carries Access-Control-Allow-Origin on this tenant's beta build (only the OPTIONS preflight does) — a real browser can never read either response, regardless of CORS configuration. Reported as a bug, not treated as a config problem to work around indefinitely.
- Set-Cookie for auth0_anon only ever fires on a genuine create call (client_id + metadata, no session_token) — confirmed empirically that a renewal call never re-sends it, whether the existing session_token is presented explicitly in the body or implicitly via the cookie itself. A session created server-side (e.g. through a relay, to work around the CORS issue above) can never have its cookie "transplanted" into the browser afterward by any subsequent call — only a fresh, browser-direct create can produce it, and that necessarily mints a new anon@ identity. Assuming a renewal will refresh the cookie is a silent failure, not an error: the call still returns 200.
- Session metadata is set-once at creation and cannot be updated on a later renewal call (session_token and metadata are mutually exclusive — confirmed with a live 400 from the API, not just the docs). Don't design a flow that assumes an anonymous session's metadata can grow over time; it can't, by design.
- Anonymous sessions are not auto-invalidated at login; a forgotten cleanup step leaves stale anon@ identities accumulating in the tenant. Logout must also be browser-direct — a server-side call can never clear a cookie Auth0 set on its own domain.
- The public client's Allowed Origins (CORS) is the entire browser-side trust boundary once tokens are minted — anyone able to add an origin there can obtain anonymous access tokens for this API from their own page.
- The auth0_anon cookie has no Domain restriction and Auth0's docs are explicit that there is no replay detection: a valid session token can be presented to /authorize any number of times before it expires. Expiry is the only lifetime control today.
Hardening Checklist
- Treat app_metadata (and anonymous session metadata) as identifier storage only — store a pointer to your own data store, never the data itself, even for "small" payloads like a shopping cart.
- Because the browser can't reliably read /anonymous/token's own response, issue tokens through a same-origin relay on your own server for anything the UI needs to read immediately — but if Transparent Mode matters, still fire one genuine browser-direct create call (never a renewal) before redirecting to login, and don't assume its resulting identity matches whatever the relay was using.
- Never key your own data store by the anonymous sub if a real handoff-time create call is involved — use an application-generated identifier (carried in localStorage across the redirect) that stays stable regardless of how many anon@ identities get minted along the way.
- Always call POST /anonymous/logout (browser-direct, with credentials) once the session has been resolved into a known user — don't rely on natural expiry alone.
- Keep the public anon client's Allowed Origins exact-match and minimal — it is not gated by a client secret, so origin allowlisting is the only thing standing between "our app" and "anyone's page" for minting anonymous tokens.
- Validate anon@ access tokens with full signature verification against the tenant's JWKS, exactly as you would any other bearer token — don't special-case or skip verification for the anonymous case.
