Demo Script

A walkthrough for each integration pattern, in the order they build on each other.

Before you start

Create a brand-new test userrather than reusing an existing account — sessions and device lists are much easier to read when they start empty. Sign up through any of the demos below using an @atko.email address, e.g. yourname.demo@atko.email. All demos share the same Auth0 tenant and connection, so one signup works for all of them.

Sign in from at least two distinct browsers or devices with that same test user — e.g. Chrome and Firefox, or a laptop and a phone. Incognito/private tabs in the samebrowser usually still share the underlying OS keychain or extensions in ways that can muddy results, so a genuinely different browser or device is worth the extra minute. Most of what's interesting here — multiple sessions, multiple devices, revoking one and watching the other die — only shows up once there's more than one signed-in client. Below, steps are tagged A or B for which browser/device to use.

The sidebar is always on.It's mounted on every page in this suite and shows your identity, every active session across every pattern, and revoke actions — you don't need to navigate anywhere to see it. Several steps below just say "check the sidebar."

1. Traditional Web App
/demo/traditional
Server-side cookie session, Session Management API, Back-Channel Logout.
  1. 1 · AOpen /demo/traditional and sign up as the new test user.
  2. 2 · AExpand the ID Token / Access Token / Refresh Token panels on the User Profile card — note these are only ever rendered server-side; the browser holds nothing but an encrypted session cookie.
  3. 3 · ACheck the sidebar — it should show exactly one active session, this browser.
  4. 4 · BOpen /demo/traditional and sign in with the same test user.
  5. 5 · AClick the refresh icon in the sidebar — it should now list two sessions. Expand each to compare device/IP/user-agent.
  6. 6 · ARevoke browser B's session directly from its card in the sidebar.
  7. 7 · BNavigate anywhere in the app — you should be forced back to login. Nothing was clicked in this browser to trigger it; the Management API deletion sent a Back-Channel Logout token that killed this session server-side.

What to look for

  • Session Management API returns both sessions with distinguishing device metadata.
  • Revoking from browser A actually terminates browser B — Back-Channel Logout, not just a client-side redirect.
2. SPA with DPoP
/demo/spa
No server-side session, DPoP-bound access tokens, Refresh Token Metadata, Device List.
  1. 1 · AOpen /demo/spa and sign in with the same test user.
  2. 2 · ACheck the token_type badge — it should read DPoP, not Bearer.
  3. 3 · AExpand the ID Token / Access Token panels. The access token is often opaque — DPoP-bound tokens don’t need to be a readable JWT to be verifiable.
  4. 4 · ACheck Refresh Token Metadata — browser/os/ip stamped by the Post-Login Action at login, mirrored onto the ID token purely so this demo can show it.
  5. 5 · AScroll to Other Devices — one entry, tagged “This device.” Click the pencil icon and rename it, e.g. “Chrome – Laptop.” Refresh the page and confirm the name stuck.
  6. 6 · BOpen /demo/spa and sign in with the same test user.
  7. 7 · BRename this device too, e.g. “Safari – iPhone.”
  8. 8 · AReload Other Devices — it should now list both devices, each with the name you gave it and its own IP/browser/OS metadata intact.
  9. 9 · ARevoke browser B's device.
  10. 10 · BTrigger any token refresh (reload the page, or wait for the access token to expire) — you should be signed out. The refresh token itself was deleted, not just a session record.

What to look for

  • Renaming a device does not wipe its other metadata fields (browser/os/ip/captured_at) — the PATCH merges rather than replaces.
  • Revoking deletes the refresh token, so the other browser can’t silently mint a new access token afterward.
  • This device list is scoped to the SPA application only — it won’t show devices from the Traditional or BFF demos even though they share a user.
3. SPA with Online Refresh Tokens
/demo/ort
Server-validated refresh tokens. No DPoP (intentional — isolates ORT from sender constraining). Demonstrates immediate revocation with no access-token grace period.
  1. 1 · AOpen /demo/ort and sign in. Note this client uses offline_access for the demo-orders.internal audience — with the online_refresh_tokens tenant flag enabled, Auth0 issues an ORT: a server-side reference token validated on every exchange.
  2. 2 · AClick "Start polling" in the Revocation Speed Test panel. It calls getTokenSilently({ cacheMode: "off" }) every 2 seconds, bypassing the SDK cache to force a fresh refresh token exchange each tick.
  3. 3 · BOpen /demo/ort in a second browser or incognito tab. Sign in with the same user.
  4. 4 · AScroll to Other Devices — both sessions should appear.
  5. 5 · ARevoke browser B's session from the device list.
  6. 6 · BWatch the polling panel in browser B — the very next tick fails with invalid_grant. Check the gap timestamp: it should be ≤2s, not minutes.

