From a3584224e91a31bf1f0e2bfd71c6f40ef400ff06 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Wed, 19 Aug 2026 10:39:35 -0400 Subject: [PATCH] docs: reject the confidential OAuth client, with the reason A confidential client must be a web client, and only native clients may register a loopback redirect, so it would need a hosted HTTPS callback between a login and the PDS. atgc will not take a runtime dependency on a server to log in. Recorded as a rejection rather than deleted so it is not proposed again, and architecture.md no longer offers it as the way out. Co-Authored-By: Claude Opus 5 (1M context) --- TODO.md | 55 ++++++++++++++++++++++++++++++++++++++------ docs/architecture.md | 10 ++++++-- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/TODO.md b/TODO.md index 6a24c71..342b262 100644 --- a/TODO.md +++ b/TODO.md @@ -21,13 +21,54 @@ 403 from the PDS on the first real comment. Adding it changed the client_id, which is a re-login for everyone; the legacy entry stays beside it, since deleting a legacy comment still needs it -- [ ] Confidential client for long sessions: host client metadata + jwks on - lance.blue, keep the private key in ClientData.keyset. Also the only way - to show "atgc" on the PDS consent page instead of "an application on - your device" — localhost clients can't set a client_name. Not the - "180-day sessions" this line used to claim: that is the spec's cap on - one refresh *token*, and Bluesky gives confidential clients a 2-year - session with 3-month tokens. See docs/architecture.md +- [ ] **The confidential client was researched and rejected**, written down + here so nobody re-proposes it. It would have bought real things: a + 2-year session with 3-month refresh tokens instead of the public + client's two weeks (`oauth-constants.ts`, `SESSION_LIFETIME_EXTENDED` + / `REFRESH_LIFETIME_EXTENDED` — so the claim this entry used to make + was correct), and the word "atgc" on the PDS consent page instead of + "an application on your device", which a localhost client cannot have + at any price. + + It is rejected on a rule, not on effort. The reference authorization + server refuses `application_type: "native"` together with + `private_key_jwt` (`oauth-provider/src/client/client-manager.ts`, + citing RFC 8252 §8.4), and loopback and private-use-scheme redirect + URIs are permitted *only* to native clients. Chain the two and a + confidential client is a `"web"` client, every one of whose redirect + URIs must be `https://`. **A confidential atgc cannot have a loopback + callback.** + + So the plan this entry used to describe — static client metadata and a + jwks on lance.blue — is necessary and nowhere near sufficient. It also + needs a *live HTTPS callback service* to receive the authorization + code and hand it back to a CLI that may be headless, over SSH or + behind NAT: either paste-back, or a stateful rendezvous that holds + authorization codes and is therefore a credential-handling endpoint to + secure, rate-limit and keep up. **atgc is not taking a hosted runtime + dependency to log in.** Everything else here reads a PDS directly and + degrades to "the index is out" rather than "the service is down", and + a login that requires lance.blue to be answering trades that away for + a longer token. + + The second reason stands even if the first were lifted. The private + key must sit wherever the CLI runs, because the CLI mints a client + assertion on every refresh — so it is either shipped with the binary, + which makes it not a secret and hands everyone a key that impersonates + "atgc" to every PDS, or generated per user, in which case the consent + page shows their name and not ours, which was the point. Proposal 0010 + (client-assertion-backend) is the named escape hatch upstream and + targets browser SPAs; it does not answer this for a CLI. + + Two findings worth keeping out of the wreckage. jacquard already + supports confidential clients end to end and would need no patching — + `keyset.rs` (`generate_es256`, `public_jwks`), `atproto.rs` + (`AuthMethod::PrivateKeyJwt`, inline `jwks`) and `request.rs` + (`build_auth` mints an RFC 7523 assertion). And the cheaper goal is + still open: if the complaint is re-authorizing every fortnight, make a + lapsed session pleasant — `auth refresh` exists, and a clear prompt at + the moment one lapses costs hours and adds no service. Revisit only if + the spec permits a confidential client a loopback redirect - [x] Several accounts at once, keyed by DID: an accounts.json registry beside jacquard's session store, `--account` / `ATGC_ACCOUNT` and a checkout's own `user.email` DID feeding one precedence chain. Login diff --git a/docs/architecture.md b/docs/architecture.md index 671a393..69fc708 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -100,8 +100,14 @@ mentioning keys. atgc is a public (localhost) OAuth client, and the spec caps those sessions at two weeks. A lapsed token is not a logged-out account: the registry outlives it, so the account still lists and is still selectable, and `atgc auth login` -re-authorizes it. Lifting the cap needs a confidential client with hosted -metadata. +re-authorizes it. + +The cap is not liftable on terms atgc will take. Only a confidential client +gets a longer session, only a `web` client may be confidential, and only a +native client may use a loopback redirect — so a confidential atgc would need +a hosted HTTPS callback standing between a login and the PDS. That is a +runtime dependency on somebody's server for the one command that has none, and +it is refused. See TODO.md. The `client_id` a grant is issued to includes the login's ephemeral callback port, so it cannot be reconstructed later. It is recorded at login, and a -- 2.51.2