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(®istered.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;