Expand description
Pushed authorization requests (RFC 9126), and where a sign-in is decided.
One seam, and this is it. PAR is the earliest point the client is
known — the first request that both names a client_id and can be
validated against fetched metadata — and it is where the decision record
an agent later answers is written. Two inputs reach that verdict and
nothing else does: plan/policy.md’s deny-only tree, asked about a
didbot_policy::Subject::Grant, and plan/scope-policy.md’s ceiling,
applied by intersection.
Client admission is one of those denials rather than a seam beside them.
plan/app-allowlist.md’s “refuse at the pushed authorization request” is
a denyClient-shaped policy in the tree, and it is asked about before
anything else here: didbot_policy::Subject::Grant names a client_id
and the requested scope but no account, so the gate’s answer cannot
depend on login_hint and does not wait for it. A client the gate
refuses outright gets no decision record and no request_uri — see
ParError::ClientRefused — the opposite of every other denial this
module writes, and deliberately so: the record would have to say so to
be useful, and saying so to an unauthenticated caller is exactly what
plan/app-allowlist.md keeps the operator’s to know.
The record is never judged again. GET /oauth/authorize and
bot.did.approveAuthorization read the record this module wrote — see
crate::oauth::decision — so a policy edited after a push never
changes an answer already given. It does bind the token that answer
would lead to: POST /oauth/token asks the gate again before issuing
one — see crate::oauth::token::GateAdmission.
login_hint naming nobody is not a third seam either — it is not a
seam at all. This endpoint is unauthenticated, and no observable
answer may depend on whether an account exists: an account this
deployment hosts, an account plan/policy.md’s gate would narrow or
deny, an account whose own pending bound is already full, and a
login_hint matching nobody here all get the same 201, or pushing one
request per candidate handle — or enough variously-spelled requests for
one candidate — is a directory of this deployment’s accounts. The one
403 this endpoint answers with is the server’s own pending_total
bound, which is identity-independent by construction: true for every
caller alike, so answering it truthfully leaks nothing. See
push_for_unresolved_hint (private, this module) for what a nonexistent
hint gets instead of a decision record, and why an account-keyed
bound on it — an earlier version of this fix — reopened the hole by a
different door.
An account at its own bound displaces its oldest undecided record.
login_hint is whatever the caller writes, so an account’s eight slots
are fillable by a stranger. Dropping the newest push let that stranger
keep an account from signing in to anything; displacing the oldest keeps
the bound and keeps the account’s freshest attempt answerable — including
when this server’s own pending_total is full, since a push that gives a
slot back before it takes one does not grow the store. See
crate::oauth::decision::DecisionStore::put.
Structs§
- Decision
Seams - The seams PAR reaches to work out what it would grant, bundled so
push_authorization_requesttakes one argument rather than seven. - Dropped
Pushes - How many pushes have not left an answerable decision record behind, this process’s lifetime, by reason.
- Memory
ParStore - The in-process PAR store.
- ParRequest
- The raw wire fields
POST /oauth/paraccepts, before client metadata is resolved. - Pushed
Request - One pushed authorization request, as pushed.
Enums§
- ParError
- Why a pushed request could not be accepted.
Constants§
- CEILING_
VERSION 🔒 - What a ceiling’s row names where a gate’s row names the policy version it judged under.
- DISPLACED_
PUSH_ 🔒LOG_ SAMPLE - How often
log_displaced_pushactually emits a log line, rather than only counting one. - MAX_
LOGIN_ HINT_ BYTES - The longest
login_hinta push may carry, in bytes. - MAX_
STATE_ BYTES - The longest
statea push may carry, in bytes. - REQUEST_
URI_ 🔒BYTES - Random bytes a
request_uriis drawn from, before encoding. - REQUEST_
URI_ 🔒PREFIX - The prefix RFC 9126 requires: “the URI … MUST include the
urn:ietf:params:oauth:request_uriprefix”. - REQUEST_
URI_ TTL - How long a pushed request stays redeemable. RFC 9126 recommends 60
seconds; this server gives a little more room for the agent-driven
browser
plan/oauth.mddescribes to loadauthorizebefore the request URI it just pushed goes stale. - S256_
CHALLENGE_ 🔒LENGTH - How long an S256
code_challengeis: the unpadded base64url encoding of a SHA-256 digest (RFC 7636 §4.2).
Statics§
- DISPLACED_
PUSH_ 🔒COUNT - How many undecided records have been displaced past an account’s own
pending bound, this process’s lifetime; see
DISPLACED_PUSH_LOG_SAMPLE. - SERVER_
FULL_ 🔒COUNT - How many pushes this server’s own
pending_totalrefused; seeParError::TooManyPending. - UNRESOLVED_
HINT_ 🔒COUNT - How many pushes named a
login_hintthis deployment resolved to no account; seepush_for_unresolved_hint.
Traits§
- ParStore
- Where pushed requests live between
POST /oauth/parandauthorize.
Functions§
- at_most 🔒
- Refuses
valueif it is longer thanmaxbytes. - decide 🔒
- The verdict for one request: what the ceiling narrows or denies. The
gate is not asked here —
push_authorization_requestalready asked it, once, before this account was even known to exist, becausedidbot_policy::Subject::Granthas no account for this function’sdidto change the answer of. A gate’s own refusal isParError::ClientRefused, not aVerdictthis function could return; only the ceiling’s per-account narrowing lives here. - dropped_
pushes DroppedPushesas it stands now.- is_
s256_ 🔒challenge - Whether
challengehas the shape of an S256 code challenge: exactlyS256_CHALLENGE_LENGTHcharacters of the base64url alphabet. - log_
displaced_ 🔒push - Logs a displaced record at
info, sampled to one inDISPLACED_PUSH_LOG_SAMPLEso the log line itself cannot be flooded. - origin_
of 🔒 - The scheme, host and port of a
client_id. - push_
authorization_ request - Handles one pushed authorization request end to end: resolves and validates the client’s metadata, checks the request against it, resolves the account it names, decides what it would grant, and stores both the request and the decision.
- push_
for_ 🔒unresolved_ hint - What
login_hintnaming nobody gets instead ofpush_authorization_request’s ordinary decision record: the identical success this endpoint hands back for a real account, and nothing an unauthenticated caller — or this store — can act on. - random_
request_ 🔒uri