diff --git a/plan/README.md b/plan/README.md index 4df67fb3..bcf31073 100644 --- a/plan/README.md +++ b/plan/README.md @@ -125,6 +125,7 @@ The exit criterion is met and something is still open in the file. Nothing yet. | [account-types](account-types.md) | Not every account is a session | open | | [scrobble](scrobble.md) | An agent says what it is working on, and the statement is its own record | open | | [write-policy](write-policy.md) | Which agents may write which record types | open | +| [auth-types](auth-types.md) | Every credential this server accepts, and what each one may do | open | | [oauth](oauth.md) | A third-party app signs in as an agent, with nobody at the consent screen | open | | [pds-xrpc](pds-xrpc.md) | A client nobody here wrote can talk to this server | open | | [credentials](credentials.md) | A session gets a credential without a wrapper process | open | diff --git a/plan/auth-types.md b/plan/auth-types.md new file mode 100644 index 00000000..7bcd5fef --- /dev/null +++ b/plan/auth-types.md @@ -0,0 +1,107 @@ +--- +id: auth-types +title: Every credential this server accepts, and what each one may do +status: open +crates: [vibescrobble-serve, vibescrobble-pds] +dependsOn: [agent-accounts, pds-writes] +exitCriterion: > + Every route the router serves names the credential types it accepts, a + request carrying none is refused wherever the route needs a principal, and + each accepted type is driven end to end by the conformance harness. +--- + +# auth-types + +Nothing authenticates. Every `com.atproto.*` route answers whoever reaches it, +and where a route needs to know which repository it is acting on it reads that +off a parameter or a header — `uploadBlob` takes the account from a header +precisely because there is no session to take it from. Read paths are +unauthenticated by accident rather than by decision, and no write path refuses +anybody. + +[oauth](oauth.md) builds one credential and the server that issues it. This +epic is the other question, which that one does not answer: which credential +types exist here at all, which routes take which, and what a route does when a +request carries none. A server that implements only OAuth still has to answer +the client that arrives holding an app password, and it has to answer it in a +way that says so. + +## The types + +**OAuth access tokens, bound with DPoP.** The atproto-native one and the only +one a third-party app should ever get. Issuing it is [oauth](oauth.md); +accepting it is here — proof verification, nonce issuance, replay, and the +thumbprint check that makes the token useless to anyone who copies it. + +**Legacy session tokens.** `com.atproto.server.createSession`, `refreshSession` +and `deleteSession`, with an app password behind them. Most deployed atproto +software speaks this and nothing else, so refusing it is a decision about who +can talk to this server, not a detail. + +**Inter-service auth.** The short-lived signed JWT from +`com.atproto.server.getServiceAuth`, naming an audience and a method. This is +how one service calls another as an account rather than on its own behalf, and +it is the credential this project's own components would use. + +**Operator auth.** [e-stop](e-stop.md) and anything under `com.atproto.admin.*` +need a caller the server trusts that is not an agent and is not an app. + +**The hook-stamped local identity** in [credentials](credentials.md) is not on +this list and must not end up on it. It says which agent is making a tool call. +It is not presented over the wire and it authenticates nothing to this server. + +- [ ] **Decide the set, then refuse everything else by name.** An unrecognised + scheme, and a recognised one a deployment has turned off, are different + answers, and a client can only act on the difference if the server states + it. +- [ ] **One extractor, and a per-route declaration of what it accepts.** The + route table already exists and is generated from one list so the + conformance harness cannot miss a route ([pds-xrpc](pds-xrpc.md)). The + accepted-credential set belongs on that same list, so a new route without + one fails a check rather than defaulting to open. +- [ ] **Answer the app-password question.** An app password is a human + affordance: a person makes one in a settings page and pastes it into a + client. An agent has no person and no settings page, so either the owner + mints them for their agents, or this server refuses `createSession` by + name and accepts that most existing clients cannot sign in. Whichever + way, it is a second issuer with its own lifetime, and + [oauth](oauth.md)'s reasoning about revocation — withdrawal is refusal + at the write path — has to hold for it too. +- [ ] **Inter-service auth, before something else grows into its place.** + `aud`, `lxm` and `exp` checked, clock skew bounded, and the signing key + named. [index](index.md) reads these servers today with no credential + because everything is public; the first non-public read is the moment + this is needed, and by then whatever was used instead is load-bearing. +- [ ] **Say what an operator credential is and keep it separate.** The halt + path has to work when the authorization server is what is broken, so it + cannot depend on one. It also cannot be a shared secret in a file that + every agent's process can read. +- [ ] **State what stays open to everyone.** `getRepo`, `getBlocks`, + `listRepos`, `describeRepo`, the DID documents and `/firehose` are public + now because nothing refuses anything. Which of them are public on purpose + is a decision this epic writes down, and it is the same decision + [spaces](spaces.md) needs an answer to. +- [ ] **The failure shapes, and the headers that go with them.** + `AuthenticationRequired`, `ExpiredToken` and `InvalidToken` are distinct + recoveries — retry, refresh, and give up — and a `WWW-Authenticate` or + `DPoP-Nonce` header is how a client learns which. They join the mapping + in `crates/vibescrobble-serve/src/error.rs` rather than starting a + second one. +- [ ] **Take the account off the header.** `uploadBlob` names its repository in + a request header because the lexicon takes no parameters and expects a + session. That header is a stand-in and it comes out when a session + exists; until it does, anyone can upload a blob into anyone's repository. +- [ ] **Per-principal rate limits.** [pds-xrpc](pds-xrpc.md) has the question + of a swarm sharing one address. Once a request carries a principal, the + limit can be about the account instead of the connection, and the two + are different defences. +- [ ] **A route that needs a principal and has none is a bug the tests can + see.** The conformance harness drives every route in-process already, so + the unauthenticated case can be asserted for each rather than reasoned + about once. + +## Done + +Nothing. The set comes first: what this server accepts is a decision about who +can reach it, and building an extractor before the list is settled fixes the +list in code where nobody reads it. diff --git a/plan/order.txt b/plan/order.txt index 259853df..46f56826 100644 --- a/plan/order.txt +++ b/plan/order.txt @@ -19,6 +19,7 @@ account-types scrobble write-policy +auth-types oauth pds-xrpc credentials