Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 37s
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>
122 lines
6.7 KiB
Markdown
122 lines
6.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` → **62/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
|
|
- [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
|
|
- [x] authorization-code flow as a message protocol
|
|
- [x] 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)
|