Files
rose-ash/plans/identity-on-sx.md
giles baee67f561
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 37s
identity: refresh-token rotation + cascading revocation (token.sx grant-centric, +9 tests)
The grant {Subject,Client,Scope,Status} becomes the unit of authorization
and cascade; access + refresh tokens reference it. issue_grant returns an
access+refresh pair; refresh (RFC 6749 §6) supersedes the presented refresh
token and mints a fresh pair; reusing a superseded refresh token is treated
as theft (RFC 6819 §5.2.2.3) and revokes the whole family, killing the live
descendant. revoke of any token cascades to the grant. All prior token
behaviour preserved. token 18/18, 62/62.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 00:26:05 +00:00

6.7 KiB

identity-on-sx: OAuth2, sessions & membership on Erlang

DRAFT outline. The identity core acl-on-sx assumes already exists. acl answers "may X do Y"; identity answers "who is X, and how did they prove it." Depends on persist-on-sx (grant/audit ledger). Pairs with acl-on-sx.

rose-ash's account domain is the OAuth2 authorization server every other app is a client of: silent SSO, per-app first-party cookies, grant verification, membership. Sessions and grants are long-lived, concurrent, individually addressable, and expire on their own — that is the actor model. Erlang's processes + mailboxes map cleanly: a session is a process, token issue/refresh/ revoke are messages, expiry is a process timeout, and SSO is one process answering many apps.

End-state: an Erlang-on-SX layer with the OAuth2 authorization-code + silent (prompt=none) flows as message protocols, a session/grant registry, token lifecycle (issue/refresh/revoke/introspect), and membership state — all auditable through the event log, all authorization questions delegated to acl-on-sx.

Status (rolling)

bash lib/identity/conformance.sh62/62 (Phase 1 + authz-code + refresh/rotation/cascade)

Ground rules

  • Scope: only lib/identity/** and plans/identity-on-sx.md. May import from lib/erlang/, and (once they exist) lib/persist/ + lib/acl/. Do not edit substrates.
  • Architecture: a session/grant is a process holding its own state; the registry routes messages by subject/client id. Tokens are opaque + introspected, not self-validating (revocation must be real). Authorization decisions are NOT made here — identity proves identity, acl decides permission.
  • Security: revocation is immediate (kill the process / tombstone the grant); no decision relies on a token that outlived its grant. Negative answers are explicit, never "absence of a yes."
  • Commits: one feature per commit. Progress log + tick boxes.

Architecture sketch

Auth request                            Token / session
  (authorize client scope subject)        {:access :refresh :expires :grant}
        │                                       ▲
        ▼                                       │
lib/identity/oauth.sx                   lib/identity/token.sx
  — authz-code + prompt=none flows        — issue / refresh / revoke / introspect
  — as Erlang message protocols           — opaque tokens, grant-backed
        │                                       ▲
        ▼                                       │
lib/identity/session.sx                 lib/identity/registry.sx
  — session = process, expiry=timeout     — route by subject/client; SSO fan-out
        │                                       │
        ▼                                       ▼
lib/identity/api.sx ── (identity/login) (identity/grant?) (identity/revoke) ──┐
        │                                                                      │
        └──────── grant + audit events → persist ;  permission? → acl ──────────┘

Phase 1 — Sessions + tokens

  • session.sx — session process, create/lookup/expire
  • token.sx — issue/introspect/revoke (opaque, grant-backed)
  • registry.sx — route by subject/client
  • api.sx + tests + scoreboard + conformance.sh

Phase 2 — OAuth2 flows

  • authorization-code flow as a message protocol
  • refresh + rotation; revocation cascades to issued tokens
  • tests: full code exchange, refresh, revoke-then-use (must fail)

Phase 3 — Silent SSO + membership

  • prompt=none cross-app login (one session, many clients)
  • membership state + per-app grant projection
  • grant verification delegated cache (mirror Redis-cache pattern)

Phase 4 — Audit + federation

  • every issue/refresh/revoke is a persist event; (identity/audit subject)
  • federated identity (peer-asserted subject) — advisory, trust-gated stub
  • tests: audit completeness, cross-instance subject mapping

Progress log

  • 2026-06-07 — token.sx grant-centric rewrite: refresh-token rotation (RFC 6749 §6) + cascading revocation. The grant {Subject,Client,Scope, Status} is the cascade unit; access + refresh tokens reference it. issue_grant → {ok, Access, Refresh}; refresh supersedes the old refresh + mints a new pair; reusing a superseded refresh token revokes the whole family (RFC 6819 §5.2.2.3), killing the live descendant. revoke of ANY token (access or refresh) cascades to the grant. All prior issue/introspect/revoke behaviour preserved. +9 → token 18, 62/62.
  • 2026-06-07 — oauth.sx: OAuth2 authorization-code flow as a message protocol (RFC 6749 §4.1) + PKCE (RFC 7636, plain). State machine on one authz-server process: authorize → {consent_required} → consent → {code} → exchange → {ok, Token}. Exchange enforces single-use codes (§10.5; removed on first attempt, replay → invalid_grant), client_id + redirect_uri binding (§4.1.3), and PKCE verifier match. Issued tokens are grant-backed so revocation stays real. +14 → 53/53.
  • 2026-06-06 — api.sx: service facade. identity:start() spawns one coordinator owning the token table + session registry; exposes login/verify/revoke/logout/session_status. Coordinator is the sessions' owner, so an expired session deregisters itself (timeout-driven, no sweep). verify answers IDENTITY only ({active, Subject, Client, Scope}); permission is acl's job — explicit delegation boundary. Phase 1 complete. +10 → 39/39.
  • 2026-06-06 — registry.sx: directory process routing sessions by id and by (subject, client). Answers the SSO probe lookup(Subject, Client) and the fan-out sessions_for(Subject) (one subject, many clients). Routes only — holds no grant state. Integration-tested end-to-end: register a live session, route to it, confirm it answers active. +9 → 29/29.
  • 2026-06-06 — token.sx: opaque grant-backed tokens. Token = make_ref (carries no info); the token table is a process; introspect is a live lookup every time so revocation is real (RFC 7009) — a revoked token reads {inactive} on the next introspection, no validity window. Reply shapes follow RFC 7662 §2.2 ({active,...} / {inactive}, never says why). +9 → 20/20.
  • 2026-06-06 — session.sx: session-as-Erlang-process. create/lookup/touch/ explicit-expire/revoke as messages; idle-timeout self-expiry via receive ... after Ttl notifying the owner then tombstoning. Tombstones answer lookups with {error, expired|revoked} — never a silent dead mailbox. Established the conformance harness (conformance.sh, scoreboard, tests/session.sx). 11/11.

Blockers

(loop fills this in)