What to look for

  • Revocation gap in the polling log: the timestamp between last-success and first-failure is one poll interval (~2s), not the access token lifetime (~5 min).
  • Error on the failed tick: invalid_grant — Auth0 rejected the refresh token exchange because the server-side record was deleted.
  • The access token displayed in the ID/AT panel was still valid at the time of revocation — ORT kills the session before that AT would have naturally expired.
  • Comparison card at the bottom: DPoP stops token replay; ORT stops the post-revocation grace period. They solve different parts of the problem.
4. Backend-for-Frontend
/demo/bff
Server-side token vault, Multi-Resource Refresh Tokens vs Standard RT, Device List.
  1. 1 · AOpen /demo/bff and sign in with the same test user.
  2. 2 · ABefore calling any API, read the MRRT Policy Comparison card at the top. It shows the BFF client has two policy entries (billing + inventory), while the SPA client has none — that structural difference is the entire MRRT feature.
  3. 3 · AClick Call Billing API. The Exchange Ledger appears: RT before, billing audience, RT after (rotated). The RT ID changed because rotation_type is rotating — the policy carries through to the new token.
  4. 4 · AClick Call Inventory API. A second row appears. The RT before on this row matches the RT after from the previous row — the chain badge confirms it is the same rotation lineage, now exchanged for a different audience.
  5. 5 · AClick 'Try unauthorized audience'. Auth0 will either reject the exchange or silently redirect it to the original audience — note the outcome banner and actual aud in the result. This proves the policy boundary is enforced.
  6. 6 · ACompare to the SPA demo — scroll to 'Standard RT — no MRRT policy' at the bottom. The SPA client has no policies field and can only exchange for one audience.
  7. 7 · ACheck Other Devices — one entry. Rename it, then confirm the name persists on refresh.
  8. 8 · BOpen /demo/bff and sign in with the same test user.
  9. 9 · AReload — Other Devices should now show two entries.
  10. 10 · ARevoke browser B's device.
  11. 11 · BReload the page — you should be redirected to log in again.

What to look for

  • MRRT policy comparison card shows the structural difference: BFF has refresh_token.policies[], SPA has nothing.
  • Exchange ledger: each row shows RT before, audience, RT after. The chain badge on row 2 confirms the rotation lineage is continuous.
  • Unauthorized audience test: Auth0 enforces the policy — the actual aud is NOT the requested audience.
  • Tokens never appear in browser storage or network responses — only the session cookie reaches the browser.
5. Unified Profile & Sidebar
/profile
Identity, sessions, and revoke actions always visible in the sidebar. My Account API profile edit and Fingerprint.com device enrichment detail live on /profile.
  1. 1 · ASign into /demo/traditional. Note the sidebar on the right — your identity (avatar, name, email) sits at the top, with a session card for this browser below.
  2. 2 · AIn the same browser, open /demo/bff — you land directly on the page with no login prompt. The sidebar's session card now shows both "Traditional" and "BFF" badges with an "SSO shared" label — the same Auth0 AS session viewed through two application lenses.
  3. 3 · AA Fingerprint badge resolves under your name in the sidebar within a second or two — visitorId, visit count, geolocation. In the background it POSTs to /api/profile/fingerprint-link, writing the visitorId into the refresh token's metadata.
  4. 4 · AExpand the session card in the sidebar. Under "Refresh tokens," find fingerprint_visitor_id. Under "Fingerprint visits," the last few visits for this browser are listed.
  5. 5 · AClick "See more on your profile" under your name in the sidebar. On /profile, scroll to "Fingerprint.com — This Device" for the full visit history table, geolocation, and — if your plan has them — Smart Signals (confidence score, bot/VPN/proxy flags). This detail doesn't fit the sidebar's narrow column, which is why it lives here.
  6. 6 · AReload /profile. The Fingerprint visit count increments; the visitorId in the sidebar stays identical — stable device identity across reloads.
  7. 7 · AOn /profile, click the pencil icon next to your display name, type a new one, press Enter. This calls PATCH /api/profile/my-account with your own scoped access token, not an M2M grant. Reload; the name persists via user_metadata.display_name.
  8. 8 · BOpen /demo/traditional in a different browser and sign in with the same test user.
  9. 9 · AClick the refresh icon in the sidebar. A second session card appears for browser B with its own device label, IP, and app badges.
  10. 10 · ARevoke browser B's session directly from its card in the sidebar. The single Revoke action removes the Auth0 session and every associated refresh token for that device.
  11. 11 · BNavigate anywhere in the demo — you are redirected to login. The revoke was driven entirely from the sidebar in browser A.
  12. 12 · AFinally, click "Revoke all" in the sidebar. Confirm the dialog. This calls POST /api/profile/sessions/revoke-all, bulk-deleting every remaining session and refresh token for the user tenant-wide — global sign-out from the sidebar, visible on every page.

