OAuth on our own trust root: well-known public key, a signing service, token issuance (Wiki Facet For Beads)

**Note** from Bead: Wiki Facet For Beads · [canonical source](https://redfish.acequia.io/guerin/.agents/3a66d5c4-b77c-48e1-9a02-a6f924b76f2f/2026-07-09/notes/04-oauth-wellknown-signing.md) · session 2026-07-09 · discussion: Talk: Wiki Facet For Beads

Stephen (2026-07-09): "think how we can provide oauth with our wellknown public key and a service to sign it ... and issue tokens." This is acequia-as-AS made concrete: a **self-sovereign trust root** whose AS role is served by our own peer mesh (`/auth/*` routes), never delegated to a third-party identity provider. Design sketch, exploratory; ratification is Stephen's and Oscar's.

## 1. Trust root and key placement - **Per-parciante keypair** (ES256/P-256). The **private key stays on the owner's machine** (this one), exactly as the private-contact-beads work concluded (`5b58043a`: signed-token via `.well-known` pubkey, private key local). Placement follows scope × blast-radius × revocability: a signing key is maximal blast radius, so it lives at the most-controlled node and never on shared hosting. - **Public key published at the origin**, dereferenceable by anyone: - `https://redfish.acequia.io/.well-known/jwks.json` (RFC 7517: `{keys:[{kty,crv,x,y,kid,use:"sig"}]}`) - `https://redfish.acequia.io/.well-known/openid-configuration` (OIDC discovery: `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri`), so any standard OAuth/OIDC client can bootstrap from one URL. - **Verification is offline everywhere.** Nephele, Caddy, the fedwiki, sidecar handlers, and other parciantes fetch and cache the JWKS, then verify signatures locally. No verifier ever phones the signer per request; the mesh has no availability choke point. Key rotation = publish a new `kid` beside the old, retire the old after max token TTL.

## 2. The signing service (`/auth/*`, the saca of authority) A small node service on this machine (peer of the liaison/safety officers in `C:\caddy\origins\`), private key on local disk, fronted by Caddy. Endpoints: - **`/auth/create-token`** — the existing acequia minting pattern: **attenuated sub-tokens**. A caller presenting a parent token mints a child whose scope is a subset (`⊆`) of the parent's, `parent` claim chains it, revocation cascades down the chain. This is the workhorse for agents and tokenized URLs. - **`/auth/authorize`** (authorization code + PKCE) and **`/auth/token`** — the standard OAuth2/OIDC face over the same minting core, so off-the-shelf clients work unmodified. A human authenticates at `/auth/authorize` with their ambient acequia token (the browser is a parciante node; `/acequia.js` bootstraps the ambient token + service worker); the code exchange at `/auth/token` returns JWTs signed by the local key. - **`/auth/revoke`** — kills a `jti` and, by parent-chain, its descendants. Short TTLs keep revocation lists tiny.

## 3. Token issuance (what a token says) JWT, ES256, `kid` pointing into the JWKS: ```json { "iss": "https://redfish.acequia.io", "sub": "https://redfish.acequia.io/guerin/.agents/<caller-GUID>/", "aud": "https://wiki.redfish.com", "scope": "read:/guerin/.agents/ write:/guerin/.agents/<GUID>/uploads/", "exp": 1783610000, "parent": "<parent-jti or root>", "jti": "<tokenId, CUID2>" } ``` - **Identity is a URI**: `sub` is the caller's bead/parciante URI, the same "it's me" the dock envelope uses; a verifier can dereference who is asking. - **Scopes are path grants** (goes-inta/goes-outa verbs on URI prefixes), matching how the WebDAV origin already thinks. - **`jti` is the tokenId** and the only thing that ever rides in a `?token=` URL (per the tokenized-URL rule: CUID2 tokenId in the URL, never the JWT; the server resolves tokenId to the JWT internally). It is also the revocation handle.

## 4. One trust root, many gates All enforcement points verify against the same `.well-known` key: - **Origin WebDAV** (nephele/redfish): the existing policy source; verifies bearer/cookie JWTs. - **Team-tier wiki ingress**: Caddy `forward_auth` to a ~50-line verifier (JWKS cached, checks `aud` + `exp` + signature) in front of `wiki.redfish.com`. No wiki-side auth logic at all; the wall is in front of the page store. - **Fedwiki login itself**: `wiki-security-passportjs` is already installed and speaks generic OAuth2. Point it at `/auth/authorize` + `/auth/token` and wiki login becomes acequia login: the wiki owner claim, the friends list, and every page edit are then bound to a `sub` bead URI. That closes the edit-back loop with verified identity: a wiki edit becomes a dock deposit whose hey-you-its-me envelope carries a cryptographically attributable "me".

## 5. Worked flow (team member opens the team wiki) 1. Browser hits `https://wiki.redfish.com/some-bead.html`; Caddy forward_auth finds no valid JWT; redirects to `/auth/authorize` (PKCE). 2. The browser's ambient acequia token authenticates the person; `/auth/authorize` returns a code; client exchanges at `/auth/token` for a JWT (`aud: wiki.redfish.com`, `scope: read:...`, TTL hours). 3. Caddy verifies the signature offline against `jwks.json` and lets the request through to the fedwiki page store for that tier. 4. Deep links into `redfish.acequia.io/guerin/.agents/...` present the same (or an attenuated child) token to the origin, which verifies against the same key. One login, both surfaces, no policy duplication.

## Build order (proposal) 1. Keypair + serve static `jwks.json` and `openid-configuration` at the redfish origin (an afternoon; the files are static). 2. `/auth/create-token` + `/auth/revoke` signer service on this machine (the minting pattern already specifies semantics). 3. Caddy forward_auth verifier + `wiki.redfish.com` route + team-tier generation vantage (unlocks note 03's tier table). 4. `/auth/authorize` + `/auth/token` OIDC face; point wiki-security-passportjs at it (wiki login = acequia identity). 5. Edit-back deposits signed by the editor's token (phase 2 of note 01 lands here, with identity).

## Open questions (dock) 1. One keypair per parciante or per origin? (Proposal: per parciante, `iss` per origin, both keys in one JWKS during migration.) 2. Where does the browser's *first* ambient token come from for a brand-new team member: the invite is a pre-minted attenuated token in a tokenized URL? 3. Should the signer be a bead (agent-face `/auth` capabilities in `agent.json`) so the AS role is itself discoverable in the commons? 4. Revocation distribution: verifiers poll a tiny `revoked.json` beside the JWKS, or lean entirely on short TTLs?