Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 57s
identity:start() spawns one coordinator owning the token table + session
registry and exposes the whole-domain ops. The coordinator is the owner
sessions notify on idle timeout, so an expired session deregisters itself
— timeout-driven, never swept. verify/2 answers identity only ({active,
Subject, Client, Scope}); permission is delegated to acl. 39/39.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
5.7 KiB
Markdown
107 lines
5.7 KiB
Markdown
# 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.sh` → **39/39** (Phase 1 complete: session, token, registry, api)
|
|
|
|
## 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
|
|
- [x] `session.sx` — session process, create/lookup/expire
|
|
- [x] `token.sx` — issue/introspect/revoke (opaque, grant-backed)
|
|
- [x] `registry.sx` — route by subject/client
|
|
- [x] `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-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)
|