What to look for

  • The sidebar, not /profile, is the single place sessions/devices are always visible — it's mounted in the root layout, so it's on every page in this suite, including the homepage.
  • /profile is trimmed to what genuinely doesn't fit a narrow column: identity editing and the Fingerprint Smart Signals/visit-history detail panel.
  • Fingerprint's visitorId is captured once, in the sidebar (the only place mounted on every render) — /profile reads the same value back from the session's refresh_token_metadata instead of re-running the agent, avoiding a second identification call for the same device.
  • One session card groups Traditional + BFF because they share an Auth0 AS session — the clients[] array on the session record is the source of truth, not which URL you happen to be on.
6. SSO Isolation — High Trust App
/demo/high-trust
sso: false on the Auth0 client — logins never reuse an existing AS session.
  1. 1 · AConfirm you are signed into /demo/traditional in browser A. The Auth0 Authorization Server has an active session for this browser.
  2. 2 · AVisit /demo/high-trust. Auth0 presents a full login prompt despite the active session — it does not silently reuse it. This is sso:false in action: the client is permanently configured to opt out of tenant-wide SSO at the Authorization Server level.
  3. 3 · ASign in with the same test user. The High Trust page notes the other active sessions (Traditional / BFF) it detected in this browser, confirming both coexist.
  4. 4 · ACheck the sidebar. Two distinct session cards appear: one for "Traditional · BFF" labelled "SSO shared", and a separate one for "High Trust" labelled "isolated login". Same user, same browser, same IP — two independent Auth0 AS sessions.
  5. 5 · ARevoke only the High Trust session card, from the sidebar. Reload /demo/high-trust — it requires login again. Reload /demo/traditional — it is unaffected. The two sessions are fully independent.
  6. 6 · ASign back into /demo/high-trust. Check the sidebar again and note the High Trust session card reappears as new — a fresh authenticated_at timestamp, new session ID, separate from the still-active Traditional session.

What to look for

  • sso:false is set on the Auth0 client record via Management API — not a runtime prompt parameter. The isolation is permanent and applies to every login, not just this session.
  • The sidebar shows two session cards for the same user because they have different Auth0 session IDs — the unified view groups by session, not by user.
  • Revoking the High Trust session leaves the Traditional session completely untouched — there is no shared state to cascade through.
  • Use case: admin panels, payment flows, medical records — any surface where re-authenticating on every access is a compliance or security requirement regardless of existing SSO state.
7. Native-to-Web SSO
/demo/native-to-web
Session transfer token — single-use, 60s expiry, IP-bound. Native session hands off to a web browser session with no re-authentication.
  1. 1 · AOpen /demo/native-to-web and sign in to the native app. This represents a mobile or desktop app that already holds an Auth0 session and a refresh token.
  2. 2 · AClick "Exchange for Session Transfer Token". The server calls POST /oauth/token with grant_type=refresh_token and audience=urn:{domain}:session_transfer — watch the exact request payload displayed on screen.
  3. 3 · AObserve the session transfer token: raw value, 60-second countdown timer, and its three security properties (single-use, 60s expiry, IP-bound). You have one minute.
  4. 4 · AClick "Authorize Web App". This constructs GET /authorize?session_transfer_token=… with the web target client_id — the request shape is shown before you confirm.
  5. 5 · AAuth0 validates the STT, issues an authorization code, and the web app callback exchanges it for tokens. You land back on the page at step 4.
  6. 6 · AClick "compare" on the ID Token and Access Token comparison cards. Claims that differ between native and web sessions are highlighted — same sub, different aud, azp, iat, and client_id.
  7. 7 · ATry "Sign out web app only" and re-run from step 2. The native session is still active — getting a new STT takes a single API call, not a new login.

What to look for

  • sub is identical across both token panels — the STT transfers the identity, not just a hint of it.
  • The STT countdown: 60 seconds is the entire usable window. If you wait too long before clicking Authorize, Auth0 rejects it.
  • The STT is gone after step 4 — single-use means a second /authorize with the same token returns an error.
  • The web session has no refresh token (allow_refresh_token: false on the web target client) — it is a forward-only transfer, not a persistent grant.
