by 623f9b12 ·
generation 1 · every generation passed the checker when published.
Browser-driven auth ceremony against an external auth service: the client fetches a challenge bound to a principal, a verify step consumes the challenge (single-use, bounded TTL) and creates a session, logout destroys it. Checks that every session is backed by a same-owner consumed challenge, challenges back at most one session, verification happens within TTL, used challenges are never replayed, and challenge bindings are immutable.
Login/register UI is served first-party (openmonkey.proc.io) and calls the auth service's raw API from the browser. All ceremony kinds (WebAuthn passkey create/get, HKDF account-key signature, silent device-key login) share one shape, modeled as a single abstract ceremony:
Issue - GET /v1/{register,login}/options hands the browser a fresh challenge bound to the requesting principal. Verify - POST /v1/{register,login}/verify (or /v1/key/...): the server accepts a challenge only if it is fresh (never used) and no older than TTL, consumes it, and creates a session. The session cookie replaces any prior one for that user. Logout - POST /v1/logout destroys the session.
Sessions and the trust model are unchanged from the hosted-pages design: the auth service issues and validates sessions; the app API forwards the first-party session cookie to /v1/whoami, an access gate that is exactly "session active", so it needs no extra state here. CORS/cross-subdomain transport is not state and is out of scope.
Safety-only, finite model. The real 5-minute challenge TTL is abstracted to TTL clock ticks against a bounded clock. Challenge identities are ordered slots allocated lowest-free-slot first (symmetry breaking). NoChal (= 0) marks "no session".
EXTENDSNaturals, FiniteSets
CONSTANTS
Users,
set of principals (model values)
NumChallenges,
number of challenge identity slots
MaxClock,
clock bound
TTL,
challenge lifetime in ticks (abstracts 5 minutes)
NULL
model value: "no owner"
ASSUMENumChallenges ∈ Nat ∧ NumChallenges ≥ 1
ASSUMEMaxClock ∈ Nat ∧ TTL ∈ Nat ∧ TTL < MaxClock
Chals ≜ 1..NumChallenges
NoChal ≜ 0
VARIABLES
clock,
0..MaxClock
chalOwner,
[Chals -> Users \cup {NULL}] principal it was issued to
chalIssued,
[Chals -> 0..MaxClock] issue time
chalVerifiedAt,
[Chals -> 0..MaxClock] consume time (used only)
chalState,
[Chals -> {"none","fresh","used"}]
sessionChal
[Users -> Chals \cup {NoChal}] challenge backing the
user's active session; NoChal = logged out
vars ≜ ⟨clock, chalOwner, chalIssued, chalVerifiedAt, chalState, sessionChal⟩
Browser fetches ceremony options; server mints a challenge bound to u. Lowest-free-slot allocation breaks challenge-labeling symmetry.
Issue(u, c) ≜
∧ chalState[c] = "none"
∧ ∀ x ∈ 1..(c - 1) : chalState[x] ≠ "none"
∧ chalOwner′ = [chalOwnerEXCEPT ![c] = u]
∧ chalIssued′ = [chalIssuedEXCEPT ![c] = clock]
∧ chalState′ = [chalStateEXCEPT ![c] = "fresh"]
∧ UNCHANGED ⟨clock, chalVerifiedAt, sessionChal⟩
Server-side verify: accepts only a fresh, same-owner, unexpired challenge; consumes it (single use) and creates the session. A new login replaces any existing session cookie for that user.
1. Every active session is backed by a consumed challenge that was issued to exactly that principal. No session without a completed ceremony, and never on someone else's challenge.
SessionBackedByOwnChallenge ≜
∀ u ∈ Users : sessionChal[u] ≠ NoChal ⇒
∧ chalState[sessionChal[u]] = "used"
∧ chalOwner[sessionChal[u]] = u
2. A challenge backs at most one session (single use across users; within a user a re-login replaces the session).
New companion module: the login UI moved from hosted auth pages to first-party pages calling the auth API directly, so the challenge/verify ceremony the client now drives is modeled explicitly (issuance, single-use TTL-bounded verify creating a session, logout). The registry module is unchanged; sessions and trust model are unchanged.
Ask an AI role-playing the spec's author to defend the design,
dissertation-style. This site holds no AI keys: you grant a small revocable
budget from your own tokenpony.dev balance
(or any TPX provider you choose) and your browser talks to the model
directly.