Skip to main content

oauth_as/
par.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// Copyright (C) 2026 Matthew Jackson
3
4//! RFC 9126 pushed authorization requests (PAR) and RFC 9101 JWT-secured authorization requests
5//! (JAR). Compiled ONLY under the off-by-default `par` and `jar` cargo features; with both off
6//! this module does not exist and nothing else in the crate changes.
7//!
8//! # What these buy
9//!
10//! In a plain OAuth 2.1 authorization request every parameter travels through the user agent as
11//! query text: the browser, its extensions, its history, its `Referer` headers and any proxy in
12//! front of it can all read the request, and anything that can rewrite the URL can change it
13//! before this server ever sees it. The two mechanisms here close that in different ways, and a
14//! deployment may use either or both:
15//!
16//! - PAR (RFC 9126) moves the parameters off the browser entirely. The client POSTs them to a
17//!   back-channel endpoint, authenticating exactly as it would at the token endpoint, and receives
18//!   an opaque `request_uri` handle. Only the handle traverses the browser, and it is single use
19//!   and short lived, so an intermediary sees nothing and can substitute nothing.
20//! - JAR (RFC 9101) leaves the parameters in the browser but SIGNS them. An intermediary can still
21//!   read the request; it can no longer alter it without the signature failing.
22//!
23//! # The two rules that decide whether either is worth anything
24//!
25//! 1. SINGLE USE, enforced by storage. A `request_uri` is consumed with
26//!    [`crate::store::Storage::take_pushed_authorization_request`], the same atomic
27//!    remove-and-return primitive that makes authorization codes and refresh tokens single use.
28//!    RFC 9126 section 4 says a client MUST use a `request_uri` once and section 7.3 asks the
29//!    server to enforce it; a read-then-delete implementation of the trait method reintroduces the
30//!    replay under concurrency, which is why the trait says what it says.
31//! 2. THE ALGORITHM COMES FROM THE REGISTRATION, never from the token. A JOSE header is written by
32//!    whoever wrote the token, so trusting its `alg` is the classic JWS algorithm confusion attack
33//!    (RFC 8725 sections 3.1 and 3.2, which RFC 9101 section 6.2 requires be applied here). This
34//!    module compares the presented `alg` against the one registered for the client and refuses
35//!    anything else; `none` can never match, because [`RequestObjectAlg`] has no variant that
36//!    spells it and no constructor that could produce one.
37//!
38//! # What is deliberately NOT implemented
39//!
40//! - RFC 9101 section 5.2's fetched `request_uri` (the AS retrieving a request object over HTTPS
41//!   from a client-supplied URL). This library never makes outbound network calls, and RFC 9101
42//!   section 10.4.1 describes exactly why an AS that does is a DDoS amplifier. The only
43//!   `request_uri` values this server accepts are the ones it minted itself at its own PAR
44//!   endpoint (RFC 9126 section 2.2's URN form).
45//! - RFC 9101 section 6.1 encrypted (JWE) request objects. A five-part JWT is refused with
46//!   `invalid_request_object` rather than silently treated as unsigned.
47
48#[cfg(feature = "jar")]
49use base64::engine::general_purpose::URL_SAFE_NO_PAD;
50#[cfg(feature = "jar")]
51use base64::Engine as _;
52#[cfg(feature = "par")]
53use serde::{Deserialize, Serialize};
54
55use crate::authorization::{
56    AuthorizationError, AuthorizationRequest, ValidatedAuthorizationRequest,
57};
58use crate::client::ClientId;
59use crate::error::{ErrorCode, ErrorResponse};
60use crate::server::{AuthorizationServer, Clock};
61use crate::store::Storage;
62
63// --------------------------------------------------------------------------- RFC 9126 PAR
64
65/// The `pushed_at` a record written before 0.9.1 gets when it is read back.
66///
67/// The epoch, because it is the fail-closed answer: every barrier is recorded after it, so a
68/// record with no stated push instant is REFUSED by a standing revocation rather than admitted by
69/// one. See the field's own documentation.
70#[cfg(feature = "par")]
71fn pushed_at_default() -> std::time::SystemTime {
72    std::time::SystemTime::UNIX_EPOCH
73}
74
75/// The shortest handle lifetime this server will offer, whatever the host configured.
76///
77/// RFC 9126 section 2.2 makes `expires_in` a POSITIVE integer, and a sub-second
78/// [`ParConfig::request_uri_ttl`] reports zero — a handle a conforming client abandons without
79/// using. One second is the smallest value that can be reported truthfully. It is a clamp rather
80/// than a rejection for the reason [`crate::server::ServerConfig::user_code_length`] clamps: a
81/// misconfiguration must not become a runtime failure in front of a user.
82#[cfg(feature = "par")]
83pub const MIN_REQUEST_URI_TTL: std::time::Duration = std::time::Duration::from_secs(1);
84
85/// The URN prefix RFC 9126 section 2.2 offers for a minted `request_uri`, registered in its
86/// section 9.3. The RFC leaves the format to the server, so this is a choice rather than a
87/// requirement; it is the choice every interoperability profile in practice expects, and it names
88/// the value as a reference to server-side data rather than as something fetchable.
89#[cfg(feature = "par")]
90pub const REQUEST_URI_PREFIX: &str = "urn:ietf:params:oauth:request_uri:";
91
92/// How many random bytes go into a `request_uri`. RFC 9126 section 7.1 defers to RFC 9101 section
93/// 10.2 clause (d) on entropy: the handle is a capability URL, so guessing one is impersonating
94/// the client that pushed it. 32 bytes is 256 bits, the same draw as every other artifact this
95/// crate mints.
96#[cfg(feature = "par")]
97const REQUEST_URI_ENTROPY_BYTES: usize = 32;
98
99/// RFC 9126 configuration. `None` on [`crate::server::ServerConfig::par`] means PAR is OFF: no
100/// endpoint is advertised and [`AuthorizationServer::pushed_authorization_request`] refuses.
101#[cfg(feature = "par")]
102#[derive(Debug, Clone, PartialEq, Eq)]
103/// `#[non_exhaustive]`, for the reason [`crate::server::ServerConfig`] carries it, and stated
104/// plainly because this type is the exception to that finding rather than an instance of it: no
105/// field here is feature gated TODAY. It is a configuration struct hanging off `ServerConfig`, it
106/// is where every future RFC 9126 policy knob will land, and a host that learns the rule from the
107/// parent config is entitled to it from the child. Construct with [`ParConfig::new`] and override
108/// what the deployment needs.
109#[non_exhaustive]
110pub struct ParConfig {
111    /// RFC 9126 section 5 `pushed_authorization_request_endpoint`. `None` derives
112    /// `{issuer}/par`.
113    pub pushed_authorization_request_endpoint: Option<String>,
114    /// How long a minted `request_uri` stays usable.
115    ///
116    /// RFC 9126 section 2.2 leaves this to the server and gives 5 to 600 seconds as the typical
117    /// range. The default here is 60 seconds, which is a redirect round trip with room to spare:
118    /// the handle exists only to get the user agent from the client to this server's authorization
119    /// endpoint, and everything the handle protects (the `code_challenge` above all) is worth less
120    /// the shorter it is guessable for.
121    pub request_uri_ttl: std::time::Duration,
122    /// RFC 9126 section 5 `require_pushed_authorization_requests`. When true, this server refuses
123    /// any authorization request whose parameters arrived in the query
124    /// ([`AuthorizationServer::validate_authorization_request`] answers `invalid_request`), which
125    /// is the section 4 policy statement that PAR is the only way in.
126    pub require_pushed_authorization_requests: bool,
127}
128
129#[cfg(feature = "par")]
130impl Default for ParConfig {
131    fn default() -> Self {
132        ParConfig::new()
133    }
134}
135
136#[cfg(feature = "par")]
137impl ParConfig {
138    /// PAR offered, not required, with the 60 second handle lifetime described on
139    /// [`ParConfig::request_uri_ttl`].
140    pub fn new() -> Self {
141        ParConfig {
142            pushed_authorization_request_endpoint: None,
143            request_uri_ttl: std::time::Duration::from_secs(60),
144            require_pushed_authorization_requests: false,
145        }
146    }
147
148    /// The advertised endpoint for `issuer`.
149    pub fn endpoint(&self, issuer: &str) -> String {
150        match &self.pushed_authorization_request_endpoint {
151            Some(url) => url.clone(),
152            None => format!("{}/par", issuer.trim_end_matches('/')),
153        }
154    }
155}
156
157/// The RFC 9126 section 2.2 success response body. The endpoint answers `201 Created`, which the
158/// RFC states rather than suggests; see [`PushedAuthorizationResponse::http_status`].
159/// `Debug` is HAND-WRITTEN (below) and does not print the `request_uri`. The stored RECORD has
160/// been hand-redacted since it was written, for the reason stated there -- the handle is a
161/// capability for as long as it is live (RFC 9126 section 7.1) -- and this type, which hands that
162/// same handle to the client, was left deriving until 0.9.2. The handle carries a fully validated
163/// authorization request including its redirect URI; a leaked live one is redeemable.
164#[cfg(feature = "par")]
165#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct PushedAuthorizationResponse {
167    /// The single-use handle the client puts in its authorization request.
168    pub request_uri: String,
169    /// The handle's lifetime in seconds (a positive integer, per section 2.2).
170    pub expires_in: u64,
171}
172
173/// Hand-written so the `request_uri` never prints. `expires_in` prints in full: a lifetime is not
174/// a credential and is the diagnostic an operator is usually after.
175#[cfg(feature = "par")]
176impl std::fmt::Debug for PushedAuthorizationResponse {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        f.debug_struct("PushedAuthorizationResponse")
179            .field("request_uri", &"[redacted]")
180            .field("expires_in", &self.expires_in)
181            .finish()
182    }
183}
184
185#[cfg(feature = "par")]
186impl PushedAuthorizationResponse {
187    /// `201`. RFC 9126 section 2.2 says the server MUST generate the request URI and provide it
188    /// "with a 201 HTTP status code", so this is not the 200 the rest of this crate's endpoints
189    /// use and a host must not substitute one.
190    pub fn http_status(&self) -> u16 {
191        201
192    }
193}
194
195/// A pushed authorization request as the host persists it, keyed by `request_uri`.
196///
197/// The fields are the authorization request parameters this server understands, rather than an
198/// opaque bag of whatever was posted. That is deliberate: the endpoint validates the pushed
199/// request at push time (RFC 9126 section 2.1 step 3), so a parameter this server cannot act on
200/// cannot have been validated, and storing it would only let it reappear at the authorization
201/// endpoint unexamined. A parameter added to the authorization request is therefore added
202/// here as well, and the compiler says so; RFC 9396 `authorization_details`, which this
203/// comment used to name as the obvious next one, is now one of them.
204///
205/// `Debug` is hand-written for the same reason as [`crate::authorization::AuthorizationCodeRecord`]'s:
206/// the `request_uri` is a capability handle for as long as it is live (RFC 9126 section 7.1), so it
207/// must not reach a host's logs through `{:?}`.
208#[cfg(feature = "par")]
209#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
210/// `#[non_exhaustive]`: `rar` adds one field and `consent` adds two, and the doc above commits this
211/// record to gaining a field for every parameter the authorization request gains, which makes it
212/// the type in this crate most certain to keep changing shape. It round-trips through a
213/// `Storage` implementor by serde exactly as the token records do, so persisting it is unaffected;
214/// [`PushedAuthorizationRequest::new`] is the path for building one directly.
215#[non_exhaustive]
216pub struct PushedAuthorizationRequest {
217    /// The handle, and the storage key.
218    pub request_uri: String,
219    /// The client that pushed it. RFC 9126 section 2.2: the value MUST be bound to this client,
220    /// and section 7.5 is the attack that binding prevents.
221    pub client_id: ClientId,
222    /// `response_type`, as pushed.
223    pub response_type: Option<String>,
224    /// `redirect_uri`, as pushed.
225    pub redirect_uri: Option<String>,
226    /// `scope`, as pushed.
227    pub scope: Option<String>,
228    /// `state`, as pushed.
229    pub state: Option<String>,
230    /// `code_challenge`, as pushed (RFC 7636 section 4.3).
231    pub code_challenge: Option<String>,
232    /// `code_challenge_method`, as pushed.
233    pub code_challenge_method: Option<String>,
234    /// RFC 8707 `resource` indicators, as pushed, in wire order.
235    pub resource: Vec<String>,
236    /// RFC 9396 `authorization_details`, as pushed: the raw JSON array.
237    ///
238    /// Stored, and not merely validated at push time, because this record IS the request
239    /// the authorization endpoint later reads (section 2.1 step 3 validates it here, and
240    /// RFC 9101 section 6.3 says the endpoint MUST use only the pushed parameters). A
241    /// parameter validated at push time and then dropped would be a parameter the client
242    /// was told was acceptable and then silently did not get, which is exactly the drop
243    /// RFC 9396 section 5 exists to prevent.
244    #[cfg(feature = "rar")]
245    pub authorization_details: Option<String>,
246    /// RFC 9470 section 4 `acr_values`, as pushed.
247    #[cfg(feature = "consent")]
248    pub acr_values: Option<String>,
249    /// RFC 9470 section 4 `max_age`, as pushed: the raw seconds text, parsed by the same
250    /// validation the query path uses so a malformed value is refused HERE, at push time, which
251    /// is what RFC 9126 section 2.1 means by processing the request as if it had been sent
252    /// directly to the authorization endpoint.
253    ///
254    /// Stored for the reason `authorization_details` above is: this record IS the request the
255    /// authorization endpoint later reads. Dropping these two did not merely lose a preference,
256    /// it disabled RFC 9470 step-up for every PAR deployment, so a client answering an
257    /// `insufficient_user_authentication` challenge with `max_age=0` got a code minted against
258    /// the session it was told to replace.
259    #[cfg(feature = "consent")]
260    pub max_age: Option<String>,
261    /// The instant this request was PUSHED.
262    ///
263    /// A pushed request is not yet a grant, but it is a thing a client authored, and a
264    /// [`crate::store::RevocationBarrier::Client`] is compared against this so that a request
265    /// pushed before an RFC 7592 deletion is refused while one pushed by a re-provisioned client
266    /// is served. `expires_at` cannot stand in for it: the TTL is short but non-zero, so a request
267    /// pushed just before the deletion still has a deadline in the future and would be admitted.
268    ///
269    /// `#[serde(default)]`, and the default is the epoch, which is the FAIL-CLOSED direction.
270    /// This field is new in 0.9.1, so a record a 0.9.0 node wrote — or is still writing, during a
271    /// rolling upgrade — carries no such key, and without a default the read fails outright and
272    /// the endpoint answers `server_error`. With it, the record deserializes and dates from before
273    /// every barrier, so a standing revocation REFUSES it rather than admitting it. A far-future
274    /// default would deserialize just as happily and admit every one of them, which is the
275    /// resurrection this field exists to stop. There is deliberately NO backfill migration: a
276    /// backfill cannot reach a 0.9.0 node still writing field-less payloads during a rolling
277    /// upgrade, which is the window that matters, so the serde default covers strictly more than
278    /// one would.
279    #[serde(default = "pushed_at_default")]
280    pub pushed_at: std::time::SystemTime,
281    /// When the handle dies. RFC 9126 section 4: an expired `request_uri` MUST be rejected.
282    pub expires_at: std::time::SystemTime,
283}
284
285#[cfg(feature = "par")]
286impl std::fmt::Debug for PushedAuthorizationRequest {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        let mut out = f.debug_struct("PushedAuthorizationRequest");
289        // `pushed_at` prints for the reason `PushedAuthorizationRequest::new` spells out below: it
290        // is the instant a `crate::store::RevocationBarrier::Client` is compared against, a record
291        // left at the epoch is refused for as long as any client barrier stands, and the endpoint
292        // then blames a client deletion that never happened. That is "a silent per-client outage if
293        // nobody says so", and an operator diagnosing it reaches for `{:?}` first. It is not a
294        // credential; `request_uri` is the credential here and stays redacted.
295        out.field("request_uri", &"[redacted]")
296            .field("pushed_at", &self.pushed_at)
297            .field("client_id", &self.client_id)
298            .field("response_type", &self.response_type)
299            .field("redirect_uri", &self.redirect_uri)
300            .field("scope", &self.scope)
301            .field("state", &self.state)
302            .field("code_challenge", &self.code_challenge)
303            .field("code_challenge_method", &self.code_challenge_method)
304            .field("resource", &self.resource);
305        #[cfg(feature = "rar")]
306        out.field("authorization_details", &self.authorization_details);
307        #[cfg(feature = "consent")]
308        out.field("acr_values", &self.acr_values)
309            .field("max_age", &self.max_age);
310        out.field("expires_at", &self.expires_at).finish()
311    }
312}
313
314#[cfg(feature = "par")]
315impl PushedAuthorizationRequest {
316    /// The record with nothing pushed but three of the four things that are not authorization
317    /// parameters at all: the handle, the client RFC 9126 section 2.2 binds it to, and the
318    /// section 4 expiry that makes it die.
319    ///
320    /// THE FOURTH IS `pushed_at`, AND A CALLER USING THIS CONSTRUCTOR MUST SET IT. It is not an
321    /// argument here because it is not a parameter a client sends; it is the instant a
322    /// [`crate::store::RevocationBarrier::Client`] compares this record against, and this
323    /// constructor leaves it at the epoch, which predates every barrier that could ever be
324    /// recorded. A record left that way is REFUSED by `put_pushed_authorization_request` for as
325    /// long as any client barrier stands, and the endpoint answers "this client was deleted while
326    /// its request was being pushed" about a client that was not deleted. That is the fail-closed
327    /// direction and it is the right default, but it is a silent per-client outage if nobody says
328    /// so — which nothing did until the 0.9.1 audit, in the doc that is the manual for exactly
329    /// this.
330    ///
331    /// Every OTHER field is a pushed parameter, every one of them is legitimately absent from a
332    /// real request, and they are all public, so a caller assigns exactly what the client sent and
333    /// leaves the rest as the `None` that says the client sent nothing. Filling them from arguments
334    /// instead would mean a positional list of ten parameters, nine of them `Option` (`resource` is
335    /// a `Vec`), which is how a `redirect_uri` ends up in the `scope` slot.
336    pub fn new(
337        request_uri: impl Into<String>,
338        client_id: ClientId,
339        expires_at: std::time::SystemTime,
340    ) -> Self {
341        PushedAuthorizationRequest {
342            // FAIL-CLOSED, as the other hand-built records are: a request assembled without
343            // saying when it was pushed must not outrank a standing revocation.
344            pushed_at: std::time::SystemTime::UNIX_EPOCH,
345            request_uri: request_uri.into(),
346            client_id,
347            response_type: None,
348            redirect_uri: None,
349            scope: None,
350            state: None,
351            code_challenge: None,
352            code_challenge_method: None,
353            resource: Vec::new(),
354            #[cfg(feature = "rar")]
355            authorization_details: None,
356            #[cfg(feature = "consent")]
357            acr_values: None,
358            #[cfg(feature = "consent")]
359            max_age: None,
360            expires_at,
361        }
362    }
363
364    /// The stored parameters as an authorization request, borrowing rather than copying: this is
365    /// what the authorization endpoint validates, and it should cost no more than reading the
366    /// record already did.
367    pub fn as_request(&self) -> AuthorizationRequest<'_> {
368        AuthorizationRequest {
369            response_type: self.response_type.as_deref().map(Into::into),
370            client_id: Some(self.client_id.as_str().into()),
371            redirect_uri: self.redirect_uri.as_deref().map(Into::into),
372            scope: self.scope.as_deref().map(Into::into),
373            state: self.state.as_deref().map(Into::into),
374            code_challenge: self.code_challenge.as_deref().map(Into::into),
375            code_challenge_method: self.code_challenge_method.as_deref().map(Into::into),
376            resource: self.resource.iter().map(|r| r.as_str().into()).collect(),
377            #[cfg(feature = "rar")]
378            authorization_details: self.authorization_details.as_deref().map(Into::into),
379            // The FIELD is not feature gated (see `AuthorizationRequest`, which states why: a
380            // build without `rar` has to see the parameter in order to refuse it). This RECORD's
381            // member still is, because such a build never stores one: the push that carried it
382            // was refused before a handle existed.
383            #[cfg(not(feature = "rar"))]
384            authorization_details: None,
385            #[cfg(feature = "consent")]
386            acr_values: self.acr_values.as_deref().map(Into::into),
387            #[cfg(feature = "consent")]
388            max_age: self.max_age.as_deref().map(Into::into),
389        }
390    }
391}
392
393// --------------------------------------------------------------------------- RFC 9101 JAR
394
395/// The signing algorithms this server will verify a request object with, in the spelling RFC 8414
396/// / RFC 9101 section 4 `request_object_signing_alg_values_supported` uses.
397///
398/// ES256 and nothing else, for the reason `Cargo.toml` gives for the RFC 9068 signer: this crate
399/// carries one curve and no JOSE framework, and an algorithm list is a menu of things an attacker
400/// may ask for. `none` is absent and unreachable: see [`RequestObjectAlg`].
401#[cfg(feature = "jar")]
402pub const REQUEST_OBJECT_SIGNING_ALGS: &[&str] = &["ES256"];
403
404/// The RFC 9101 section 9.4.1 media type for a request object, used as the JOSE `typ` header
405/// parameter that RFC 9101 section 10.8 recommends for a new deployment.
406#[cfg(feature = "jar")]
407pub const REQUEST_OBJECT_TYP: &str = "oauth-authz-req+jwt";
408
409/// RFC 9101 configuration. `None` on [`crate::server::ServerConfig::jar`] means signed request
410/// objects are OFF: nothing is advertised and a `request` parameter is refused.
411#[cfg(feature = "jar")]
412#[derive(Debug, Clone, PartialEq, Eq)]
413/// `#[non_exhaustive]` on the same argument as [`ParConfig`] next door, and with the same
414/// admission: nothing here is feature gated today, and this is the config family being made
415/// uniform rather than a variance being contained. It grew a second field within one release of
416/// that note being written, which is the argument making itself. [`JarConfig::new`] and `Default`
417/// both give the accepted-not-required policy with the default lifetime ceiling.
418#[non_exhaustive]
419pub struct JarConfig {
420    /// RFC 9101 section 10.5 `require_signed_request_object`. When true, this server refuses any
421    /// authorization request that is not a signed request object, which is what stops an attacker
422    /// stripping the signature and falling back to a plain RFC 6749 request (the downgrade that
423    /// section names).
424    pub require_signed_request_object: bool,
425    /// The longest remaining life this server will honour on a request object, measured from now to
426    /// its `exp`. Default five minutes.
427    ///
428    /// A request object is a BEARER CREDENTIAL that travels in a browser query string, so it lands
429    /// in history, in `Referer`, and in every proxy log on the path. Anyone who reads one can
430    /// re-drive `/authorize` with it until it dies, which makes "when does it die" the only thing
431    /// standing between a captured URL and an indefinite replay.
432    ///
433    /// RFC 9101 does not set this. It does not require `exp` at all, and section 9.1 registers it
434    /// without a requirement level, so the lifetime is implementer discretion. What the RFC does
435    /// say, in section 10.2(d) about request object URIs, is that "a general guidance for the
436    /// validity time would be less than a minute", which is the spec's own view of how long one of
437    /// these should live. Five minutes is that guidance loosened to survive ordinary clock skew and
438    /// a user who is slow to land on the page, and it is a ceiling rather than a lifetime: an
439    /// object asking for less gets less.
440    ///
441    /// Set it larger if a deployment genuinely needs it, and know what is being bought with it.
442    pub max_request_object_lifetime: std::time::Duration,
443}
444
445#[cfg(feature = "jar")]
446impl JarConfig {
447    /// Signed request objects accepted, not required, with the default lifetime ceiling.
448    pub fn new() -> Self {
449        JarConfig::default()
450    }
451}
452
453/// Written out rather than derived, and the reason is the whole point of the field: a derived
454/// `Default` gives `Duration::ZERO` for the ceiling, and a zero ceiling refuses every request
455/// object ever presented. A default that silently turns the feature off would be discovered by a
456/// host at runtime, on a flow that used to work.
457#[cfg(feature = "jar")]
458impl Default for JarConfig {
459    fn default() -> Self {
460        JarConfig {
461            require_signed_request_object: false,
462            max_request_object_lifetime: std::time::Duration::from_secs(300),
463        }
464    }
465}
466
467/// A signature algorithm a client may register for its request objects.
468///
469/// A one-variant enum on purpose. The value that matters is the one that is NOT here: RFC 9101
470/// section 10.5 requires `alg: none` to be rejected, and the cheapest way to guarantee that is a
471/// type in which "none" cannot be spelled, so no configuration mistake and no future edit to a
472/// string comparison can reintroduce it.
473#[cfg(feature = "jar")]
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
475pub enum RequestObjectAlg {
476    /// ECDSA using P-256 and SHA-256 (RFC 7518 section 3.4).
477    Es256,
478}
479
480#[cfg(feature = "jar")]
481impl RequestObjectAlg {
482    /// The registered JOSE `alg` spelling.
483    pub fn as_str(self) -> &'static str {
484        match self {
485            RequestObjectAlg::Es256 => "ES256",
486        }
487    }
488}
489
490/// A public key was not usable as a request object verification key. Carries no key material.
491///
492/// The payload is sealed and read through [`RequestObjectKeyError::detail`], matching
493/// [`crate::token_exchange::UnknownTokenTypeIdentifier`]: both are one-payload rejections, both
494/// are readable, and neither can be forged by a caller.
495#[cfg(feature = "jar")]
496#[derive(Debug, Clone, PartialEq, Eq)]
497pub struct RequestObjectKeyError(String);
498
499#[cfg(feature = "jar")]
500impl RequestObjectKeyError {
501    /// Why the key was refused, as a sentence.
502    ///
503    /// A `&'static`-shaped description of the CONDITION, never any part of the key: the variants
504    /// this type is built from name a decoding or a width failure, and the type's own docs commit
505    /// to carrying no key material.
506    pub fn detail(&self) -> &str {
507        &self.0
508    }
509}
510
511#[cfg(feature = "jar")]
512impl std::fmt::Display for RequestObjectKeyError {
513    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514        write!(f, "request object key error: {}", self.0)
515    }
516}
517
518#[cfg(feature = "jar")]
519impl std::error::Error for RequestObjectKeyError {}
520
521/// What a client registered to sign its request objects with: one algorithm and one public key.
522///
523/// Both halves are the REGISTRATION's, never the token's. RFC 9101 section 6.2 requires the
524/// signature to be validated "using a key associated with the client and the algorithm specified
525/// in the `alg` Header Parameter", with RFC 8725 sections 3.1 and 3.2 applied, and the only
526/// reading of those together that is not an algorithm confusion attack is: take the algorithm the
527/// client registered, and refuse the token if its header names anything else.
528#[cfg(feature = "jar")]
529#[derive(Clone, PartialEq, Eq)]
530pub struct RegisteredRequestObjectKey {
531    alg: RequestObjectAlg,
532    kid: Option<String>,
533    /// The public key, in the ONE shape this crate's [`crate::jwt::Es256Verifier`] seam takes.
534    ///
535    /// It was an uncompressed SEC 1 point (`0x04 || x || y`) through 0.9.0, alongside a PRIVATE
536    /// second copy of ES256 verification in this file that knew how to read one. The seam collapsed
537    /// both into the single implementation behind `crate::jwt`, which is the point: two verifiers
538    /// behind two independent code paths is how a codebase ends up with an algorithm confusion bug
539    /// in whichever half nobody reviewed, and this crate has already had to unify `CLOCK_SKEW_LEEWAY`
540    /// and a hex digit table for the same reason.
541    ///
542    /// ONE CONSEQUENCE, stated because it is a real change rather than a refactor: the "is this
543    /// point actually on P-256" check no longer happens at REGISTRATION, because this crate no
544    /// longer contains an elliptic curve. It moved into the installed verifier, per request, where
545    /// [`crate::jwt::Es256Verifier`] states it as a MUST and names what it is for (an
546    /// invalid-curve attack is what a missing on-curve check buys). What the constructors below
547    /// still catch at registration time is every encoding mistake (a trimmed coordinate, a wrong
548    /// length, non-base64url), which is what a host actually gets wrong when it copies a JWK out
549    /// of its client table.
550    ///
551    /// WHO ESTABLISHES IT, precisely, because "it still fails closed" is a claim and claims in
552    /// this crate are meant to be checkable. For the built-in `jwt-p256` backend it is
553    /// established: `p256`'s `from_sec1_bytes` rejects a point that is not on the curve, so an
554    /// off-curve coordinate pair cannot verify anything. For a HOST verifier it is the host's
555    /// contract to meet and nothing in this crate checks it: [`crate::signer_conformance`] does
556    /// not currently present an off-curve key, so a green run there does not cover this clause.
557    /// A host whose backend hands raw coordinates to a library that skips point validation should
558    /// test that clause itself until the harness carries it.
559    key: crate::jwt::PublicJwk,
560}
561
562#[cfg(feature = "jar")]
563impl std::fmt::Debug for RegisteredRequestObjectKey {
564    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
565        f.debug_struct("RegisteredRequestObjectKey")
566            .field("alg", &self.alg)
567            .field("kid", &self.kid)
568            .finish()
569    }
570}
571
572#[cfg(feature = "jar")]
573impl RegisteredRequestObjectKey {
574    /// Register an ES256 key from the `x` and `y` members of a client's RFC 7517 JWK, exactly as
575    /// they appear there (base64url, unpadded, 32 bytes each).
576    pub fn es256_from_jwk_coordinates(
577        kid: Option<String>,
578        x: &str,
579        y: &str,
580    ) -> Result<Self, RequestObjectKeyError> {
581        let x = URL_SAFE_NO_PAD
582            .decode(x)
583            .map_err(|_| RequestObjectKeyError("x is not base64url".into()))?;
584        let y = URL_SAFE_NO_PAD
585            .decode(y)
586            .map_err(|_| RequestObjectKeyError("y is not base64url".into()))?;
587        // The width check is `public_jwk`'s, in ONE place, so that the two constructors cannot
588        // drift apart on what a coordinate is.
589        //
590        // Re-encoded from the DECODED bytes rather than passed through, so that the two
591        // constructors cannot disagree about what they accepted: whatever `x` and `y` spelled, what
592        // is stored is the canonical unpadded base64url of exactly 32 bytes.
593        Ok(RegisteredRequestObjectKey {
594            alg: RequestObjectAlg::Es256,
595            kid,
596            key: public_jwk(&x, &y)?,
597        })
598    }
599
600    /// Register an ES256 key from an uncompressed SEC 1 point (65 bytes, `0x04 || x || y`).
601    pub fn es256_from_sec1(
602        kid: Option<String>,
603        sec1: &[u8],
604    ) -> Result<Self, RequestObjectKeyError> {
605        if sec1.len() != 65 {
606            return Err(RequestObjectKeyError(
607                "an uncompressed P-256 point is exactly 65 bytes".into(),
608            ));
609        }
610        // The leading byte is checked because it is the one thing that says which SEC 1 encoding
611        // this is: 0x02 and 0x03 are the COMPRESSED forms, which are 33 bytes and so cannot arrive
612        // here, but 0x00 or 0x04 written by hand from a truncated buffer can. A host handing in 65
613        // bytes that do not start 0x04 has not handed in the point it thinks it has.
614        if sec1[0] != 0x04 {
615            return Err(RequestObjectKeyError(
616                "an uncompressed P-256 point begins with 0x04".into(),
617            ));
618        }
619        Ok(RegisteredRequestObjectKey {
620            alg: RequestObjectAlg::Es256,
621            kid,
622            key: public_jwk(&sec1[1..33], &sec1[33..])?,
623        })
624    }
625
626    /// The registered algorithm. This, not the token header, decides.
627    pub fn alg(&self) -> RequestObjectAlg {
628        self.alg
629    }
630
631    /// The registered `kid`, when the client named one.
632    pub fn kid(&self) -> Option<&str> {
633        self.kid.as_deref()
634    }
635}
636
637/// Where the request object verification keys come from: the host answers "what did this client
638/// register".
639///
640/// This is a SEAM rather than a field on [`crate::client::Client`], and the reason is stated
641/// plainly because it is a temporary one. RFC 7523 `private_key_jwt` client authentication needs
642/// the same thing (a public key per client), so client-registered key material belongs on the
643/// registration once that lands, and this trait should then be re-pointed at it rather than
644/// duplicated. Until then, a host installs one of these with
645/// [`AuthorizationServer::with_request_object_keys`] and answers from wherever it already keeps
646/// client keys.
647///
648/// With none installed, a `request` parameter is refused: a server that cannot check a signature
649/// must never treat the request as if it had checked one.
650#[cfg(feature = "jar")]
651pub trait RequestObjectKeys: Send + Sync {
652    /// The key `client_id` registered for signing request objects, or `None` if it registered
653    /// none (in which case it may not use JAR at all).
654    fn registered_key(&self, client_id: &ClientId) -> Option<RegisteredRequestObjectKey>;
655}
656
657/// The authorization request parameters carried as claims of a verified request object.
658///
659/// Only the parameters this server acts on are extracted. RFC 9101 section 4 lets a request object
660/// carry any extension parameter, and RFC 6749 section 3.1 requires unknown ones to be ignored,
661/// which is what not extracting them means here.
662#[cfg(feature = "jar")]
663#[derive(Debug)]
664struct RequestObjectClaims {
665    client_id: String,
666    response_type: Option<String>,
667    redirect_uri: Option<String>,
668    scope: Option<String>,
669    state: Option<String>,
670    code_challenge: Option<String>,
671    code_challenge_method: Option<String>,
672    resource: Vec<String>,
673    #[cfg(feature = "rar")]
674    authorization_details: Option<String>,
675    #[cfg(feature = "consent")]
676    acr_values: Option<String>,
677    #[cfg(feature = "consent")]
678    max_age: Option<String>,
679}
680
681#[cfg(feature = "jar")]
682impl RequestObjectClaims {
683    fn as_request(&self) -> AuthorizationRequest<'_> {
684        AuthorizationRequest {
685            response_type: self.response_type.as_deref().map(Into::into),
686            client_id: Some(self.client_id.as_str().into()),
687            redirect_uri: self.redirect_uri.as_deref().map(Into::into),
688            scope: self.scope.as_deref().map(Into::into),
689            state: self.state.as_deref().map(Into::into),
690            code_challenge: self.code_challenge.as_deref().map(Into::into),
691            code_challenge_method: self.code_challenge_method.as_deref().map(Into::into),
692            resource: self.resource.iter().map(|r| r.as_str().into()).collect(),
693            #[cfg(feature = "rar")]
694            authorization_details: self.authorization_details.as_deref().map(Into::into),
695            // The FIELD is not feature gated (see `AuthorizationRequest`, which states why: a
696            // build without `rar` has to see the parameter in order to refuse it). This CLAIM SET
697            // still is, and never carries one in such a build: `verified_request_object` refuses
698            // an object whose claims contain `authorization_details` before it builds this.
699            #[cfg(not(feature = "rar"))]
700            authorization_details: None,
701            #[cfg(feature = "consent")]
702            acr_values: self.acr_values.as_deref().map(Into::into),
703            #[cfg(feature = "consent")]
704            max_age: self.max_age.as_deref().map(Into::into),
705        }
706    }
707}
708
709/// One base64url (unpadded) segment of a JWS compact serialization.
710///
711/// `refusal` is the WHOLE description rather than the name of the segment, and it is
712/// `&'static str`, so the refusal borrows a constant instead of formatting one. There are exactly
713/// three call sites and three sentences (below), and this is the authorization endpoint: nothing
714/// has authenticated at this point, so the caller chooses how many of these it asks for.
715#[cfg(feature = "jar")]
716fn decode_segment(segment: &str, refusal: &'static str) -> Result<Vec<u8>, ErrorResponse> {
717    URL_SAFE_NO_PAD
718        .decode(segment)
719        .map_err(|_| ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(refusal))
720}
721
722/// The three refusals [`decode_segment`] can produce, spelled out so the call sites read as they
723/// did when the sentence was built from the segment's name.
724#[cfg(feature = "jar")]
725const HEADER_NOT_BASE64URL: &str = "the header is not base64url";
726#[cfg(feature = "jar")]
727const PAYLOAD_NOT_BASE64URL: &str = "the payload is not base64url";
728#[cfg(feature = "jar")]
729const SIGNATURE_NOT_BASE64URL: &str = "the signature is not base64url";
730
731/// One [`crate::jwt::PublicJwk`] from two 32-byte coordinates.
732///
733/// THE SEAM NOTE THAT USED TO BE HERE IS DISCHARGED. Through 0.9.0 this file carried its own
734/// private `verify_es256` over `p256::ecdsa::VerifyingKey`, a second copy of the same twenty lines
735/// that `src/jwt.rs` held for RFC 7523 client assertions and RFC 9449 DPoP proofs, with a comment
736/// promising it would become a call to them. It now is one: this function converts the registered
737/// key into the shape the [`crate::jwt::Es256Verifier`] seam takes, and the verification itself
738/// happens in the single implementation behind that trait.
739#[cfg(feature = "jar")]
740fn public_jwk(x: &[u8], y: &[u8]) -> Result<crate::jwt::PublicJwk, RequestObjectKeyError> {
741    // RFC 7518 section 6.2.1.2 fixes both coordinates at the curve's full byte length, so a
742    // trimmed leading zero is a different (and unusable) key rather than the same one.
743    if x.len() != 32 || y.len() != 32 {
744        return Err(RequestObjectKeyError(
745            "a P-256 coordinate is exactly 32 bytes".into(),
746        ));
747    }
748    crate::jwt::PublicJwk::from_coordinates(&URL_SAFE_NO_PAD.encode(x), &URL_SAFE_NO_PAD.encode(y))
749        .map_err(|_| RequestObjectKeyError("a P-256 coordinate is exactly 32 bytes".into()))
750}
751
752/// Read one claim that RFC 9101 section 4 requires to be a JSON string.
753#[cfg(feature = "jar")]
754fn string_claim(
755    claims: &serde_json::Map<String, serde_json::Value>,
756    name: &str,
757) -> Result<Option<String>, ErrorResponse> {
758    match claims.get(name) {
759        None => Ok(None),
760        Some(serde_json::Value::String(s)) => Ok(Some(s.clone())),
761        // Section 4: "Parameter names and string values MUST be included as JSON strings". A
762        // number or an object here is not a request parameter that could ever have been sent in a
763        // query string, so coercing it would be inventing a request the client did not make.
764        Some(_) => Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
765            .with_description(format!("the {name} claim must be a JSON string"))),
766    }
767}
768
769// ------------------------------------------------------------------- the server-side endpoints
770
771impl<S: Storage, C: Clock> AuthorizationServer<S, C> {
772    /// RFC 9126 section 2: the pushed authorization request endpoint.
773    ///
774    /// `parameters` is the form body exactly as it arrived, so that this can apply section 2.1
775    /// step 2 (a pushed request carrying `request_uri` is refused) and section 3 (a `request`
776    /// parameter is a signed request object) rather than making the host decide either. Parameters
777    /// this server does not know are ignored, per RFC 6749 section 3.1.
778    ///
779    /// The client authenticates exactly as at the token endpoint (section 2.1 step 1), and the
780    /// pushed request is fully validated here (step 3): client, exact-match redirect URI, PKCE
781    /// S256, scope inside the registration, RFC 8707 resource indicators. That is the whole point
782    /// of the back channel. A client learns its request is malformed from a JSON error it can read
783    /// rather than from a browser redirect it cannot.
784    ///
785    /// Errors follow RFC 9126 section 2.3: the token endpoint's RFC 6749 section 5.2 shape, with
786    /// `invalid_request` standing in for the authorization errors section 4.1.2.1 refuses to
787    /// redirect (a missing or mismatching redirect URI above all).
788    #[cfg(feature = "par")]
789    pub async fn pushed_authorization_request(
790        &self,
791        client_id: &ClientId,
792        client_secret: Option<&str>,
793        parameters: &[(&str, &str)],
794    ) -> Result<PushedAuthorizationResponse, ErrorResponse> {
795        self.pushed_authorization_request_with_credential(
796            client_id,
797            &crate::server::ClientCredential::secret(client_secret),
798            parameters,
799        )
800        .await
801    }
802
803    /// [`AuthorizationServer::pushed_authorization_request`] for a client presenting any credential
804    /// this server accepts at the token endpoint, not just a shared secret.
805    ///
806    /// RFC 9126 section 2.1 step 1 says the client authenticates here "in the same way as at the
807    /// token endpoint", so an RFC 7523 `private_key_jwt` client that the token endpoint accepts
808    /// must be accepted here too; a PAR endpoint that only understood `client_secret_basic` would
809    /// lock exactly the deployments that most want PAR (FAPI 2.0 requires both) out of it. This
810    /// mirrors `device_authorization_with_credential` and
811    /// `introspection_response_with_credential`, for the same reason and with the same shape.
812    #[cfg(feature = "par")]
813    pub async fn pushed_authorization_request_with_credential(
814        &self,
815        client_id: &ClientId,
816        credential: &crate::server::ClientCredential<'_>,
817        parameters: &[(&str, &str)],
818    ) -> Result<PushedAuthorizationResponse, ErrorResponse> {
819        // Read before authenticating: a server that is not offering PAR should say so whatever the
820        // credential was, and should not become a client-credential oracle for a feature it does
821        // not run.
822        if self.config().par.is_none() {
823            return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
824                .with_description("this server does not offer pushed authorization requests"));
825        }
826
827        // STAMPED BEFORE THE REGISTRATION IS READ, and that ordering is the whole point.
828        //
829        // `pushed_at` is what a [`crate::store::RevocationBarrier::Client`] is compared against,
830        // so it must date from BEFORE the read the write derives from. Taking it after
831        // `authenticate_client` — one `get_client` round trip, a secret verification, and on the
832        // RFC 7523 path a JWT verify plus a `claim_replay_id` write, followed by a SECOND
833        // `get_client` inside `validate_direct_authorization_request` — would date the push later
834        // than a `delete_client` landing in that window, so the comparison would ADMIT the write
835        // and mint a handle for a registration that no longer exists. The refusal below would
836        // then fire only for the sliver between building the record and taking the store lock,
837        // rather than for the window its own comment names.
838        //
839        // Same defect, same fix, as `client_credentials_token`: found by auditing the 0.9.1 fix
840        // that made barriers compare instants at all. Refusing on identity alone had closed it
841        // for free.
842        let pushed_at = self.now();
843
844        // 1. Client authentication, "in the same way as at the token endpoint" (section 2.1).
845        let client = self.authenticate_client(client_id, credential).await?;
846
847        // 2. Section 2.1: `request_uri` MUST NOT be provided here. Chaining one handle to another
848        //    would let a client (or an attacker who captured a handle) re-push somebody else's
849        //    request under its own identity.
850        if parameters.iter().any(|(name, _)| *name == "request_uri") {
851            return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
852                .with_description("request_uri must not be pushed (RFC 9126 s2.1)"));
853        }
854
855        // Section 3: the parameters may instead arrive inside a signed request object, in which
856        // case they are the ONLY source; anything alongside it in the form body is client
857        // authentication and nothing else.
858        #[cfg(feature = "jar")]
859        if let Some((_, object)) = parameters.iter().find(|(name, _)| *name == "request") {
860            let claims = self.verified_request_object(&client.client_id, object)?;
861            return self
862                .store_pushed_request(&client.client_id, &claims.as_request(), pushed_at)
863                .await;
864        }
865
866        // RFC 9101 SECTION 10.5, ON THE PUSHED PATH TOO. `require_signed_request_object` says this
867        // server will not act on an authorization request that is not signed, and a request pushed
868        // as plain form parameters is exactly that. Enforcing it only at the authorization endpoint
869        // would leave PAR as the door the policy does not cover, which is the same shape as the
870        // RFC 9126 gate this file just gained one level up: a policy that holds on one entry point
871        // and not the other is a policy the deployment does not have.
872        //
873        // Refused HERE rather than when the handle is redeemed, because a handle minted from an
874        // unsigned request is a handle that can never be spent, and answering that at push time
875        // tells the client which request was wrong while it still has it in hand.
876        #[cfg(feature = "jar")]
877        if matches!(&self.config().jar, Some(jar) if jar.require_signed_request_object) {
878            return Err(ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
879                "this server acts only on signed request objects (RFC 9101 s10.5), and this push \
880                 carried none",
881            ));
882        }
883
884        let request = AuthorizationRequest::from_pairs(parameters.iter().copied());
885        self.store_pushed_request(&client.client_id, &request, pushed_at)
886            .await
887    }
888
889    /// Validate a pushed request and mint its handle. Split out so that the form-body path and the
890    /// signed-request-object path cannot drift apart.
891    #[cfg(feature = "par")]
892    async fn store_pushed_request(
893        &self,
894        authenticated: &ClientId,
895        request: &AuthorizationRequest<'_>,
896        pushed_at: std::time::SystemTime,
897    ) -> Result<PushedAuthorizationResponse, ErrorResponse> {
898        // A client may push only its OWN request. RFC 9126 section 2.1 makes `client_id` a
899        // required parameter here with its ordinary meaning, and section 3 step 3 states the rule
900        // explicitly for the request-object form: the authenticated client and the request's
901        // client must be the same. Without this, an authenticated client could lodge a request
902        // that names a victim client and hand the handle to the browser.
903        match request.client_id.as_deref() {
904            Some(pushed) if pushed == authenticated.as_str() => {}
905            Some(_) => {
906                return Err(
907                    ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
908                        "client_id does not match the authenticated client (RFC 9126 s2.1)",
909                    ),
910                )
911            }
912            None => {
913                return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
914                    .with_description("client_id is required (RFC 9126 s2.1)"))
915            }
916        }
917
918        // Section 2.1 step 3: the same validation the authorization endpoint performs, reused
919        // rather than reimplemented. Two copies of this drift, and the copy that drifts is the one
920        // an attacker uses.
921        //
922        // The two-shape authorization error (RFC 6749 section 4.1.2.1) collapses to one JSON body
923        // here, because there is no user agent to redirect: section 2.3 says so and gives
924        // `invalid_request` as the default for the non-redirectable half.
925        self.validate_direct_authorization_request(request)
926            .await
927            .map_err(|e| match e {
928                AuthorizationError::Direct(error) => error,
929                AuthorizationError::Redirect(redirect) => redirect.error,
930            })?;
931
932        // CLAMPED, not trusted. `request_uri_ttl` is a plain public field with no validating
933        // constructor, and RFC 9126 section 2.2 says `expires_in` is a POSITIVE integer — which a
934        // sub-second `Duration` is not: it reports `0`, a handle a conforming client treats as
935        // already dead. `Duration::ZERO` is worse, because `expires_at` then equals `now` and the
936        // handle is refused on its first presentation while the push itself answered `201`. The
937        // host sees a successful push and a flow that cannot proceed, with nothing pointing at the
938        // TTL.
939        //
940        // Clamped rather than rejected, for the same reason `ServerConfig::user_code_length`
941        // clamps: a misconfiguration must not become a runtime failure at the one moment a user is
942        // standing in front of a device.
943        let ttl = match &self.config().par {
944            Some(par) => par.request_uri_ttl.max(MIN_REQUEST_URI_TTL),
945            // Unreachable through the public endpoint, which checks this first; answered rather
946            // than panicked because a library must not take a host's process down over its own
947            // configuration.
948            None => {
949                return Err(ErrorResponse::new(ErrorCode::InvalidRequest)
950                    .with_description("this server does not offer pushed authorization requests"))
951            }
952        };
953        let now = self.now();
954        // FALLIBLE, not `expect`. This draw is reachable from an ordinary PAR request, and every
955        // other fallible step on this path — the store write below, the client authentication
956        // above — answers `server_error` rather than taking the host's process down. A library
957        // that panics inside a host's request handler is worse than one that refuses: under
958        // `panic = "abort"` the whole server dies, and the diagnostic is a panic message rather
959        // than the host's own error channel. `getrandom` fails for reasons a deployment really
960        // meets — fd exhaustion, a seccomp filter without `getrandom(2)`, an uninitialised
961        // early-boot pool.
962        let request_uri = match crate::server::try_random_hex(REQUEST_URI_ENTROPY_BYTES) {
963            Some(hex) => format!("{REQUEST_URI_PREFIX}{hex}"),
964            None => return Err(ErrorResponse::new(ErrorCode::ServerError)),
965        };
966        let expires_at = crate::server::saturating_deadline(now, ttl);
967        let record = PushedAuthorizationRequest {
968            // Read at request ENTRY, above `authenticate_client`, not here. See the comment there:
969            // a barrier recorded between the two must refuse this write, and it can only do so if
970            // the instant predates the read the write is derived from. `expires_at` below is
971            // deliberately still measured from the write, because the TTL is a promise about how
972            // long the handle lives, not about when the client authored it.
973            pushed_at,
974            request_uri: request_uri.clone(),
975            client_id: authenticated.clone(),
976            response_type: request.response_type.as_deref().map(str::to_string),
977            redirect_uri: request.redirect_uri.as_deref().map(str::to_string),
978            scope: request.scope.as_deref().map(str::to_string),
979            state: request.state.as_deref().map(str::to_string),
980            code_challenge: request.code_challenge.as_deref().map(str::to_string),
981            code_challenge_method: request.code_challenge_method.as_deref().map(str::to_string),
982            resource: request.resource.iter().map(|r| r.to_string()).collect(),
983            #[cfg(feature = "rar")]
984            authorization_details: request.authorization_details.as_deref().map(str::to_string),
985            #[cfg(feature = "consent")]
986            acr_values: request.acr_values.as_deref().map(str::to_string),
987            #[cfg(feature = "consent")]
988            max_age: request.max_age.as_deref().map(str::to_string),
989            expires_at,
990        };
991        // A REFUSAL HERE IS NOT AN ERROR TO REPORT AS ONE. `authenticate_client` succeeded a
992        // moment ago, so reaching this line with a barrier in the way means the registration was
993        // deleted between that check and this write. The client genuinely no longer exists, and
994        // `invalid_client` is the truthful answer rather than the `server_error` a storage failure
995        // would deserve.
996        let stored = self
997            .store()
998            .put_pushed_authorization_request(record)
999            .await
1000            .map_err(|e| {
1001                let _ = e;
1002                ErrorResponse::new(ErrorCode::ServerError)
1003            })?;
1004        if stored.is_refused() {
1005            return Err(ErrorResponse::new(ErrorCode::InvalidClient)
1006                .with_description("this client was deleted while its request was being pushed"));
1007        }
1008        Ok(PushedAuthorizationResponse {
1009            request_uri,
1010            // Derived from the deadline actually recorded, not from the configured TTL. The two
1011            // can disagree: `saturating_deadline` clamps near the platform ceiling, and a client
1012            // told a lifetime longer than the record's would hold a handle it believes is live
1013            // after the store has stopped honouring it.
1014            expires_in: expires_at
1015                .duration_since(now)
1016                .unwrap_or(MIN_REQUEST_URI_TTL)
1017                .as_secs(),
1018        })
1019    }
1020
1021    /// The authorization endpoint for a request that arrived as `client_id` plus `request_uri`
1022    /// (RFC 9126 section 4).
1023    ///
1024    /// Every OTHER query parameter is ignored, and this signature is how that is enforced: they
1025    /// are not accepted, so there is no code path in which one of them could win. RFC 9101
1026    /// section 6.3, which RFC 9126 section 4 builds on, is explicit that the server MUST only use
1027    /// the parameters from the reference "even if the same parameter is provided in the query
1028    /// parameter". A client that duplicates `scope` in the query gets the pushed `scope`; an
1029    /// attacker who appends one gets the same.
1030    ///
1031    /// The handle is consumed atomically, so a second use of it fails however many requests are in
1032    /// flight (RFC 9126 section 4 and section 7.3).
1033    #[cfg(feature = "par")]
1034    pub async fn validate_pushed_authorization_request(
1035        &self,
1036        client_id: &str,
1037        request_uri: &str,
1038    ) -> Result<ValidatedAuthorizationRequest, AuthorizationError> {
1039        // Errors here are DIRECT, never a redirect: the redirect URI lives inside the record that
1040        // has not been read yet (or does not exist), and RFC 6749 section 4.1.2.1 forbids
1041        // redirecting to a URI the server has not validated. That is the whole reason the two
1042        // error shapes exist.
1043        // `&'static str`: every refusal below names a condition, never a value out of the
1044        // request, so the description is always a constant and never needs copying.
1045        let direct = |code: ErrorCode, why: &'static str| {
1046            AuthorizationError::Direct(ErrorResponse::new(code).with_description(why))
1047        };
1048
1049        let record = self
1050            .store()
1051            .take_pushed_authorization_request(request_uri)
1052            .await
1053            .map_err(|e| {
1054                let _ = e;
1055                AuthorizationError::Direct(ErrorResponse::new(ErrorCode::ServerError))
1056            })?
1057            .ok_or_else(|| {
1058                // Unknown, already used, or swept. One answer for all three: an attacker probing
1059                // handles learns nothing from the difference, and a client that reloaded its
1060                // browser learns the same thing either way.
1061                direct(
1062                    ErrorCode::InvalidRequestUri,
1063                    "unknown, expired or already used request_uri",
1064                )
1065            })?;
1066
1067        // RFC 9126 section 2.2: the handle is bound to the client that pushed it, and section 7.5
1068        // (request URI swapping) is the attack. The record goes BACK, exactly as a cross-client
1069        // authorization code does in `server.rs`: burning a live handle for a request that was
1070        // never entitled to it is a denial of service handed to whoever asks.
1071        if record.client_id.as_str() != client_id {
1072            // NOT fire-and-forget, and word for word the argument the authorization code path in
1073            // `server.rs` gives for the same situation. If this write fails, a LIVE pushed request
1074            // belonging to an honest client has just been destroyed by a stranger's request, and
1075            // answering `invalid_request_uri` would report that as the ordinary refusal it is not:
1076            // the honest client would arrive a moment later, be told `invalid_request_uri` as
1077            // well, and nobody would ever connect the two. `server_error` is the truthful answer
1078            // and it is the only place this failure can surface, because the party in front of us
1079            // is not the one who was harmed.
1080            //
1081            // It reveals nothing a probe can use: reaching this branch at all requires a real
1082            // handle, and the difference between the two answers is a store failure the caller
1083            // cannot provoke.
1084            // A REFUSAL IS THE RIGHT OUTCOME AND NOT AN ERROR. It means `delete_client` cascaded
1085            // this client away while the record was out of the store, so the handle SHOULD stay
1086            // gone: putting it back would resurrect a pushed request belonging to a registration
1087            // that no longer exists, which is the rule in `oauth_as::store`'s module docs. The
1088            // stranger in front of us is answered `invalid_request_uri` either way, so there is
1089            // nothing to report differently on the wire.
1090            let _restored = self
1091                .store()
1092                .put_pushed_authorization_request(record)
1093                .await
1094                .map_err(|e| {
1095                    let _ = e;
1096                    AuthorizationError::Direct(ErrorResponse::new(ErrorCode::ServerError))
1097                })?;
1098            return Err(direct(
1099                ErrorCode::InvalidRequestUri,
1100                "request_uri was not issued to this client",
1101            ));
1102        }
1103
1104        // Section 4: an expired request_uri MUST be rejected. Not put back: it can never become
1105        // valid again, so retaining it would only leave a guessable string in the store.
1106        if self.now() >= record.expires_at {
1107            return Err(direct(
1108                ErrorCode::InvalidRequestUri,
1109                "request_uri has expired",
1110            ));
1111        }
1112
1113        // Section 4 again: validated as any other authorization request would be. The pushed
1114        // request was validated at push time too, and doing it twice is the answer section 7.4
1115        // asks for, since the client's policy may have changed in between (a redirect URI removed,
1116        // a scope withdrawn, the client deleted outright).
1117        self.validate_direct_authorization_request(&record.as_request())
1118            .await
1119    }
1120
1121    /// The authorization endpoint for a request that arrived as `client_id` plus a signed
1122    /// `request` object (RFC 9101 sections 5.1 and 6).
1123    ///
1124    /// As with [`AuthorizationServer::validate_pushed_authorization_request`], the query
1125    /// parameters that a client may have duplicated alongside the object are not accepted here at
1126    /// all: RFC 9101 section 6.3 requires the server to use only the object's own parameters, and
1127    /// the surest way to honour that is to have no other parameters in hand.
1128    #[cfg(feature = "jar")]
1129    pub async fn validate_signed_authorization_request(
1130        &self,
1131        client_id: &str,
1132        request_object: &str,
1133    ) -> Result<ValidatedAuthorizationRequest, AuthorizationError> {
1134        // RFC 9126 SECTION 5, AND THE GATE THAT WAS NOT HERE.
1135        //
1136        // `require_pushed_authorization_requests` means this server "accepts authorization request
1137        // data only via PAR". The gate lived solely in `validate_authorization_request`, whose
1138        // comment explained that it is the query-parameter door and that `par.rs` reaches
1139        // validation directly "having already established that the request was pushed or signed".
1140        // That conflated two switches which buy different things:
1141        //
1142        //   `require_signed_request_object` buys INTEGRITY for a request that travels the browser.
1143        //   `require_pushed_authorization_requests` buys that the request NEVER TRAVELS THE BROWSER,
1144        //   and that it was lodged by an AUTHENTICATED client behind an atomically single-use,
1145        //   expiring handle.
1146        //
1147        // A signed request object has the first property and neither of the other two. So a
1148        // deployment that set the PAR flag, and also enabled JAR for a client, still accepted
1149        // authorization request data through the browser: it was refused at the plain-query door
1150        // and waved through this one. Refused here on the same terms as there.
1151        #[cfg(feature = "par")]
1152        if matches!(&self.config().par, Some(par) if par.require_pushed_authorization_requests) {
1153            return Err(AuthorizationError::Direct(
1154                ErrorResponse::new(ErrorCode::InvalidRequest).with_description(
1155                    "this server accepts authorization request data only via PAR (RFC 9126 s4)",
1156                ),
1157            ));
1158        }
1159        let claims = self
1160            .verified_request_object(&ClientId::new(client_id), request_object)
1161            .map_err(AuthorizationError::Direct)?;
1162        self.validate_direct_authorization_request(&claims.as_request())
1163            .await
1164    }
1165
1166    /// Verify a JWS-signed request object and extract its authorization request parameters
1167    /// (RFC 9101 section 6.2 and section 6.3).
1168    ///
1169    /// `client_id` is the client the request CLAIMS to be, taken from the authenticated client at
1170    /// the PAR endpoint or from the `client_id` query parameter at the authorization endpoint
1171    /// (RFC 9101 section 5 makes it REQUIRED there). It selects the verification key, and section
1172    /// 6.3 then requires the object's own `client_id` claim to be the same value: selecting the
1173    /// key by the claimed identity is safe precisely because the signature check that follows is
1174    /// what proves the identity.
1175    #[cfg(feature = "jar")]
1176    fn verified_request_object(
1177        &self,
1178        client_id: &ClientId,
1179        request_object: &str,
1180    ) -> Result<RequestObjectClaims, ErrorResponse> {
1181        if self.config().jar.is_none() {
1182            // RFC 9101 section 7 registers `request_not_supported` for exactly this.
1183            return Err(ErrorResponse::new(ErrorCode::RequestNotSupported)
1184                .with_description("this server does not accept signed request objects"));
1185        }
1186        let keys = self.hooks().request_object_keys().ok_or_else(|| {
1187            // A server with no key source cannot check a signature, and "cannot check" must never
1188            // read as "checked out".
1189            ErrorResponse::new(ErrorCode::InvalidRequestObject)
1190                .with_description("no request object verification keys are installed")
1191        })?;
1192        let registered = keys.registered_key(client_id).ok_or_else(|| {
1193            ErrorResponse::new(ErrorCode::InvalidRequestObject)
1194                .with_description("the client registered no request object key")
1195        })?;
1196        // The SAME refusal shape, one seam along: with no ES256 backend installed this server
1197        // cannot check the signature, and a server that cannot check a signature must never behave
1198        // as though it had checked one. Resolved before any segment is decoded, so an unverifiable
1199        // request object costs an unauthenticated caller nothing but the lookup.
1200        let verifier = self.es256_verifier().ok_or_else(|| {
1201            ErrorResponse::new(ErrorCode::InvalidRequestObject)
1202                .with_description("no ES256 verifier is installed")
1203        })?;
1204
1205        // RFC 7515 section 3.1 compact serialization: exactly three parts. A five-part token is a
1206        // JWE, which RFC 9101 section 6.1 defines and this server does not implement; refusing it
1207        // by shape is what stops it being read as an unsigned JWS.
1208        let mut parts = request_object.split('.');
1209        let (header_b64, payload_b64, signature_b64) =
1210            match (parts.next(), parts.next(), parts.next(), parts.next()) {
1211                (Some(h), Some(p), Some(s), None) => (h, p, s),
1212                _ => {
1213                    return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1214                        .with_description("not a three part JWS compact serialization"))
1215                }
1216            };
1217
1218        let header: serde_json::Value =
1219            serde_json::from_slice(&decode_segment(header_b64, HEADER_NOT_BASE64URL)?).map_err(
1220                |_| {
1221                    ErrorResponse::new(ErrorCode::InvalidRequestObject)
1222                        .with_description("the header is not JSON")
1223                },
1224            )?;
1225        let alg = header
1226            .get("alg")
1227            .and_then(serde_json::Value::as_str)
1228            .ok_or_else(|| {
1229                ErrorResponse::new(ErrorCode::InvalidRequestObject)
1230                    .with_description("the header has no alg")
1231            })?;
1232
1233        // RFC 7515 section 4.1.11 `crit`. Same rule as `CompactJws::reject_unknown_crit`, spelled
1234        // out here because this path parses the header by hand (it must read `alg` and `kid`
1235        // BEFORE choosing a verifier, so it cannot go through `CompactJws::parse` first). The two
1236        // must stay in agreement; if this path ever moves onto `CompactJws`, delete this and call
1237        // that.
1238        //
1239        // A JWS whose header names an extension the recipient does
1240        // not understand is INVALID, unconditionally: the point of the member is that the producer
1241        // is saying "this one changes the meaning, refuse me if you cannot process it". This
1242        // verifier implements NO extensions, so any `crit` at all is a refusal, and the empty
1243        // array is a refusal too because the section forbids it ("MUST NOT be used ... with an
1244        // empty list").
1245        //
1246        // Checked BEFORE `alg`, and before any signature work, for the same reason `alg` is checked
1247        // before the signature: a header that says the recipient cannot process this object is
1248        // answered without spending an ECDSA verification on it.
1249        match header.get("crit") {
1250            None => {}
1251            Some(serde_json::Value::Array(names)) => {
1252                return Err(
1253                    ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
1254                        if names.is_empty() {
1255                            "the header has an empty crit, which RFC 7515 s4.1.11 forbids"
1256                                .to_string()
1257                        } else {
1258                            "the header's crit names an extension this server does not implement"
1259                                .to_string()
1260                        },
1261                    ),
1262                )
1263            }
1264            Some(_) => {
1265                return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1266                    .with_description("the header's crit is not an array"))
1267            }
1268        }
1269
1270        // THE algorithm check. The registration decides; the header only gets to agree with it.
1271        // This is what makes `alg: none` and every other substitution (RFC 8725 sections 3.1 and
1272        // 3.2) a refusal rather than a verification path, and it is the reason `alg` is compared
1273        // BEFORE any signature work is attempted.
1274        if alg != registered.alg.as_str() {
1275            return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1276                .with_description("alg does not match the algorithm registered for this client"));
1277        }
1278
1279        // RFC 9101 section 6.2: "If a kid Header Parameter is present, the key identified MUST be
1280        // the key used and MUST be a key associated with the client."
1281        if let Some(presented) = header.get("kid").and_then(serde_json::Value::as_str) {
1282            match registered.kid() {
1283                Some(kid) if kid == presented => {}
1284                _ => {
1285                    return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1286                        .with_description(
1287                            "kid does not identify a key registered for this client",
1288                        ))
1289                }
1290            }
1291        }
1292
1293        // RFC 9101 section 10.8 (cross-JWT confusion), applying RFC 8725 section 3.11. A request
1294        // object is not the only JWT this issuer's clients hold, and a JWT minted for another
1295        // purpose must not be usable as an authorization request. `typ` is optional (requiring it
1296        // would break clients that predate the media type), but a `typ` that names something else
1297        // is a token that was made for something else.
1298        if let Some(typ) = header.get("typ").and_then(serde_json::Value::as_str) {
1299            if typ != REQUEST_OBJECT_TYP && typ != "JWT" && typ != "jwt" {
1300                return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1301                    .with_description("typ names a JWT that is not a request object"));
1302            }
1303        }
1304
1305        let signature = decode_segment(signature_b64, SIGNATURE_NOT_BASE64URL)?;
1306        // The JWS Signing Input is the ASCII of "header.payload" (RFC 7515 section 5.2 step 8),
1307        // taken from the ORIGINAL text rather than re-encoded: re-encoding would verify a
1308        // normalisation of the token instead of the token, which is how a signature check gets
1309        // decoupled from what it is supposed to be checking.
1310        let signing_input = &request_object.as_bytes()[..header_b64.len() + 1 + payload_b64.len()];
1311        if !verifier.verify(&registered.key, signing_input, &signature) {
1312            return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1313                .with_description("the signature did not verify"));
1314        }
1315
1316        // Only now is anything in the payload worth reading.
1317        let payload: serde_json::Value =
1318            serde_json::from_slice(&decode_segment(payload_b64, PAYLOAD_NOT_BASE64URL)?).map_err(
1319                |_| {
1320                    ErrorResponse::new(ErrorCode::InvalidRequestObject)
1321                        .with_description("the payload is not JSON")
1322                },
1323            )?;
1324        let claims = payload.as_object().ok_or_else(|| {
1325            ErrorResponse::new(ErrorCode::InvalidRequestObject)
1326                .with_description("the payload is not a JSON object")
1327        })?;
1328
1329        // RFC 9101 section 4: "request and request_uri parameters MUST NOT be included in Request
1330        // Objects". A nested reference would be a request that never terminates, and at the PAR
1331        // endpoint it would smuggle past the section 2.1 refusal of `request_uri`.
1332        for forbidden in ["request", "request_uri"] {
1333            if claims.contains_key(forbidden) {
1334                return Err(
1335                    ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
1336                        "a request object must not carry request or request_uri (RFC 9101 s4)",
1337                    ),
1338                );
1339            }
1340        }
1341
1342        // Section 6.3: the two client ids MUST be identical.
1343        match string_claim(claims, "client_id")? {
1344            Some(claimed) if claimed == client_id.as_str() => {}
1345            _ => return Err(
1346                ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
1347                    "the client_id claim does not match the request's client_id (RFC 9101 s6.3)",
1348                ),
1349            ),
1350        }
1351
1352        // RFC 9101 section 4 says a signed request object SHOULD carry `aud` naming this server's
1353        // issuer identifier. When it does, it is checked: an object addressed to a different
1354        // authorization server, replayed here by whoever intercepted it, is the mix-up that `aud`
1355        // exists to stop. When it does not, the object is still accepted, because SHOULD is not
1356        // MUST and refusing would break conforming clients.
1357        if let Some(aud) = payload.get("aud") {
1358            let issuer = self.issuer_identifier();
1359            let addressed_here = match aud {
1360                serde_json::Value::String(one) => one == issuer,
1361                serde_json::Value::Array(many) => many
1362                    .iter()
1363                    .any(|v| v.as_str().map(|s| s == issuer).unwrap_or(false)),
1364                _ => false,
1365            };
1366            if !addressed_here {
1367                return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1368                    .with_description("aud does not name this authorization server"));
1369            }
1370        }
1371
1372        // RFC 7519 section 4.1.4 / 4.1.5, AND THE THREE WAYS THIS USED TO FAIL OPEN.
1373        //
1374        // A request object is a bearer credential that travels in a browser query string, so it
1375        // lands in history, in `Referer` and in proxy logs. Its `exp` is the only thing between a
1376        // captured URL and an indefinite replay, and all three of the following let that check be
1377        // skipped rather than failed:
1378        //
1379        //   1. NO `exp` AT ALL. The old code honoured a lifetime "if one was carried", so an object
1380        //      without one authorized its exact request forever. RFC 9101 does not require `exp`
1381        //      and section 9.1 registers it with no requirement level, so this is implementer
1382        //      discretion rather than a rule to follow; the discretion is exercised the same way
1383        //      `client-assertion` already exercises it for a missing `jti`, and for the reason
1384        //      written there: an untrackable bearer credential is one anybody who saw the request
1385        //      can send again. Section 10.2(d)'s own guidance for a request object URI is "less
1386        //      than a minute", so a spec-shaped object is short lived by intent.
1387        //   2. A MALFORMED `exp`, which was WORSE than a missing one and is the reason this block
1388        //      was rewritten. The old code read the claim with `as_u64()` inside an `if let`, so a
1389        //      string, a fraction, a negative or exponent notation all produced `None` and the
1390        //      branch simply did not run. The client wrote an expiry, a reviewer reading the object
1391        //      sees an expiry, and the server ignored it. "We could not check this" must never read
1392        //      as "checked out".
1393        //   3. AN UNBOUNDED `exp`. An object may not name its own replay window: a year out is an
1394        //      immortal credential with a lifetime claim stapled to it. `max_request_object_lifetime`
1395        //      is the ceiling and the object gets the lesser of the two.
1396        //
1397        // The claim is a NumericDate per RFC 7519 section 2: "a JSON numeric value", and the
1398        // section says it "intentionally allows non-integer values". So 1.5 and 1.7e9 are LEGAL
1399        // spellings that must be honoured, and only a NON-NUMBER is malformed. The old code read
1400        // these with `as_u64`, which answers `None` for every legal non-integer spelling and every
1401        // illegal one alike, and then treated both as "the claim is absent".
1402        let numeric_date = |name: &str| -> Result<Option<f64>, ErrorResponse> {
1403            match payload.get(name) {
1404                None => Ok(None),
1405                Some(v) => v.as_f64().map(Some).ok_or_else(|| {
1406                    ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(format!(
1407                        "the request object's {name} is not a NumericDate (RFC 7519 s2)"
1408                    ))
1409                }),
1410            }
1411        };
1412        let now_secs = crate::server::unix_seconds(self.now()).ok_or_else(|| {
1413            ErrorResponse::new(ErrorCode::ServerError)
1414                .with_description("the server clock is outside the representable range")
1415        })? as f64;
1416        let exp = numeric_date("exp")?.ok_or_else(|| {
1417            ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
1418                "the request object has no exp, so it would authorize its request for as long as \
1419                 the client's key stays registered",
1420            )
1421        })?;
1422        if now_secs >= exp {
1423            return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1424                .with_description("the request object has expired"));
1425        }
1426        let ceiling = self
1427            .config()
1428            .jar
1429            .as_ref()
1430            .map(|j| j.max_request_object_lifetime)
1431            .unwrap_or_else(|| std::time::Duration::from_secs(300));
1432        if exp - now_secs > ceiling.as_secs() as f64 {
1433            return Err(
1434                ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
1435                    "the request object's remaining lifetime exceeds what this server accepts",
1436                ),
1437            );
1438        }
1439        // `nbf` reads the same way. It can only ever REFUSE an object that is otherwise fine, so a
1440        // malformed one failing closed costs a legitimate client nothing that sending a valid claim
1441        // would not have cost it. The leeway is the crate's single definition of clock skew rather
1442        // than a second one invented here, which is the drift `skew.rs` exists to have ended.
1443        if let Some(nbf) = numeric_date("nbf")? {
1444            if now_secs + crate::skew::CLOCK_SKEW_LEEWAY.as_secs() as f64 <= nbf {
1445                return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1446                    .with_description("the request object is not yet valid"));
1447            }
1448        }
1449
1450        // RFC 9396 section 5, in the build that supports NO authorization detail type at all: the
1451        // parameter is REFUSED rather than ignored. Without `rar` the claim below does not exist,
1452        // so the object's `authorization_details` would be dropped on the floor and the client
1453        // would receive a code, and then a token, that says nothing about the permission it asked
1454        // for and believes it obtained. Section 5 makes refusing that a MUST, and a REQUEST OBJECT
1455        // is the worst place to ignore it: the client SIGNED these parameters, and RFC 9101
1456        // section 6.3 requires this server to use the object's parameters and no others. Same
1457        // posture as `request_not_supported` above for an object this server will not process at
1458        // all: say so, rather than proceed as though the parameter had not been sent.
1459        #[cfg(not(feature = "rar"))]
1460        if claims.contains_key("authorization_details") {
1461            return Err(ErrorResponse::new(ErrorCode::InvalidAuthorizationDetails)
1462                .with_description("this server does not support authorization_details"));
1463        }
1464
1465        // RFC 8707 section 2 allows `resource` more than once, which in a JSON claim set is an
1466        // array; a single indicator stays a plain string.
1467        let resource = match claims.get("resource") {
1468            None => Vec::new(),
1469            Some(serde_json::Value::String(one)) => vec![one.clone()],
1470            Some(serde_json::Value::Array(many)) => {
1471                let mut out = Vec::with_capacity(many.len());
1472                for value in many {
1473                    match value.as_str() {
1474                        Some(s) => out.push(s.to_string()),
1475                        None => {
1476                            return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1477                                .with_description(
1478                                    "every resource claim entry must be a JSON string",
1479                                ))
1480                        }
1481                    }
1482                }
1483                out
1484            }
1485            Some(_) => {
1486                return Err(
1487                    ErrorResponse::new(ErrorCode::InvalidRequestObject).with_description(
1488                        "the resource claim must be a string or an array of strings",
1489                    ),
1490                )
1491            }
1492        };
1493
1494        Ok(RequestObjectClaims {
1495            client_id: client_id.as_str().to_string(),
1496            response_type: string_claim(claims, "response_type")?,
1497            redirect_uri: string_claim(claims, "redirect_uri")?,
1498            scope: string_claim(claims, "scope")?,
1499            state: string_claim(claims, "state")?,
1500            code_challenge: string_claim(claims, "code_challenge")?,
1501            code_challenge_method: string_claim(claims, "code_challenge_method")?,
1502            resource,
1503            // RFC 9396 s2 makes `authorization_details` a JSON ARRAY, and inside a request
1504            // object it stays one: RFC 9101 s4 requires request parameters to be JSON
1505            // strings but exempts values that are themselves JSON, and a client that had to
1506            // string-escape its array here would produce something no other endpoint
1507            // accepts. It is re-serialized to the compact text the rest of this crate
1508            // parses, so the request object and the query string reach exactly the same
1509            // validation rather than two nearly identical ones.
1510            #[cfg(feature = "rar")]
1511            authorization_details: match claims.get("authorization_details") {
1512                None => None,
1513                Some(value @ serde_json::Value::Array(_)) => Some(value.to_string()),
1514                Some(_) => {
1515                    return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1516                        .with_description("the authorization_details claim must be a JSON array"))
1517                }
1518            },
1519            // RFC 9470 s4, from INSIDE the signature. A client answering a step-up challenge puts
1520            // these in the request object (RFC 9101 s4), and s6.3 requires the server to use only
1521            // the object's parameters "even if the same parameter is provided in the query
1522            // parameter". Reading the query for these two instead meant trusting the one part of
1523            // a JAR request an intermediary can still rewrite, on a request whose entire purpose
1524            // is that it cannot be.
1525            #[cfg(feature = "consent")]
1526            acr_values: string_claim(claims, "acr_values")?,
1527            // `max_age` is the one exception to the JSON-string rule above, and OpenID Connect
1528            // Core section 6.1's own worked example is why: it shows `"max_age": 86400`, a JSON
1529            // NUMBER, in a request object. Refusing that would refuse the conforming client this
1530            // parameter exists for, so a non-negative integer is accepted and normalised to the
1531            // decimal text the rest of this crate parses. A fractional or negative number is not
1532            // "the number of seconds" (OpenID Connect Core s3.1.2.1) and is refused.
1533            #[cfg(feature = "consent")]
1534            max_age: match claims.get("max_age") {
1535                None => None,
1536                Some(serde_json::Value::String(s)) => Some(s.clone()),
1537                Some(serde_json::Value::Number(n)) => match n.as_u64() {
1538                    Some(secs) => Some(secs.to_string()),
1539                    None => {
1540                        return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1541                            .with_description(
1542                                "the max_age claim must be a non-negative number of seconds",
1543                            ))
1544                    }
1545                },
1546                Some(_) => {
1547                    return Err(ErrorResponse::new(ErrorCode::InvalidRequestObject)
1548                        .with_description("the max_age claim must be a JSON string or number"))
1549                }
1550            },
1551        })
1552    }
1553}
1554
1555#[cfg(test)]
1556#[path = "tests/par.rs"]
1557mod tests;