Skip to main content

push_authorization_request

Function push_authorization_request 

Source
pub async fn push_authorization_request(
    http: &Client,
    cache: &dyn ClientMetadataCache,
    store: &dyn ParStore,
    request: ParRequest,
    resolve: &(dyn Fn(&str) -> Option<String> + Send + Sync),
    seams: &DecisionSeams<'_>,
) -> Result<(String, Duration), ParError>
Expand description

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.

Returns the minted request_uri and its lifetime — for every well-formed push alike, whatever it goes on to answer. resolve turns login_hint into an account DID — the HTTP layer’s own didbot_pds::Registry lookup, passed in because this module holds no registry — and a hint that resolves to nobody gets the same 201, not a failure: see push_for_unresolved_hint (private, this module) for what it gets instead of a decision record, and why. This is the fix for this endpoint’s own account-enumeration hole — unauthenticated, and the earliest point a login_hint is even looked at — which a distinguishable refusal here reopened no matter how carefully worded, since a caller only has to compare it against what a resolvable hint gets, and nothing here ever refuses one of those. login_hint is mandatory here — RFC 9126 itself does not require one, but this profile addresses the decision record it mints to an account, and there is no other field naming which one; a missing hint is ParError::MissingParameter, the same as any other field this endpoint cannot proceed without, since every account alike would refuse the request regardless of which one asked.

A 403 must depend on server load, never on an account. A real account whose own pending_per_account bound is already full is not refused — that would resurrect the same oracle as a login_hint naming nobody, just aimed at an account that exists rather than one that does not (push enough candidate spellings and time which ones eventually answer 403). Its push is stored, and the account’s oldest undecided record makes way for it (crate::oauth::decision::DecisionStore::put). That record’s pushed request is taken back out of ParStore and its crate::oauth::consent::ConsentReference discarded from crate::oauth::consent::ConsentStore — both undone, or an unauthenticated caller could leak one live consent entry per push against any account it names, forever. An authorize for the displaced request then fails exactly as it would for a request_uri that expired. The only 403 this endpoint answers with is crate::oauth::decision::DecisionRefused::TooMany — the server itself out of room, true for every caller identically — and a displacement is logged at info, sampled, so an operator can still see a flood happening even though the caller cannot tell it apart from an ordinary push.

The decision is minted in the same call, and the two are stored together on the path that stores anything at all: if the record is refused by the server-wide bound the pushed request is taken straight back out, so a request_uri never exists without the decision that explains it — and neither does a displaced one.