8. Cross-Domain SSO
/demo/cross-domain-sso
Two genuinely separate Auth0 domains, same tenant — the mirror image of the High Trust App demo. Session transfer token bridges them since there is no shared AS session cookie across domains.
  1. 1 · AOpen /demo/cross-domain-sso — this is Brand A, authenticating against the tenant's default domain. Sign in with the test user.
  2. 2 · ANote the ID Token card's iss badge — it names Brand A's domain specifically.
  3. 3 · AClick "Get session transfer token." Watch the exact /oauth/token request shown below the button — grant_type=refresh_token, audience naming Brand A's own domain (the source, not the target — a real, undocumented gotcha found building this).
  4. 4 · ACheck issued_token_type in the response. It should be populated — a real STT. If it's blank, Auth0 silently substituted a plain access token because the target client isn't oidc_conformant; the demo flags this explicitly rather than failing silently.
  5. 5 · ANote the device-binding callout under the token — enforce_device_binding: 'ip' is live even though the mint call runs server-side, because Auth0-Forwarded-For plus Brand A's Trust Token Endpoint IP Header setting tells Auth0 to trust the forwarded browser IP for this refresh_token exchange.
  6. 6 · AClick "Redeem on Brand B" — opens Brand B in a new tab, authenticated via the STT, no login screen.
  7. 7.On the new tab (Brand B), confirm the "Authenticated via session transfer token" banner. Note the ID Token's iss badge names a different domain than Brand A's.
  8. 8.On Brand B, in a fresh session, click "Check SSO (prompt=none) — expect failure" instead. It correctly returns login_required — Brand A's AS session cookie is scoped to Brand A's domain and Brand B can never see it, which is exactly why the STT bridge exists.

