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>
6.7 KiB
identity-on-sx: OAuth2, sessions & membership on Erlang
DRAFT outline. The identity core
acl-on-sxassumes already exists.aclanswers "may X do Y"; identity answers "who is X, and how did they prove it." Depends onpersist-on-sx(grant/audit ledger). Pairs withacl-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.sh → 62/62 (Phase 1 + authz-code + refresh/rotation/cascade)
Ground rules
- Scope: only
lib/identity/**andplans/identity-on-sx.md. May import fromlib/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 —
identityproves identity,acldecides 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/expiretoken.sx— issue/introspect/revoke (opaque, grant-backed)registry.sx— route by subject/clientapi.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=nonecross-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
persistevent;(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.sxgrant-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};refreshsupersedes 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.revokeof 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).verifyanswers 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 probelookup(Subject, Client)and the fan-outsessions_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;introspectis 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 viareceive ... after Ttlnotifying 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)