Skip to content

auth and session

Kadyapam edited this page May 24, 2026 · 1 revision

Auth and session

How the SPA authenticates the user and how it stays signed in.

The model is Auth0 for end-user identity → gateway for session. The SPA never holds a long-lived API token. The gateway issues a short-lived session_token that travels on every request.

In-repo references:

Flow

┌──────────┐    1. Auth0 SPA login          ┌──────────┐
│ Browser  │ ◄────────────────────────────► │  Auth0   │
│          │       (id_token, hash flow)    │          │
└────┬─────┘                                └──────────┘
     │
     │ 2. POST /api/auth/login { id_token }
     ▼
┌──────────────────────────┐
│   NoETL Gateway          │
│   - verifies id_token    │
│     against Auth0 JWKS   │
│   - upserts user         │
│   - issues session_token │
└──────────┬───────────────┘
           │ 3. response: { session_token, expires_at, user_id }
           ▼
┌──────────┐
│ Browser  │   stores session_token in localStorage
│          │   includes it on every subsequent gateway request
└──────────┘

Subsequent requests carry the session_token. On app load, the SPA validates the token (POST /api/auth/validate) before trusting it; on validation failure, the SPA clears storage and routes to Auth0 login.

What lives where

Concept Where Why
Auth0 tenant config VITE_AUTH0_DOMAIN, VITE_AUTH0_CLIENT_ID (build-time) Identifies the Auth0 application the SPA logs into.
Auth0 ID token Browser memory (Auth0 SDK in-memory only) Short-lived. Exchanged for gateway session immediately.
Gateway session_token Browser localStorage Used for all subsequent gateway calls until expiry.
Auth0 client secret Gateway environment (k8s Secret) Never reaches the browser; only the gateway needs it to verify ID tokens.
User record Gateway DB (auth.users) Upserted on first login.
Session record Gateway DB (auth.sessions) Tracks active sessions, expiry, IP.

The SPA bundle has zero secret material. The Auth0 domain and SPA client ID are public values (they appear in Auth0's authorize URL).

The auth0_login playbook

The user-facing flow happens partly in NoETL. The gateway's /api/auth/login does not write to the DB itself — it dispatches the api_integration/auth0/auth0_login playbook (or its optimized variant) which:

  1. Validates the JWT signature against Auth0 JWKS.
  2. Upserts the user row in auth.users keyed by auth0_id.
  3. Inserts a new row in auth.sessions with a random session_token and 24-hour TTL.
  4. Caches the session in NATS KV (bucket sessions).
  5. Sends send_success_callback back to the gateway, which forwards the result to the SPA over SSE.

Source (the optimized 3-step variant): fixtures/playbooks/api_integration/auth0/auth0_login_optimized.yaml.

Even the auth flow itself is a playbook. The gateway calls one; it doesn't open a DB connection itself.

Guest mode

For local development, the SPA can run without authentication:

VITE_ALLOW_GUEST=true

In guest mode, the SPA calls NoETL directly (/execute) instead of going through the gateway's authenticated GraphQL endpoint. Use this for local SPA iteration against a local NoETL.

Guest mode is never used in production. The gateway URL is set to a Cloudflare-fronted address that requires the session_token.

Swapping the identity provider

The "Auth0" part of the flow is replaceable. To swap in a different OIDC provider (e.g. Cognito, Keycloak, Google Identity Platform):

  1. Change @auth0/auth0-react to your provider's SDK in the SPA. The contract you need at the SPA layer is just "give me an OIDC ID token".
  2. Update the gateway's auth0 verification logic to point at the new JWKS. The gateway has the JWKS URL in its config.
  3. Adjust the auth0_login playbook (or write a new one) to handle any provider-specific token claims you care about (the nickname, email, picture fields, etc).
  4. Update the relevant VITE_* env vars at build time.

The rest of the stack — SSE delivery, session token, playbook dispatch — does not care which OIDC provider issued the ID token. The gateway's session model is intentionally provider-agnostic.

Session expiry

Gateway sessions expire after 24 hours by default (INTERVAL '24 hours' in the auth0_login playbook's create_user_session step). On expiry:

  • The SPA's next gateway call returns 401.
  • The SPA clears storage and redirects to Auth0 login.
  • The Auth0 SDK silently re-authenticates if the Auth0 session is still live (otherwise prompts for credentials).

There is no refresh-token flow today. If the SPA needs longer sessions, increase the playbook's interval.

Credential trail

For pushback against "could we just put X in env":

  • Auth0 client secret? Gateway only. Never in SPA env, never in worker env.
  • Database connections used by the auth playbook? Pulled from the keychain by alias ({{ db_credential }}) — currently pg_auth. The playbook references the alias; the keychain resolves to the actual DSN at step execution.
  • NATS connection used by the auth playbook? Same pattern, {{ nats_credential }}.

See the secrets-and-credentials rule in the architecture doc.

Related

Clone this wiki locally