What to look for

  • issued_token_type is the only field that distinguishes a real STT from Auth0's silent fallback to a plain access token — this demo surfaces it explicitly because the failure mode otherwise looks like an ordinary 200 response.
  • The STT audience must name the source domain (Brand A's) — passing the target domain doesn't error, Auth0 just silently mints a plain token instead.
  • prompt=none on Brand B always returns login_required regardless of Brand A's session — two real origins simply can't share one AS session cookie, unlike the High Trust demo where isolation is a deliberate client setting rather than a browser security boundary.
9. On-Behalf-Of Token Exchange
/demo/obo
RFC 8693 delegation chain, identity preservation, AI agent pattern.
  1. 1 · AOpen /demo/obo and sign in with the test user.
  2. 2 · ARead the Delegation Chain diagram at the top. Note the two access tokens: AT1 is scoped to the middle-tier audience and carries the user identity; AT2 is the OBO result scoped to the orders audience with the same sub and a new act claim.
  3. 3 · AClick Run OBO Exchange. The exchange result card appears with two toggle buttons for AT1 and AT2.
  4. 4 · AOpen AT1 (incoming). Note sub = the user and aud = middle-tier audience.
  5. 5 · AOpen AT2 (OBO result). The highlighted box at the top shows: sub is unchanged (still the user), azp = the middle-tier service client, and act.sub records who performed the exchange. Open the full claims to confirm aud has changed to the orders audience.
  6. 6 · AClick Call Orders API (full flow). The act delegation chain section builds from top to bottom: the top node is the current token holder, the bottom is the original caller.
  7. 7 · ARead the Orders API response at the bottom — it was called with the OBO token. In a real system, the Orders API would use sub for row-level access control without trusting any out-of-band identity claim.

What to look for

  • sub is identical in AT1 and AT2 — the user identity is preserved through the exchange.
  • azp in AT2 is the middle-tier service client ID, not the user-facing app.
  • act claim records the delegation chain: who exchanged the token and on whose behalf.
  • The downstream Orders API never sees the original AT1 — it only sees the scoped, delegated AT2.
10. Custom Token Exchange
/demo/cte
Exchange any external token format for Auth0 AT + ID Token + RT via an Action.
  1. 1 · AOpen /demo/cte. No login — Custom Token Exchange IS the authentication. There is no browser redirect to Universal Login.
  2. 2 · ARead the flow diagram: External Partner JWT goes directly to Auth0 /oauth/token. A CTE Action validates it and calls setUser(). Auth0 returns a full AT + ID Token + RT.
  3. 3 · AEdit the external token fields — change the email to match your test user on the tenant (the Action maps by email). Click Exchange for Auth0 Tokens.
  4. 4 · AIn the External Partner JWT card, read the claims that were sent to Auth0. Note the iss is https://partner-idp.demo — an entirely external issuer Auth0 does not natively trust.
  5. 5 · AOpen the ID Token. Confirm sub is an Auth0 user ID (auth0|...) — the Action mapped the external identity to an existing Auth0 user. The email and name from the external JWT appear as standard OIDC claims.
  6. 6 · AOpen the Access Token. aud is the Auth0 middle-tier API — a fully functional Auth0 AT that can be used to call downstream APIs, despite the user never seeing a login screen.
  7. 7 · ATry changing the email to one that does not exist on the tenant. The exchange will fail with an error from the Action — demonstrating that CTE does not create new users; it only maps to existing ones unless the Action explicitly provisions them.

What to look for

  • No browser redirect — CTE is a pure server-side token endpoint call.
  • The incoming token format is completely custom (HS256 JWT with a demo issuer) — Auth0 never validates it; the Action does.
  • The resulting ID Token contains an Auth0 sub, not the external sub — the Action performed identity mapping.
  • If the CTE Profile is not yet created in the dashboard, the error card shows the exact manual step needed.
11. Anonymous to Known — PostHog
/demo/anon-to-known
Analytics-layer identity stitch — PostHog merges a pre-auth distinct_id into the authenticated user after callback. No API access, no OAuth concept of "anonymous."
  1. 1 · AOpen /demo/anon-to-known and browse the "Anonymous" phase for a moment (simulated storefront actions) before signing in.
  2. 2 · ASign in. After the callback, posthog.identify() merges the pre-auth distinct_id into the newly authenticated Auth0 sub. Open the in-app reporting panel — the stitched timeline shows both the anonymous and authenticated activity as one continuous journey.

What to look for

  • The stitch happens entirely in the analytics layer (client-side distinct_id merged via identify()) — Auth0 itself has no concept of "anonymous" in this path.
  • Only PostHog can power the in-app reporting panel — GA4's Data API has a 24–48h delay and Segment has no queryable Events API.
  • See also /demo/anon-sessions (next phase) for the other half of this problem: authenticated API access before login, not analytics attribution.
12. Auth0 Anonymous Sessions
/demo/anon-sessions
A genuine server-verified anonymous OAuth identity (beta) — real access tokens before login, a transparent cookie handoff into a known user, no analytics platform involved.
  1. 1 · AOpen /demo/anon-sessions. Open dev tools' Network tab first. Browse the catalog — confirm the request has no Authorization header at all.
  2. 2 · AClick "Try add to cart (no token)" on any product. The storefront API rejects it with 401 — browsing needs nothing, writing needs a real bearer token.
  3. 3 · AClick "Create Anonymous Session." This goes through our own server (a same-origin relay) — Auth0's beta build never sends CORS headers on the actual token response, only on the preflight, so a browser can't read it directly.
  4. 4 · AAdd a couple of items to cart. /api/anon-sessions/cart returns 200 with a JWKS-verified sub: anon@xxx, and the item is written to our own Firestore store keyed by a client-generated cart_id — not by sub, and not into Auth0 at all.
  5. 5 · AClick "Log In" or "Sign Up" — watch the button read "Sealing…" briefly first. This fires one genuine browser-direct create call (not a renewal) carrying only cart_id as metadata — the one call in the whole flow that actually sets auth0_anon, since renewals never do. The latter also passes screen_hint=signup straight through to Universal Login.
  6. 6 · AAfter login, check the Post-Login Action card — event.anonymous_session.user_id was available server-side with zero app code involved (a genuinely different anon@ sub from the one used for shopping — that's expected, see Security Implications). Then check the Cart Sessions card: app_metadata holds only a cart_id, a session_id, and timestamps — the actual items shown are dereferenced from Firestore by that cart_id.
  7. 7 · AAdd an item to cart again, now as the known user. Same /api/anon-sessions/cart endpoint and the same cart_id (still in localStorage), but sub is now your real Auth0 sub — check the Backend API log panel to diff the anonymous and known calls side by side.
  8. 8 · ARun the whole flow again with the same test user (reset, shop, sign in again). Reset clears the stored cart_id, so this is a genuinely new cart — the Cart Sessions card should now show two entries, each with its own correct items.

What to look for

  • The catalog call has zero Authorization header; the cart call without a token 401s; the cart call with the anon access token 200s with sub: anon@xxx — three distinct auth modes against one small API surface.
  • Session creation is a same-origin relay, not a browser-direct call — confirmed as a real gap with Auth0's product team: the actual POST response to /anonymous/token and /anonymous/logout never carries Access-Control-Allow-Origin on this beta build, only the OPTIONS preflight does, so a browser can never read either response directly.
  • Set-Cookie for auth0_anon only ever fires on that one final create call, never on a renewal — confirmed empirically, not assumed. That's exactly why a fresh anon@ sub shows up at login: the relay's session and the sealed session are genuinely different identities, and cart_id (not sub) is what stays continuous across that gap.
  • app_metadata.cart_sessions never contains cart contents, only cart_id/session_id/timestamps — also confirmed with Auth0's product team as deliberate design, not a limitation of this demo: metadata is for identifiers, never general data storage.
  • The Backend API log reads back the server's own verified record of each call (JWKS-checked sub, not a client-side guess) — the same /api/anon-sessions/cart route visibly serves both an anon@xxx and an auth0|... caller.
  • This feature is explicitly beta with real limitations (no Password Reset, Device Code, CIBA, or CTE for anonymous users) and no replay detection on the session token — named directly in the Security Implications section, not glossed over.
13. CIBA — Decoupled Backchannel Auth
/demo/ciba
Real Guardian push, no browser round-trip on the approver's side. Toggle between a plain binding_message and Rich Authorization Requests (Highly Regulated Identity).
  1. 1 · ASign into any pattern first (e.g. Traditional) — CIBA approves an action for whichever user is currently signed in, framed as step-up approval mid-session.
  2. 2 · AOpen /demo/ciba. If Guardian isn't enrolled yet, click "Start Guardian enrollment" — this opens Universal Login's own hosted enrollment page in a new tab, not a QR generated by this app. The enrollment ticket is meant to be opened as a link; Universal Login renders its own correctly-formatted QR there.
  3. 3.On your phone, scan the QR shown on Universal Login's page with the Guardian app.
  4. 4 · AOnce enrolled, the page auto-advances. Toggle "Standard" vs "HRI (Rich Authorization Requests)" above the binding_message input.
  5. 5 · AIn Standard mode, click "Send approval request." A real Guardian push arrives on the enrolled device with the plain binding_message sentence.
  6. 6.Approve it on your phone. Tokens are decoded and displayed back on the page.
  7. 7 · AClick "Send another request," switch the toggle to HRI mode. Note the preview card showing itemized fields (Amount, Order, Action) instead of a single sentence.
  8. 8 · ASend the request. Compare the push notification on the phone — Guardian renders the same transaction as structured, itemized data rather than one opaque string.
  9. 9.Try denying a request on the phone, and separately let one expire by waiting out the countdown — confirm both error paths render distinctly (denied vs expired).

What to look for

  • The Guardian enrollment ticket URL is meant to be opened as a link, not fed into Guardian's own in-app QR scanner — that exact mismatch is what broke it during development (the in-app scanner expects a different payload format).
  • Standard mode's binding_message is one opaque string the approver has to trust; HRI mode's authorization_details are itemized fields Guardian renders natively — same underlying transaction, genuinely different trust properties.
  • HRI requires the Guardian schema type (urn:auth0:schemas:authorization-details:v1:user-profile) to be registered on the resource server first — Auth0 rejects unregistered types with invalid_authorization_details rather than silently ignoring the field.
  • No persisted consent — a repeat request seconds later still prompts fresh; Auth0 never caches a prior approval.
14. Device Authorization Grant
/demo/device-code
RFC 8628 — no prior session, no MFA dependency, works on any Auth0 plan. The only demo with no cookie session on the initiating side at all.
  1. 1 · AOpen /demo/device-code directly — no need to sign in first. This simulates an input-constrained device (a CLI tool, a smart TV) with no browser of its own.
  2. 2 · AClick "Request device code." A user_code and QR code appear — the QR encodes verification_uri_complete. Scan it with your phone's plain camera app, not a special scanner.
  3. 3.On your phone (or a second tab), open the link, or type the user_code manually at the plain verification_uri shown. Complete a real Universal Login as any test user.
  4. 4 · AWatch the page poll and pick up the token set automatically once you approve — decoded claims and access token displayed.
  5. 5 · AClick "Request another code" and this time just let the countdown run out without completing login — confirm the expired-code error path renders distinctly from a denied one.

What to look for

  • This is the one demo in the whole suite with no cookie session dependency at all — the initiating page genuinely doesn't know who the user is until the second screen resolves it, so the sidebar's identity header stays blank here until you sign in somewhere else.
  • Auth0 requires app_type: "native" for this grant — the resulting client is public, with token_endpoint_auth_method: "none". There is no client secret anywhere in this flow.
  • Device-code phishing is a real, named risk (see the Security Implications section on this page) — an attacker can relay their own code or link to a victim under a pretext.
15. Hono on Cloudflare Workers
/demo/hono-workers
A real, separately-deployed confidential Auth0 client compared directly against Traditional and BFF — same OIDC flow, a genuinely different session-storage and runtime story.
  1. 1 · AOpen /demo/hono-workers. Read the comparison table before clicking anything — it is the actual point of this demo.
  2. 2 · AClick "Open the Hono/Workers demo" — opens the real deployed Cloudflare Worker in a new tab. Genuinely separate: its own repo, its own Auth0 application, its own deployment.
  3. 3.On the new tab, click "Log in." Note there's no branding to speak of — deliberately plain, since the point here is the mechanism, not the polish.
  4. 4.After login, land on /dashboard. The claims shown come straight from the SDK's getUser() call — already decoded, no JWT library needed on this stack.
  5. 5.Note the access token panel — audience, scope, and expiry, truncated rather than shown in full.
  6. 6.Log out, then try /dashboard again directly — redirected straight to Universal Login, no error page.
  7. 7 · ABack in this suite, note the session-storage line in the comparison table: this deployment uses a stateless encrypted cookie by default — there's no Firestore doc to inspect for this one, unlike Traditional/BFF.

What to look for

  • Same confidential-client model and OIDC flow as Traditional/BFF — the genuinely different axes are session storage (self-contained encrypted cookie vs Firestore-backed) and runtime (V8 isolate vs Node serverless), not the auth flow itself.
  • The default session mode has no server-side kill switch — ending a session here means key rotation or waiting out the TTL, not deleting a database row like Traditional/BFF's Back-Channel Logout does.
  • Building this surfaced real, undocumented gotchas: the SDK's authRequired option defaulting to true (401'd the homepage until set explicitly), an undocumented nodejs_compat requirement, and Cloudflare rejecting a "future" compatibility date — none of which the original SDK guide mentions.
16. Cross App Access (XAA)
/demo/cross-app-access
ID-JAG-gated agent-to-app access via a real Auth0 jwt-bearer grant. Enterprise IdP simulated locally; Todo0 (Resource App) is real Auth0. Agent0's redemption call attempts real Auth0 first and falls back to a labeled mock — Auth0 only supports the Resource App side of XAA as of writing.
  1. 1.Open /demo/cross-app-access. No login required — this demo runs as a fixed, pre-provisioned demo identity throughout.
  2. 2.Click "Mint mock ID token" — simulates the enterprise SSO login into Agent0 that's covered elsewhere in this suite.
  3. 3.Click "Exchange for ID-JAG." Note the IdP-signed JWT: short exp (~60s), aud naming Todo0, client_id naming Agent0 as intended holder.
  4. 4.Click "Redeem for access token." Watch the exact request built — grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer, assertion=<ID-JAG>, client_id/client_secret — sent to the real, shared Auth0 tenant. Note the badge: Auth0 doesn't yet support Requesting App redemption, so this shows a clearly-labeled mock alongside Auth0's real rejection response.
  5. 5.Click "Call Todo0 API." The protected endpoint verifies the Bearer token (real or mock, whichever it received) and returns data with the caller's identity echoed back.
  6. 6.Toggle "IT admin revokes access" to Revoked. Re-run the ID-JAG step — the mock IdP itself returns 403 access_denied before an ID-JAG is even minted.
  7. 7.Toggle back to Active and re-run to confirm the flow recovers.

What to look for

  • sub is identical from the ID token through to the final access token, mocked or real — the end user's identity is preserved end-to-end.
  • No OAuth consent screen appears anywhere in this flow — the trust decision was made centrally via the admin policy toggle, not per-request by the user.
  • The 403 from the toggle comes from the mock IdP's mint call, not from Auth0 — governance is enforced at the point of assertion issuance, a separate boundary from Auth0's own real XAA policy.
  • The redemption step always makes the real call first — once Auth0 ships Requesting App support, the "🧪 Mocked" badge should flip to "✅ Real Auth0" with no code changes on this end.
  • Compare the "With XAA" vs "Without XAA" panel above the demo — same end goal, four steps and no redirect vs. a traditional OAuth consent dance repeated every time.
17. Session Delegation
/demo/session-delegation
Auth0's new paved path for impersonation-style access: a support agent mints a Session Transfer Token via Custom Token Exchange, naming a different (subject) user and their own ID token as actor_token. Uses its own fixed demo accounts, not the shared test user from the rest of this script — support.agent@atko.email as the actor, and alice.customer@atko.email / bob.customer@atko.email as the two customers you can delegate as. Password for all three: SessionDelegation!Demo2026.
  1. 1.Open /demo/session-delegation and sign in as the support agent (support.agent@atko.email). This is a real, separate Auth0 login — not the shared test user.
  2. 2.Pick Alice or Bob from the customer picker and click "Mint Session Transfer Token." Read the actual request shown: grant_type=token-exchange, subject_token identifying the chosen customer, actor_token = the agent's own ID token.
  3. 3.Watch the 60-second countdown on the raw token — same single-use, short-lived shape as the native-to-web and cross-domain-sso STTs, but this one names a different subject than the actor who minted it.
  4. 4.Click "Skip logout first — see Auth0's real error page" instead of the normal redeem button. Because a delegated session can never coexist with any other session on the domain — including the actor's own — this fails visibly. Come back and mint a fresh token afterward (the first one is now consumed/expired).
  5. 5.This time click "Redeem as customer." The demo logs the agent out of the Auth0 domain first, then redeems the token — landing you in an authenticated session as the customer with no password prompt.
  6. 6.Open the act claim panel on both the ID Token and Access Token. sub is the customer; act.sub is the support agent who delegated the session — recorded on the token itself, not just in this app's UI.
  7. 7.Check the live tenant log panel below it — real sdel/sdeleacft/sdelsa entries from this tenant, not a simulated log.
  8. 8.Follow the "log in normally" link at the bottom to sign in as the same customer with a real password. Delegated sessions suppress last-login/login-count updates and welcome emails, but any custom ID token claim set during the delegated session persists — this link demonstrates that side effect directly rather than just describing it.

What to look for

  • act claim shape: { sub, sub_profile: "human", role: "support" } — richer than the plain act.sub seen in the OBO demo, since Session Delegation is built for a human actor, not a service.
  • The domain-wide "log out first" rule is genuinely surprising the first time you hit it — it blocks on any active session on the domain, not just a session belonging to the subject being impersonated.
  • No refresh token is ever issued for the delegated session, and both the AT and the session share a hard, non-configurable 2-hour ceiling.
  • The Management API schema for this feature (found empirically, not from the docs) nests everything — mint-side and redeem-side settings alike — under one session_transfer object on the client; Auth0's own error message labels it "Native to Web SSO Configuration," confirming it's the same underlying mechanism as phase 7.
18. Token Vault — Connected Apps
/demo/token-vault
Auth0 Connected Accounts + getAccessTokenForConnection() link Google Calendar and GitHub as secondary connections, independent of whatever connection you logged into this app with. Two columns, side by side — connect only one of the two and the other stays clearly labeled "simulated" rather than failing outright.
  1. 1.Open /demo/token-vault. Both columns load first — each independently, showing either real data (if connected) or a clearly-labeled simulated fallback.
  2. 2.Click "Connect Google Calendar." This is a real, separate OAuth consent — Auth0 Connected Accounts, not the login connection you signed into this app with.
  3. 3.Once connected, the Google column swaps from simulated to real: your actual upcoming Calendar events render, and "Simulated" disappears.
  4. 4.Click "Show access token" underneath the real results. This is the literal token getAccessTokenForConnection() returned for the google-oauth2 connection, plus its expiry.
  5. 5.Repeat for GitHub — connect it, watch the column switch from simulated repos to your real ones.
  6. 6.Click "Disconnect" on either column. The column reverts to "Not connected" and the data flips back to simulated on the next fetch — no page reload needed.

What to look for

  • If you originally logged into this app via Google, that column shows "Signed in (profile only)" instead of "Not connected" until you explicitly connect it — being signed in with a provider is not the same as having a Connected Account for it.
  • The access token shown is scoped narrowly — calendar.readonly or public_repo — not the same token or scope as this app's own session.
  • Disconnect calls a real Management API revocation, not just a local UI toggle — reconnecting afterward requires consent again.
19. Ephemeral Sessions
/demo/ephemeral-sessions
A dedicated always-ephemeral client where a Post-Login Action unconditionally calls api.session.setCookieMode('non-persistent') plus tightened setExpiresAt/setIdleExpiresAt(90s absolute, 30s idle) on every login — no client-asserted "shared device" flag involved. No offline_access scope, so no refresh token is ever minted. Compare with phase 1 (Traditional Web App) for the persistent-session contrast.
  1. 1.Open /demo/ephemeral-sessions and log in. Every login on this client is treated as ephemeral — there is no toggle to opt out.
  2. 2.Check the "Post-Login Action Proof" card. It decodes the https://example.com/ephemeral custom claim straight off the ID token — claim mode, absolute/idle expiry timestamps, and when the Action ran.
  3. 3.Watch the countdown underneath tick down both the absolute and idle deadlines in real time.
  4. 4.Watch the "Token Validity Polling" card — it hits /api/protected/test every 5s. Each poll is activity against this app's own session, so the idle deadline shown pushes forward every 5s while the absolute deadline stays fixed.
  5. 5.Click "Pause polling," then wait past the idle deadline shown (30s) without touching the page. Resume polling — the next call should come back expired even though the fixed absolute deadline (90s) may not have hit yet, proving idle is enforced independently.
  6. 6.Read the "Two sessions, both shortened" card — the Auth0 AS-side session (what the Action controls) and this Next.js app's own session cookie are separate mechanisms that just happen to be aligned to the same windows here.
  7. 7.For the strongest proof, quit every window of your browser entirely (not just the tab), then reopen and return to this page — you'll be signed out, because both cookies were session-only/short-lived rather than persistent.

What to look for

  • The ephemeral claim comes from a Post-Login Action applied unconditionally to this client — nothing in the browser or login request asserts "treat me as ephemeral."
  • No offline_access scope means getSession() on this client can never silently refresh — once the access/ID token expires, re-authentication is the only path back in.
  • Idle and absolute deadlines are enforced independently — pausing polling to let idle lapse expires the session well before the absolute cap would have.
Cleanup

When you're done, delete the test user from the Auth0 dashboard (User Management → Users) rather than leaving it around — it keeps the tenant's user list legible for the next person running through this script.