| 1 | package sheets |
| 2 | |
| 3 | import ( |
| 4 | "net/http" |
| 5 | |
| 6 | "amadan.net/oficina/home/connect" |
| 7 | "amadan.net/rastrillo/rastrillo/auth" |
| 8 | ) |
| 9 | |
| 10 | // handleBegin is POST /signin, in both modes: auth's Begin for EVERY |
| 11 | // address, under connect.OAuthContinuation. |
| 12 | // |
| 13 | // What the form answers depends on the address's domain, never on |
| 14 | // admission. Answering "that address cannot sign in" — or answering an |
| 15 | // admitted address differently from a refused one in any way — would |
| 16 | // turn this endpoint into a membership oracle: anyone could enumerate |
| 17 | // who is on the team by watching which addresses get a different |
| 18 | // answer. So: |
| 19 | // |
| 20 | // - A Keymail address (a domain that runs Keymail, or delegates to it |
| 21 | // with a `_keymail` TXT record), admitted or not, goes to Keymail. |
| 22 | // Begin answers with a 303 to Keymail's authorize URL, and |
| 23 | // OAuthContinuation turns that into a page (renderHomeOAuth): CSP's |
| 24 | // form-action 'self' covers a form submission's whole redirect |
| 25 | // chain in Safari, so a 303 out of the POST was refused there |
| 26 | // ("Refused to load https://keymail.dev/oauth/authorize… form-action", |
| 27 | // Paul, 2026-10-06). Admission is asked after Keymail has verified |
| 28 | // the address, by auth's Authorize (App.Admits), and a refusal is |
| 29 | // the door saying so (App.refused), told only to someone who has |
| 30 | // just proved they hold the address. |
| 31 | // - Any other address is answered ?sent=1, and mailQueue mails the |
| 32 | // link only if App.Admits says yes — after the response, in a |
| 33 | // worker, so not even the response time differs (mailqueue.go). Without that, anyone who |
| 34 | // can reach the instance could make it email a live sign-in link to |
| 35 | // any address they name — abuse of our sending identity, found by |
| 36 | // POSTing a stranger's address at the deployed instance. The cost |
| 37 | // is real and accepted: a member who mistypes their own address is |
| 38 | // told to check their email and nothing arrives. |
| 39 | // |
| 40 | // Both kinds of address spend the same rate budgets and are parsed the |
| 41 | // same way, so ?err=rate and ?err=address say nothing about admission |
| 42 | // either; and since the send happens after the response, a failed one |
| 43 | // is logged rather than answered. |
| 44 | // |
| 45 | // It used to gate admission in front of Begin instead, which answered a |
| 46 | // non-admitted Keymail address ?sent=1 and sent an admitted one to |
| 47 | // Keymail: the oracle, one level narrower. |
| 48 | func (a *App) handleBegin(w http.ResponseWriter, r *http.Request) { |
| 49 | connect.OAuthContinuation(a.origin, a.auth.Begin, a.renderHomeOAuth)(w, r) |
| 50 | } |
| 51 | |
| 52 | // refused is auth's Refused hook: Keymail or an emailed link verified |
| 53 | // the address and Admits said no. It answers with the sign-in door, |
| 54 | // carrying the refusal, in a 403 — no session is minted, and auth has |
| 55 | // already ended the attempt. |
| 56 | // |
| 57 | // Rendered, not redirected to ?err=refused: a query parameter is |
| 58 | // something anybody can type, and the sentence means "you proved this |
| 59 | // address and it is not admitted" (Astra on oficina/slides #20, round |
| 60 | // 3). Only this hook can say it, and auth calls it only after the |
| 61 | // address is proved, so saying it is no membership oracle. |
| 62 | func (a *App) refused(w http.ResponseWriter, r *http.Request, _ auth.Identity) { |
| 63 | a.renderDoor(w, r, http.StatusForbidden, |
| 64 | "That address can’t sign in to Sheets. Use a different address, or ask someone to share a sheet with it.", false) |
| 65 | } |
| 66 | |