Skip to main content

oauth_resource_server/
jwks.rs

1//! The authorization server's signing keys: discovery, fetching, caching and
2//! per-key algorithm binding.
3//!
4//! Everything here fails closed — an unreachable IdP, a malformed key set, an
5//! unknown `kid` during the refetch cooldown, a key whose type cannot produce the
6//! token's `alg` all mean "no key", never "skip the check" — and a failed refresh
7//! keeps the keys already held, so an IdP outage does not revoke keys that are
8//! still good — until a refresh succeeds. A key the authorization server has
9//! withdrawn therefore stays trusted for as long as refreshes keep failing;
10//! there is deliberately no maximum staleness after which held keys are
11//! dropped, since that would turn an IdP outage into a full outage here too.
12//!
13//! Every refetch runs in a task of its own that holds the refresh lock until
14//! the fetch completes, so a caller that stops waiting (a client disconnect, a
15//! timeout layer) cannot cancel a fetch halfway and leave the unknown-`kid`
16//! cooldown spent with no keys loaded.
17
18use std::collections::HashSet;
19use std::sync::{Arc, PoisonError};
20use std::time::{Duration, Instant, SystemTime};
21
22use base64::Engine as _;
23use base64::engine::general_purpose::URL_SAFE_NO_PAD;
24use jsonwebtoken::DecodingKey;
25use jsonwebtoken::jwk::{AlgorithmParameters, Jwk, KeyOperations, PublicKeyUse};
26use serde_json::Value;
27use tokio::sync::{Mutex, OwnedMutexGuard, RwLock};
28use tracing::{Instrument, Span, debug, info, warn};
29
30use crate::algorithms::{Algorithm, key_algorithms, signing_algorithm};
31use crate::config::{KeyNamingBuf, ResolvedOAuthConfig};
32use crate::observe::record_field;
33use crate::token::{InvalidTokenKind, TokenRejection, describe_kid, for_log};
34use crate::validator::{
35    is_canonical_url, is_loopback_url, parsed_plain_http_non_loopback, plain_http_non_loopback,
36    url_is_loopback,
37};
38
39/// How long an unknown `kid` is allowed to trigger a JWKS refetch again.
40///
41/// An unknown `kid` is attacker-controllable — it is just a field in an unverified
42/// token header — so without this a stream of junk tokens would turn this server
43/// into an amplifier pointed at the identity provider. One refetch per minute is
44/// far faster than any real key rotation needs (JWKS rollovers publish the new key
45/// alongside the old one well before signing with it) and slow enough that the IdP
46/// never notices us.
47pub(crate) const JWKS_MIN_REFETCH_INTERVAL: Duration = Duration::from_secs(60);
48
49/// Ceiling on a single metadata or JWKS fetch, unless
50/// [`crate::OAuthValidatorBuilder::fetch_timeout`] sets another. Bounds how
51/// long a refresh holds `refresh_lock`, and therefore how long a stalled IdP
52/// can stall validation of a token whose key is not already cached.
53pub const DEFAULT_FETCH_TIMEOUT: Duration = Duration::from_secs(10);
54
55/// The shortest timeout [`crate::OAuthValidatorBuilder::fetch_timeout`]
56/// accepts. Zero would fail every fetch; under a second a TLS handshake to a
57/// distant authorization server fails intermittently.
58pub const MIN_FETCH_TIMEOUT: Duration = Duration::from_secs(1);
59
60/// The longest timeout [`crate::OAuthValidatorBuilder::fetch_timeout`]
61/// accepts. The timeout bounds how long a refresh holds the refresh lock, and
62/// every request whose key is not cached waits behind it — a discovery pass
63/// can chain three fetches — so a minute (the unknown-`kid` refetch interval)
64/// is the ceiling.
65pub const MAX_FETCH_TIMEOUT: Duration = Duration::from_secs(60);
66
67/// How often the background task re-reads the JWKS even when every `kid` is known.
68///
69/// The unknown-`kid` refetch picks up a NEW key; only a periodic re-read notices a
70/// key the authorization server has WITHDRAWN (rotated out after a compromise, for
71/// instance). Without it a retired key would stay trusted for the life of the
72/// process. An hour bounds that window — once a re-read succeeds — without
73/// being a load anyone would notice. After a FAILED pass the background task
74/// retries sooner (see [`background_retry_delay`]).
75pub(crate) const JWKS_BACKGROUND_REFRESH_INTERVAL: Duration = Duration::from_secs(3600);
76
77/// How long the background task waits after its `failures`-th consecutive
78/// failed pass: [`JWKS_MIN_REFETCH_INTERVAL`], doubling each time, capped at
79/// [`JWKS_BACKGROUND_REFRESH_INTERVAL`]. A validator whose first load failed
80/// (the IdP was down at boot) is then keyless for about a minute rather than
81/// an hour, without retrying a long outage more than hourly.
82pub(crate) fn background_retry_delay(failures: u32) -> Duration {
83    let doublings = failures.saturating_sub(1).min(16);
84    JWKS_MIN_REFETCH_INTERVAL
85        .saturating_mul(1 << doublings)
86        .min(JWKS_BACKGROUND_REFRESH_INTERVAL)
87}
88
89/// First retry after a failed background pass while NO key is held (the first
90/// load failed and none has succeeded since). One request every 5 s is nothing
91/// to an authorization server, and it is the floor: nothing here retries faster.
92pub(crate) const KEYLESS_RETRY_FLOOR: Duration = Duration::from_secs(5);
93
94/// Ceiling on the keyless retry delay. A keyless validator refuses every token,
95/// and a readiness probe on [`crate::OAuthValidator::is_ready`] keeps traffic —
96/// and with it every request-driven refetch — away from it, so this schedule
97/// is its only way back. Five minutes bounds how long it stays down after the
98/// authorization server recovers; at the cap it is 12 requests an hour.
99pub(crate) const KEYLESS_RETRY_CAP: Duration = Duration::from_secs(300);
100
101/// How long the background task waits after its `failures`-th consecutive
102/// failed pass while no key is held: [`KEYLESS_RETRY_FLOOR`], doubling each
103/// time, capped at [`KEYLESS_RETRY_CAP`] (5, 10, 20, 40, 80, 160, then 300 s).
104/// Once any key is held the task uses [`background_retry_delay`] instead.
105/// Timer-driven only — nothing a request carries can shorten it, so it is no
106/// amplification surface — and independent of the unknown-`kid` cooldown
107/// ([`JWKS_MIN_REFETCH_INTERVAL`]), which it neither shortens nor bypasses.
108pub(crate) fn keyless_retry_delay(failures: u32) -> Duration {
109    let doublings = failures.saturating_sub(1).min(16);
110    KEYLESS_RETRY_FLOOR
111        .saturating_mul(1 << doublings)
112        .min(KEYLESS_RETRY_CAP)
113}
114
115/// Cap on a metadata/JWKS response body. Real key sets are a few KiB; the cap is
116/// there so a misbehaving (or impersonated) endpoint cannot make this process
117/// buffer an unbounded body on the credential-checking path.
118pub(crate) const MAX_FETCH_BYTES: usize = 256 * 1024;
119
120/// Cap on keys taken from one JWK Set, for the same reason as [`MAX_FETCH_BYTES`]:
121/// every key is parsed and scanned on lookup, and no real AS publishes dozens.
122pub(crate) const MAX_JWKS_KEYS: usize = 64;
123
124/// A failed key refresh: discovery, fetch, or a key set with nothing usable in
125/// it. The keys held before the attempt are kept.
126///
127/// `Display` is the whole cause chain, outermost first, joined with `": "` (e.g.
128/// `fetching the JWKS from https://…: request failed: …`), so a log line needs no
129/// special formatting to show the root cause. [`RefreshError::kind`] is the
130/// coarse, matchable stage that failed.
131///
132/// # Security
133///
134/// A configured `issuer` or `jwks_uri` may carry a credential: userinfo
135/// (`https://user:pass@…`, which the fetch sends as HTTP Basic auth) or a
136/// query string (`…/jwks?key=…`). Every URL in the message is therefore
137/// redacted — userinfo becomes `***@`, a query `?***` and a fragment `#***`,
138/// while scheme, host, port and path stay, so the endpoint is still
139/// identifiable — and upstream errors are included without the URL they
140/// would otherwise repeat verbatim. The fetch itself uses the URL unchanged.
141/// The message still names the endpoints and repeats upstream error text,
142/// so it is for logs and operators: a public, unauthenticated endpoint (a
143/// health check reachable from outside, say) should report
144/// [`RefreshError::kind`] instead.
145#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
146#[error("{message}")]
147pub struct RefreshError {
148    kind: RefreshErrorKind,
149    message: String,
150}
151
152impl RefreshError {
153    fn new(kind: RefreshErrorKind, message: impl Into<String>) -> Self {
154        Self {
155            kind,
156            message: message.into(),
157        }
158    }
159
160    /// The same error, with `context` prepended to the message.
161    fn context(self, context: impl std::fmt::Display) -> Self {
162        Self {
163            kind: self.kind,
164            message: format!("{context}: {}", self.message),
165        }
166    }
167
168    /// Which stage of the refresh failed.
169    pub fn kind(&self) -> RefreshErrorKind {
170        self.kind
171    }
172}
173
174/// The stage at which a key refresh failed — see [`RefreshError::kind`].
175///
176/// `#[non_exhaustive]`: match with a wildcard arm; a stage may be added in a
177/// minor release.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179#[non_exhaustive]
180pub enum RefreshErrorKind {
181    /// No `jwks_uri` is configured and none could be discovered: every
182    /// metadata URL failed, answered for a different issuer, or named a
183    /// refused `jwks_uri`.
184    Discovery,
185    /// The JWKS request failed: network, TLS, a refused redirect, a timeout, a
186    /// non-success status, or a body over the size cap.
187    Fetch,
188    /// The JWKS response was not JSON, or not a JWK Set.
189    Parse,
190    /// The JWK Set held no key usable for a signature under the configured
191    /// algorithms.
192    NoUsableKeys,
193}
194
195impl RefreshErrorKind {
196    /// A short, stable, lower-case label (`discovery`, `fetch`, `parse`,
197    /// `no_usable_keys`), suitable for a metrics label or a public health
198    /// response.
199    pub fn as_str(self) -> &'static str {
200        match self {
201            Self::Discovery => "discovery",
202            Self::Fetch => "fetch",
203            Self::Parse => "parse",
204            Self::NoUsableKeys => "no_usable_keys",
205        }
206    }
207}
208
209impl std::fmt::Display for RefreshErrorKind {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        f.write_str(self.as_str())
212    }
213}
214
215/// A point-in-time view of the signing keys an [`crate::OAuthValidator`] holds,
216/// from [`crate::OAuthValidator::key_set_status`].
217///
218/// Reading it does no I/O and never waits on a refresh in flight, so a
219/// readiness probe, a status page or a metrics scrape can call it as often as
220/// it likes. `#[non_exhaustive]`: read its fields, never build one; a field
221/// may be added in a minor release.
222#[derive(Debug, Clone, Default, PartialEq, Eq)]
223#[non_exhaustive]
224pub struct KeySetStatus {
225    /// How many usable verification keys are held.
226    pub keys: usize,
227    /// The JWKS URL in use: the configured `jwks_uri`, or the one discovered
228    /// from the issuer's metadata — `None` while it is still undiscovered.
229    /// Redacted as [`RefreshError`]'s `# Security` note describes: any
230    /// userinfo shows as `***@` and any query as `?***`.
231    pub jwks_uri: Option<String>,
232    /// When the most recent refresh started, whether or not it has finished
233    /// and however it ended; `None` before the first one.
234    pub last_attempt: Option<SystemTime>,
235    /// When a refresh last succeeded (loaded a key set with at least one
236    /// usable key); `None` if none ever has. A failed refresh leaves it — and
237    /// the keys — as they were.
238    pub last_success: Option<SystemTime>,
239    /// Why the most recent *finished* refresh failed; `None` if it succeeded
240    /// or none has finished yet. Read [`RefreshError`]'s `# Security` note
241    /// before exposing its `Display` publicly.
242    pub last_error: Option<RefreshError>,
243}
244
245impl KeySetStatus {
246    /// At least one usable key is held — see
247    /// [`crate::OAuthValidator::is_ready`].
248    pub fn is_ready(&self) -> bool {
249        self.keys > 0
250    }
251}
252
253/// `raw` with any credential it may carry masked, for a log line, an error
254/// message, a `Debug` impl or [`KeySetStatus::jwks_uri`]: userinfo (user, or
255/// user and password) becomes `***@`, a query `?***` and a fragment `#***`.
256/// Scheme, host, port and path are kept, so the endpoint stays identifiable. A
257/// URL with none of the three comes back exactly as given; one that does not
258/// parse as a URL with a host comes back as a fixed placeholder, since there
259/// is then no telling where a credential in it might be. Never panics.
260pub(crate) fn redact_url(raw: &str) -> String {
261    try_redact_url(raw).unwrap_or_else(|| "<unparseable URL, redacted>".to_string())
262}
263
264/// The host of `raw` alone, for a span field: [`redact_url`]'s parse, then
265/// nothing but the host (no scheme, port, path, userinfo, query or
266/// fragment, so nothing a credential could sit in), or its placeholder.
267pub(crate) fn jwks_host(raw: &str) -> String {
268    try_redact_url(raw)
269        .and_then(|shown| {
270            reqwest::Url::parse(&shown)
271                .ok()?
272                .host_str()
273                .map(str::to_owned)
274        })
275        .unwrap_or_else(|| redact_url(""))
276}
277
278/// A URL for a `Debug` impl: blank stays as given (an unset field), anything
279/// else goes through [`redact_url`].
280pub(crate) fn debug_url(raw: &str) -> String {
281    if raw.trim().is_empty() {
282        raw.to_string()
283    } else {
284        redact_url(raw)
285    }
286}
287
288/// [`redact_url`], but `None` where it would return its placeholder — for a
289/// message that can say something more useful than the placeholder (that the
290/// value is not an absolute URL with a host) without echoing the value.
291pub(crate) fn try_redact_url(raw: &str) -> Option<String> {
292    // No host means no telling userinfo from path: `alice:s3cret@idp/jwks`
293    // parses as scheme `alice` with an opaque path.
294    let mut url = reqwest::Url::parse(raw).ok()?;
295    url.host_str()?;
296    // An `@` the parser did not read as the userinfo separator means the
297    // input is not the URL it looks like — `http://alice:1234/s3cret@host`
298    // is host `alice`, port `1234`, path `/s3cret@host` — and a credential
299    // may sit in what parsed as host, port or path. (One in the query or
300    // fragment is masked below anyway.)
301    if url.path().contains('@') {
302        return None;
303    }
304    let userinfo = !url.username().is_empty() || url.password().is_some();
305    if !userinfo && url.query().is_none() && url.fragment().is_none() {
306        return Some(raw.to_string());
307    }
308    if userinfo && (url.set_password(None).is_err() || url.set_username("***").is_err()) {
309        return None;
310    }
311    if url.query().is_some() {
312        url.set_query(Some("***"));
313    }
314    if url.fragment().is_some() {
315        url.set_fragment(Some("***"));
316    }
317    Some(url.to_string())
318}
319
320/// `err` and every `source()` below it, joined with `": "`.
321pub(crate) fn error_chain(err: &dyn std::error::Error) -> String {
322    let mut out = err.to_string();
323    let mut source = err.source();
324    while let Some(cause) = source {
325        out.push_str(": ");
326        out.push_str(&cause.to_string());
327        source = cause.source();
328    }
329    out
330}
331
332/// `context: <err and its causes>`.
333fn context(context: &str, err: &dyn std::error::Error) -> String {
334    format!("{context}: {}", error_chain(err))
335}
336
337/// Where a redirect may go, decided by [`judge_redirect`].
338#[derive(Debug, PartialEq, Eq)]
339enum Hop {
340    Follow,
341    /// Plain http to a non-loopback host, followed only because
342    /// `allow_insecure_http` is set — logged as a `warn`.
343    FollowInsecure,
344    Refuse(String),
345}
346
347/// Judge one redirect hop to `next`, after the `previous` URLs.
348///
349/// A hop from https to plain http is refused outright: it would let anyone on
350/// the path substitute the signing keys. A hop to plain http on a
351/// non-loopback host is held to the same `allow_insecure_http` rule as a
352/// configured URL (`opt_in_key` names that setting), so a loopback `jwks_uri`
353/// or issuer cannot redirect key fetches onto a cleartext network path
354/// without the opt-in. A fetch that STARTED on a loopback URL (`previous[0]`,
355/// the URL [`HttpClients::for_url`] chose the proxy-free
356/// [`HttpClients::loopback`] client for) may not leave loopback at all: that
357/// client has no proxy and resolves every name to loopback
358/// ([`LoopbackResolver`]), so a hop off loopback would either skip the
359/// explicit or environment proxy the operator set for every non-loopback
360/// fetch or connect somewhere it does not name. Some servers redirect their
361/// JWKS path (a trailing-slash rewrite, say); a handful of hops covers that,
362/// and an unbounded chain only stretches a refresh out.
363fn judge_redirect(
364    next: &reqwest::Url,
365    previous: &[reqwest::Url],
366    allow_insecure_http: bool,
367    opt_in_key: &str,
368) -> Hop {
369    if next.scheme() != "https" && previous.iter().any(|u| u.scheme() == "https") {
370        return Hop::Refuse("redirect from https to a non-https URL refused".to_string());
371    }
372    if previous.first().is_some_and(is_loopback_url) && !is_loopback_url(next) {
373        return Hop::Refuse(format!(
374            "redirect from a loopback URL to a non-loopback host ({}) refused — a fetch that \
375             starts on loopback stays on loopback",
376            for_log(&redact_url(next.as_str()))
377        ));
378    }
379    let insecure = parsed_plain_http_non_loopback(next);
380    if insecure && !allow_insecure_http {
381        return Hop::Refuse(format!(
382            "redirect to plain http on a non-loopback host ({}) refused — set {opt_in_key} \
383             to permit it",
384            for_log(&redact_url(next.as_str()))
385        ));
386    }
387    if previous.len() > 3 {
388        Hop::Refuse("too many redirects".to_string())
389    } else if insecure {
390        Hop::FollowInsecure
391    } else {
392        Hop::Follow
393    }
394}
395
396/// Hosts an explicit proxy is never used for: every loopback name and
397/// address ([`crate::validator::url_is_loopback`]'s set — `localhost`,
398/// `*.localhost`, `127.0.0.0/8`, `::1`). The URLs this crate fetches from a
399/// loopback host go through [`HttpClients::loopback`] anyway; this also
400/// covers a redirect hop to one inside [`HttpClients::normal`].
401pub(crate) const PROXY_BYPASS: &str = "localhost, 127.0.0.0/8, ::1";
402
403/// How the HTTP clients are built beyond the redirect policy: set by
404/// [`crate::OAuthValidatorBuilder`], defaulted by [`crate::OAuthValidator::new`].
405/// Holds `reqwest` types, so it never leaves the crate.
406pub(crate) struct FetchSettings {
407    /// Per-request timeout (connect through last body byte).
408    pub(crate) timeout: Duration,
409    /// Trust anchors ADDED to the TLS backend's own root set.
410    pub(crate) roots: Vec<reqwest::Certificate>,
411    /// An explicit proxy URL, already validated. `None` leaves reqwest's
412    /// own proxy behavior in place for non-loopback fetches.
413    pub(crate) proxy: Option<reqwest::Url>,
414}
415
416impl Default for FetchSettings {
417    fn default() -> Self {
418        Self {
419            timeout: DEFAULT_FETCH_TIMEOUT,
420            roots: Vec::new(),
421            proxy: None,
422        }
423    }
424}
425
426/// The two HTTP clients every metadata and JWKS fetch goes through, built
427/// from the same [`FetchSettings`] and redirect policy; [`HttpClients::for_url`]
428/// picks one per fetch from the URL being fetched.
429///
430/// A loopback URL names THIS host. Sent through a proxy it would name the
431/// proxy's host instead, and the plain-http loopback exemption (no
432/// `allow_insecure_http` needed) would carry the key fetch across the
433/// network in cleartext, where anyone on the path could substitute the keys.
434/// reqwest's own proxy handling (the `*_PROXY` environment variables, and on
435/// macOS and Windows the system settings when reqwest's `system-proxy`
436/// feature is on in the build) cannot be given extra exceptions, so rather
437/// than reimplementing it, loopback fetches use a second client with no
438/// proxy at all, and every other fetch keeps reqwest's behavior untouched.
439pub(crate) struct HttpClients {
440    /// Every non-loopback fetch. With no explicit proxy, a plain reqwest
441    /// client: environment and system proxies apply exactly as reqwest
442    /// applies them. With one, only that proxy (reqwest turns the
443    /// environment and system proxies off once a proxy is set), skipping
444    /// [`PROXY_BYPASS`].
445    pub(crate) normal: reqwest::Client,
446    /// Every fetch whose URL is loopback: `no_proxy()`, no proxy of any kind,
447    /// and [`LoopbackResolver`] in place of the system resolver, so a
448    /// `localhost`/`*.localhost` name reaches this host whatever the
449    /// resolver would have answered.
450    pub(crate) loopback: reqwest::Client,
451}
452
453impl HttpClients {
454    /// The client for a fetch of `url`: [`HttpClients::loopback`] when `url`
455    /// satisfies [`crate::validator::url_is_loopback`], otherwise
456    /// [`HttpClients::normal`].
457    ///
458    /// Chosen once per fetch, from its first URL; redirects are followed
459    /// inside the chosen client. A redirect from a loopback URL to a
460    /// non-loopback one is refused by [`judge_redirect`], so the proxy-free
461    /// `loopback` client never carries a hop off this host. A redirect from a
462    /// non-loopback URL to a loopback one stays in `normal`, and with an
463    /// environment or system proxy (never an explicit one, which skips
464    /// [`PROXY_BYPASS`]) that hop can go through the proxy, as it always
465    /// has. Such a hop is either https to https — TLS end to end through a
466    /// `CONNECT` tunnel, the certificate still checked — or plain http, which
467    /// [`judge_redirect`] follows only after an https-free chain and, off
468    /// loopback, only with `allow_insecure_http`.
469    pub(crate) fn for_url(&self, url: &str) -> &reqwest::Client {
470        if url_is_loopback(url) {
471            &self.loopback
472        } else {
473            &self.normal
474        }
475    }
476}
477
478/// Build [`HttpClients`]. Redirects are judged by [`judge_redirect`];
479/// `opt_in_key` names the `allow_insecure_http` setting in a refusal or
480/// warning.
481///
482/// Extra roots are added with `add_root_certificate`, which every TLS backend
483/// this crate offers treats as an addition: rustls puts them into the root
484/// store alongside the webpki and/or native roots its feature loads, and
485/// native-tls adds them to the platform store it keeps.
486pub(crate) fn http_clients(
487    allow_insecure_http: bool,
488    opt_in_key: &str,
489    settings: &FetchSettings,
490) -> Result<HttpClients, reqwest::Error> {
491    let mut normal = client_builder(allow_insecure_http, opt_in_key, settings);
492    if let Some(proxy) = &settings.proxy {
493        normal = normal.no_proxy().proxy(
494            reqwest::Proxy::all(proxy.clone())?
495                .no_proxy(reqwest::NoProxy::from_string(PROXY_BYPASS)),
496        );
497    }
498    Ok(HttpClients {
499        normal: normal.build()?,
500        loopback: client_builder(allow_insecure_http, opt_in_key, settings)
501            .no_proxy()
502            .dns_resolver(Arc::new(LoopbackResolver))
503            .build()?,
504    })
505}
506
507/// The [`HttpClients::loopback`] client's resolver: every name resolves to
508/// `[::1]` and `127.0.0.1`, never through DNS.
509///
510/// That client only ever fetches a URL [`url_is_loopback`] accepted — an IP
511/// literal in `127.0.0.0/8` or `::1`, which never reaches a resolver, or the
512/// NAME `localhost` or `*.localhost` — and [`judge_redirect`] keeps every
513/// redirect hop of such a fetch on loopback too. RFC 6761 §6.3 only says a
514/// resolver SHOULD answer `*.localhost` with loopback: glibc without
515/// nss-myhostname, musl and some container DNS servers forward it upstream,
516/// where anyone who controls that DNS could point it elsewhere and receive a
517/// cleartext, proxy-free key fetch the operator believed stayed on this
518/// host. Pinning the answer here makes the name-based exemption mean what it
519/// says. The port is the URL's: reqwest replaces the `0` given here with it
520/// (or the scheme's default).
521struct LoopbackResolver;
522
523impl reqwest::dns::Resolve for LoopbackResolver {
524    fn resolve(&self, _name: reqwest::dns::Name) -> reqwest::dns::Resolving {
525        let addrs: Vec<std::net::SocketAddr> = vec![
526            (std::net::Ipv6Addr::LOCALHOST, 0).into(),
527            (std::net::Ipv4Addr::LOCALHOST, 0).into(),
528        ];
529        Box::pin(std::future::ready(Ok(
530            Box::new(addrs.into_iter()) as reqwest::dns::Addrs
531        )))
532    }
533}
534
535/// A client builder with the timeout, extra roots and redirect policy both
536/// [`HttpClients`] share, and reqwest's default proxy behavior.
537fn client_builder(
538    allow_insecure_http: bool,
539    opt_in_key: &str,
540    settings: &FetchSettings,
541) -> reqwest::ClientBuilder {
542    let opt_in_key = opt_in_key.to_string();
543    let mut builder = reqwest::Client::builder().timeout(settings.timeout);
544    for root in &settings.roots {
545        builder = builder.add_root_certificate(root.clone());
546    }
547    builder.redirect(reqwest::redirect::Policy::custom(
548        move |attempt| match judge_redirect(
549            attempt.url(),
550            attempt.previous(),
551            allow_insecure_http,
552            &opt_in_key,
553        ) {
554            Hop::Follow => attempt.follow(),
555            Hop::FollowInsecure => {
556                warn!(
557                    url = %for_log(&redact_url(attempt.url().as_str())),
558                    "OAuth: following a redirect to plain http on a non-loopback host \
559                     ({opt_in_key} is set) — signing keys fetched over it can be \
560                     substituted by anyone on the path"
561                );
562                attempt.follow()
563            }
564            Hop::Refuse(reason) => attempt.error(reason),
565        },
566    ))
567}
568
569/// One usable verification key from the JWK Set, with the algorithms it may verify.
570///
571/// `algorithms` is the intersection of what the key's TYPE can produce, what its
572/// own `alg` parameter declares (when present) and the configured allowlist. A
573/// token's `alg` must be in it, which is what stops an attacker-chosen header from
574/// steering an RSA key into an ECDSA verification, or any key into HMAC.
575pub(crate) struct CachedKey {
576    kid: Option<String>,
577    key: DecodingKey,
578    pub(crate) algorithms: Vec<Algorithm>,
579    /// The JWK declared no `alg` and more than one allowlisted algorithm fits
580    /// its type — the RFC 8725 §3.1 deviation warned about when the key first
581    /// appears.
582    pub(crate) ambiguous: bool,
583}
584
585/// The in-memory JWKS plus when we last *attempted* to refresh it.
586///
587/// Attempt, not success, on purpose: a failing IdP must be backed off exactly like
588/// a successful-but-stale one, or an outage turns every junk token into a retry
589/// against a service that is already struggling.
590#[derive(Default)]
591struct JwksCache {
592    keys: Vec<CachedKey>,
593    last_attempt: Option<Instant>,
594}
595
596/// The fields behind [`JwksStore::status`].
597#[derive(Default)]
598struct Tracked {
599    /// What a status read copies. Its `jwks_uri` is [`redact_url`] of the
600    /// one below, so a credential in the URL never reaches a status page.
601    public: KeySetStatus,
602    /// The `jwks_uri` the fetch uses, unredacted: the configured one when
603    /// there is one (fixed for the life of the process — a new value means a
604    /// config change, which means a new validator); otherwise `None` until
605    /// discovery fills it in, and `None` again after a fetch of the
606    /// discovered one fails, so the next refresh re-reads the metadata (an
607    /// authorization server that moved its JWKS is followed without a
608    /// restart). The public copy keeps showing the last one discovered.
609    jwks_uri: Option<String>,
610    /// How many refreshes have started, stamped with `last_attempt`. Lets a
611    /// refresh task that died tell whether a newer attempt has run since.
612    attempts: u64,
613}
614
615impl Tracked {
616    fn set_jwks_uri(&mut self, uri: Option<String>) {
617        self.public.jwks_uri = uri.as_deref().map(redact_url);
618        self.jwks_uri = uri;
619    }
620}
621
622/// The key source behind [`crate::OAuthValidator`]: owns the JWKS cache and every
623/// fetch that fills it.
624pub(crate) struct JwksStore {
625    issuer: String,
626    /// The issuer's host alone ([`jwks_host`]): the `issuer_host` label of
627    /// the `metrics` feature's key-set metrics and the discovery span field.
628    /// Fixed by configuration, so its cardinality is the number of
629    /// validators, never anything a request controls.
630    issuer_host: String,
631    /// See [`crate::OAuthConfig::allow_insecure_http`]: whether a discovered
632    /// `jwks_uri` may be plain http on a non-loopback host.
633    allow_insecure_http: bool,
634    /// No `jwks_uri` is configured, so the one fetched comes from discovery
635    /// and is dropped after a failed fetch (see [`Tracked::jwks_uri`]).
636    discovers_jwks_uri: bool,
637    algorithms: Vec<Algorithm>,
638    naming: KeyNamingBuf,
639    http: HttpClients,
640    /// Only ever held for in-memory reads and swaps — never across a network
641    /// call. tokio's `RwLock` queues new readers behind a waiting writer, so a
642    /// writer parked on a slow IdP would stall every request, including ones whose
643    /// key is already cached.
644    jwks: RwLock<JwksCache>,
645    /// What [`JwksStore::status`] reports, plus the `jwks_uri` actually
646    /// fetched — see [`Tracked`].
647    ///
648    /// A synchronous `std` mutex, separate from `jwks`, so reading the status
649    /// is a plain function a probe can call from anywhere. It is locked only
650    /// to copy or overwrite these few fields — never across an `.await`, and
651    /// never while taking `jwks` or `refresh_lock` — so it can never wait on a
652    /// network call or on a refresh in flight. Reading `jwks` instead would
653    /// make the status async and queue it behind tokio's writer-preferring
654    /// lock, and `try_read` would fail spuriously whenever a swap was queued.
655    status: std::sync::Mutex<Tracked>,
656    /// Serializes refreshes instead: a burst of unknown-`kid` requests, or
657    /// the background refresher racing one, collapse into one fetch while
658    /// cached-key lookups carry on untouched. Owned guards, so the detached
659    /// task running a fetch holds it until the fetch is done, whoever was
660    /// waiting on it.
661    refresh_lock: Arc<Mutex<()>>,
662    /// Normally [`JWKS_MIN_REFETCH_INTERVAL`]; overridden only by tests, which
663    /// would otherwise have to sleep a minute to observe a refetch.
664    min_refetch_interval: Duration,
665}
666
667impl JwksStore {
668    /// A store holding `seed` (keys from
669    /// [`crate::OAuthValidatorBuilder::initial_jwks`], already parsed by
670    /// [`keys_from_jwk_set_json`]; empty otherwise).
671    ///
672    /// Seeded keys count toward [`KeySetStatus::keys`] (so the validator is
673    /// ready at once), but stamp neither `last_attempt` nor `last_success`:
674    /// no fetch has happened, so the first unknown `kid` may fetch at once and
675    /// `last_success` keeps meaning "the authorization server answered".
676    pub(crate) fn new(
677        config: &ResolvedOAuthConfig,
678        http: HttpClients,
679        min_refetch_interval: Duration,
680        seed: Vec<CachedKey>,
681    ) -> Self {
682        let mut tracked = Tracked::default();
683        let configured_uri = config.jwks_uri.clone().filter(|uri| !uri.trim().is_empty());
684        let discovers_jwks_uri = configured_uri.is_none();
685        tracked.set_jwks_uri(configured_uri);
686        tracked.public.keys = seed.len();
687        warn_about_new_ambiguous_keys(&[], &seed, &config.key_naming);
688        let issuer_host = jwks_host(&config.issuer);
689        crate::observe::set_keys(&issuer_host, seed.len());
690        Self {
691            issuer: config.issuer.clone(),
692            issuer_host,
693            allow_insecure_http: config.allow_insecure_http,
694            discovers_jwks_uri,
695            algorithms: config.algorithms.clone(),
696            naming: config.key_naming.clone(),
697            http,
698            jwks: RwLock::new(JwksCache {
699                keys: seed,
700                last_attempt: None,
701            }),
702            status: std::sync::Mutex::new(tracked),
703            refresh_lock: Arc::new(Mutex::new(())),
704            min_refetch_interval,
705        }
706    }
707
708    /// Load (or reload) the key set now, discovering the JWKS URI first if
709    /// needed. Returns how many usable keys it holds. On failure the previous
710    /// keys are kept.
711    pub(crate) async fn refresh_now(self: &Arc<Self>) -> Result<usize, RefreshError> {
712        let guard = Arc::clone(&self.refresh_lock).lock_owned().await;
713        self.refresh_detached(guard).await
714    }
715
716    /// The status fields, locked for the instant a caller copies or updates
717    /// them. Never hold the guard across an `.await`. A poisoned lock (a panic
718    /// mid-update of plain data) is still readable, so it is not propagated.
719    fn status_fields(&self) -> std::sync::MutexGuard<'_, Tracked> {
720        self.status.lock().unwrap_or_else(PoisonError::into_inner)
721    }
722
723    /// A copy of the key-set status. No I/O; never waits on `jwks` or
724    /// `refresh_lock`.
725    pub(crate) fn status(&self) -> KeySetStatus {
726        self.status_fields().public.clone()
727    }
728
729    /// Whether at least one usable key is held. As [`JwksStore::status`].
730    pub(crate) fn has_keys(&self) -> bool {
731        self.status_fields().public.keys > 0
732    }
733
734    /// Whether a refresh holds `refresh_lock` right now — for tests that
735    /// drive a paused clock and must know when a fetch has finished.
736    #[cfg(test)]
737    pub(crate) fn refresh_in_flight(&self) -> bool {
738        self.refresh_lock.try_lock().is_err()
739    }
740
741    /// Run one refresh in a task of its own, which holds `guard` (the refresh
742    /// lock) until the fetch completes, and wait for it.
743    ///
744    /// Detached so the fetch cannot be cancelled by its caller being dropped —
745    /// a client disconnecting mid-request, a timeout layer, an HTTP/2 reset.
746    /// Run inline, a drop after [`JwksStore::refresh`] stamps `last_attempt`
747    /// would spend the unknown-`kid` cooldown without loading any keys, and an
748    /// unauthenticated client could repeat that every minute to keep a rotated
749    /// key from ever being picked up on demand.
750    async fn refresh_detached(
751        self: &Arc<Self>,
752        guard: OwnedMutexGuard<()>,
753    ) -> Result<usize, RefreshError> {
754        // Read while `guard` is held, so no other refresh can start first:
755        // this task's own attempt, if it got as far as stamping one, is the
756        // next number.
757        let ours = self.status_fields().attempts + 1;
758        let store = Arc::clone(self);
759        // `in_current_span`: the refresh's span is a child of the caller's
760        // (a request's `oauth_rs.validate`), so a trace shows what the request
761        // waited on. Tracing only; the task is detached exactly as before.
762        let task = tokio::spawn(
763            async move {
764                let _refreshing = guard;
765                store.refresh().await
766            }
767            .in_current_span(),
768        );
769        task.await.unwrap_or_else(|e| {
770            let err = RefreshError::new(
771                RefreshErrorKind::Fetch,
772                format!("the key refresh task did not finish: {e}"),
773            );
774            // A cancelled task means the runtime is shutting down: nothing
775            // failed, and nobody is left to read the status. A panicked one
776            // released the refresh lock while unwinding, so a newer attempt
777            // may already have run — and succeeded; record the panic only if
778            // none has started since.
779            if e.is_panic() {
780                let mut status = self.status_fields();
781                if status.attempts <= ours {
782                    status.public.last_error = Some(err.clone());
783                }
784            }
785            Err(err)
786        })
787    }
788
789    /// The verification key for `kid` and `alg` among the keys already held, or
790    /// `None`. Never fetches and never waits on `refresh_lock`.
791    pub(crate) async fn cached_decoding_key(
792        &self,
793        kid: Option<&str>,
794        alg: Algorithm,
795    ) -> Option<DecodingKey> {
796        lookup(&self.jwks.read().await.keys, kid, alg)
797    }
798
799    /// Resolve the verification key for `kid` and `alg`, fetching or refetching the
800    /// JWKS as needed.
801    ///
802    /// Fails closed in every failure mode — an unreachable IdP, a malformed key set,
803    /// an unknown `kid` during the refetch cooldown, a key whose type cannot produce
804    /// `alg` — because the alternative shape ("could not check, so allow") is the
805    /// one bug in this crate that would be worth a CVE.
806    pub(crate) async fn decoding_key(
807        self: &Arc<Self>,
808        kid: Option<&str>,
809        alg: Algorithm,
810    ) -> Result<DecodingKey, TokenRejection> {
811        if let Some(key) = lookup(&self.jwks.read().await.keys, kid, alg) {
812            return Ok(key);
813        }
814
815        // One refresher at a time. A thundering herd of concurrent unknown-`kid`
816        // requests queues HERE, not on the key lock, so requests whose key is
817        // already cached are never held up by a slow IdP; the fetch timeout
818        // (`DEFAULT_FETCH_TIMEOUT` unless the builder set one) bounds how long
819        // the queued ones wait.
820        let refreshing = Arc::clone(&self.refresh_lock).lock_owned().await;
821
822        // Another task may have fetched while we waited.
823        let last_attempt = {
824            let cache = self.jwks.read().await;
825            if let Some(key) = lookup(&cache.keys, kid, alg) {
826                return Ok(key);
827            }
828            cache.last_attempt
829        };
830
831        if let Some(last) = last_attempt
832            && last.elapsed() < self.min_refetch_interval
833        {
834            // See `JWKS_MIN_REFETCH_INTERVAL`: `kid` comes from an unverified token
835            // header, so an unknown one must not be able to schedule IdP traffic.
836            // When the last attempt failed, or no key is held at all, the key
837            // is missing because the authorization server is unreachable, not
838            // because the token names a key it never published: that is an
839            // outage (`KeySetUnavailable`), which an operator alerts on.
840            let (outage, detail_suffix) = {
841                let status = self.status_fields();
842                if status.public.last_error.is_some() {
843                    (true, " (the last JWKS refresh failed)")
844                } else if status.public.keys == 0 {
845                    (true, " (no signing key is held)")
846                } else {
847                    (false, "")
848                }
849            };
850            return Err(TokenRejection::invalid(
851                if outage {
852                    InvalidTokenKind::KeySetUnavailable
853                } else {
854                    InvalidTokenKind::KeyNotFound
855                },
856                format!(
857                    "no {alg} key for kid {} and the JWKS was refetched less than {}s \
858                     ago{detail_suffix}",
859                    describe_kid(kid),
860                    self.min_refetch_interval.as_secs()
861                ),
862            ));
863        }
864
865        if let Err(e) = self.refresh_detached(refreshing).await {
866            warn!(
867                issuer = %redact_url(&self.issuer),
868                error = %e,
869                "JWKS refresh failed — tokens signed by a key we do not already hold will \
870                 be rejected until the next attempt"
871            );
872            return Err(TokenRejection::invalid(
873                InvalidTokenKind::KeySetUnavailable,
874                format!("JWKS refresh failed: {e}"),
875            ));
876        }
877
878        lookup(&self.jwks.read().await.keys, kid, alg).ok_or_else(|| {
879            TokenRejection::invalid(
880                InvalidTokenKind::KeyNotFound,
881                format!(
882                    "no {alg} key for kid {} in the fetched JWKS",
883                    describe_kid(kid)
884                ),
885            )
886        })
887    }
888
889    /// One refresh attempt. The caller holds `refresh_lock`; the key lock is taken
890    /// only for the instant it takes to read or swap in-memory state, never across
891    /// the network. Records the attempt time first, so a failure is backed off like
892    /// a success, and leaves the old keys in place on any failure. The outcome is
893    /// recorded in the status fields once it is known.
894    ///
895    /// Runs in an `info` span `oauth_rs.jwks_refresh` (target
896    /// `oauth_resource_server::jwks`) with `jwks.host` (the host alone, from
897    /// [`jwks_host`]), `result` (`success` or the [`RefreshErrorKind`] label)
898    /// and `keys` (held afterwards); feature `metrics` counts it in
899    /// `oauth_rs_jwks_refresh_total` and sets `oauth_rs_jwks_keys`.
900    async fn refresh(&self) -> Result<usize, RefreshError> {
901        let span = tracing::info_span!(
902            "oauth_rs.jwks_refresh",
903            jwks.host = tracing::field::Empty,
904            result = tracing::field::Empty,
905            keys = tracing::field::Empty,
906        );
907        let result = self.refresh_in(&span).instrument(span.clone()).await;
908        let label = match &result {
909            Ok(_) => "success",
910            Err(e) => e.kind().as_str(),
911        };
912        let held = self.status_fields().public.keys;
913        record_field(&span, "result", label);
914        record_field(&span, "keys", held);
915        crate::observe::count_refresh(&self.issuer_host, label, held);
916        result
917    }
918
919    /// The body of [`JwksStore::refresh`].
920    async fn refresh_in(&self, span: &Span) -> Result<usize, RefreshError> {
921        self.jwks.write().await.last_attempt = Some(Instant::now());
922        let known_uri = {
923            let mut status = self.status_fields();
924            status.attempts += 1;
925            status.public.last_attempt = Some(SystemTime::now());
926            status.jwks_uri.clone()
927        };
928        if !span.is_disabled()
929            && let Some(uri) = known_uri.as_deref()
930        {
931            record_field(span, "jwks.host", jwks_host(uri).as_str());
932        }
933        let result = self.load(known_uri, span).await;
934        let status = &mut self.status_fields().public;
935        match &result {
936            Ok(count) => {
937                status.keys = *count;
938                status.last_success = Some(SystemTime::now());
939                status.last_error = None;
940            }
941            Err(e) => status.last_error = Some(e.clone()),
942        }
943        result
944    }
945
946    /// Discover the JWKS URI if `known_uri` is `None`, then fetch the key set
947    /// and swap it in: the body of [`JwksStore::refresh`].
948    async fn load(&self, known_uri: Option<String>, span: &Span) -> Result<usize, RefreshError> {
949        let jwks_uri = match known_uri {
950            Some(uri) => uri,
951            None => {
952                let uri = self.discover_jwks_uri().await?;
953                if !span.is_disabled() {
954                    record_field(span, "jwks.host", jwks_host(&uri).as_str());
955                }
956                info!(
957                    issuer = %redact_url(&self.issuer),
958                    jwks_uri = %redact_url(&uri),
959                    "OAuth: discovered the JWKS URI from the issuer's metadata"
960                );
961                if plain_http_non_loopback(&uri) {
962                    // Reachable only with the opt-in: `jwks_uri_from_metadata`
963                    // refuses this without it.
964                    warn!(
965                        jwks_uri = %redact_url(&uri),
966                        "the discovered JWKS URI uses plain http on a non-loopback host \
967                         ({} is set) — signing keys fetched over it can be substituted by \
968                         anyone on the path. Use https.",
969                        self.naming.key("allow_insecure_http")
970                    );
971                }
972                self.status_fields().set_jwks_uri(Some(uri.clone()));
973                uri
974            }
975        };
976        let shown = redact_url(&jwks_uri);
977        let keys = match self.fetch_jwks(&jwks_uri).await {
978            Ok(keys) => keys,
979            Err(e) => {
980                if self.discovers_jwks_uri {
981                    // Re-read the metadata next time: the authorization
982                    // server may have moved its JWKS. The public status keeps
983                    // showing the URI that failed.
984                    self.status_fields().jwks_uri = None;
985                }
986                return Err(e.context(format_args!("fetching the JWKS from {shown}")));
987            }
988        };
989        let count = keys.len();
990        debug!(count, jwks_uri = %shown, "Fetched JWKS");
991        let previous = std::mem::replace(&mut self.jwks.write().await.keys, keys);
992        let cache = self.jwks.read().await;
993        warn_about_new_ambiguous_keys(&previous, &cache.keys, &self.naming);
994        Ok(count)
995    }
996
997    /// Find the JWKS URI in the issuer's own metadata (used only when no
998    /// `jwks_uri` is configured).
999    ///
1000    /// Only URLs derived from the CONFIGURED issuer are ever fetched — nothing in a
1001    /// token influences where this goes, so it is not an SSRF surface. The
1002    /// document's `issuer` must equal the configured one byte-for-byte (RFC 8414
1003    /// §3.3, OIDC Discovery §4.3: a mismatching document MUST NOT be used), which is
1004    /// what stops a proxy or a misconfigured path from handing us some other
1005    /// server's keys.
1006    ///
1007    /// Runs in an `info` span `oauth_rs.jwks_discovery` with `issuer.host`
1008    /// and, once found, `jwks.host` (hosts alone, from [`jwks_host`]), and
1009    /// `result` (`success` or `discovery`).
1010    async fn discover_jwks_uri(&self) -> Result<String, RefreshError> {
1011        let span = tracing::info_span!(
1012            "oauth_rs.jwks_discovery",
1013            issuer.host = tracing::field::Empty,
1014            jwks.host = tracing::field::Empty,
1015            result = tracing::field::Empty,
1016        );
1017        if !span.is_disabled() {
1018            record_field(&span, "issuer.host", self.issuer_host.as_str());
1019        }
1020        let result = self.discover_in().instrument(span.clone()).await;
1021        match &result {
1022            Ok(uri) => {
1023                if !span.is_disabled() {
1024                    record_field(&span, "jwks.host", jwks_host(uri).as_str());
1025                }
1026                record_field(&span, "result", "success");
1027            }
1028            Err(e) => {
1029                record_field(&span, "result", e.kind().as_str());
1030            }
1031        }
1032        result
1033    }
1034
1035    /// The body of [`JwksStore::discover_jwks_uri`].
1036    async fn discover_in(&self) -> Result<String, RefreshError> {
1037        let issuer_key = self.naming.key("issuer");
1038        let mut errors = Vec::new();
1039        for url in discovery_urls(&self.issuer) {
1040            match self.fetch_json(&url).await {
1041                Ok(doc) => match jwks_uri_from_metadata(
1042                    &doc,
1043                    &self.issuer,
1044                    &issuer_key,
1045                    self.allow_insecure_http,
1046                    &self.naming.key("allow_insecure_http"),
1047                ) {
1048                    Ok(uri) => return Ok(uri),
1049                    Err(e) => errors.push(format!("{}: {e}", redact_url(&url))),
1050                },
1051                Err(e) => errors.push(format!("{}: {e}", redact_url(&url))),
1052            }
1053        }
1054        Err(RefreshError::new(
1055            RefreshErrorKind::Discovery,
1056            format!(
1057                "could not discover a jwks_uri for {issuer_key} {:?} — set {} explicitly or \
1058                 fix the issuer. Tried: {}",
1059                redact_url(&self.issuer),
1060                self.naming.key("jwks_uri"),
1061                errors.join("; ")
1062            ),
1063        ))
1064    }
1065
1066    async fn fetch_jwks(&self, uri: &str) -> Result<Vec<CachedKey>, RefreshError> {
1067        let doc = self.fetch_json(uri).await?;
1068        keys_from_jwk_set(&doc, &self.algorithms, &self.naming)
1069    }
1070
1071    /// GET a JSON document with the body capped at [`MAX_FETCH_BYTES`].
1072    async fn fetch_json(&self, url: &str) -> Result<Value, RefreshError> {
1073        let fetch = |message: String| RefreshError::new(RefreshErrorKind::Fetch, message);
1074        let mut resp = self
1075            .http
1076            .for_url(url)
1077            .get(url)
1078            .header(reqwest::header::ACCEPT, "application/json")
1079            .send()
1080            .await
1081            .map_err(|e| fetch(context("request failed", &e.without_url())))?;
1082        // Success only: `error_for_status` lets a 3xx through (one with no
1083        // `Location`, which reqwest cannot follow), and a redirect's body is
1084        // not the document asked for.
1085        let status = resp.status();
1086        if !status.is_success() {
1087            return Err(fetch(format!("non-success status: {status}")));
1088        }
1089        if let Some(len) = resp.content_length()
1090            && len > MAX_FETCH_BYTES as u64
1091        {
1092            return Err(fetch(format!(
1093                "response is {len} bytes, over the {MAX_FETCH_BYTES}-byte cap"
1094            )));
1095        }
1096        let mut body = Vec::new();
1097        while let Some(chunk) = resp
1098            .chunk()
1099            .await
1100            .map_err(|e| fetch(context("reading the response body", &e.without_url())))?
1101        {
1102            if body.len() + chunk.len() > MAX_FETCH_BYTES {
1103                return Err(fetch(format!(
1104                    "response exceeds the {MAX_FETCH_BYTES}-byte cap"
1105                )));
1106            }
1107            body.extend_from_slice(&chunk);
1108        }
1109        serde_json::from_slice(&body).map_err(|e| {
1110            RefreshError::new(
1111                RefreshErrorKind::Parse,
1112                context("response was not JSON", &e),
1113            )
1114        })
1115    }
1116}
1117
1118/// RFC 8725 §3.1 binds each key to exactly one algorithm. A JWK that
1119/// declares no `alg` (it is OPTIONAL, RFC 7517 §4.4) is usable here for
1120/// every allowlisted algorithm its type can produce — an RSA key for
1121/// RS256/384/512 and PS256/384/512 by default. Accepted, because an
1122/// authorization server that omits `alg` gives no other way to know which
1123/// one it signs with, and no practical attack mixing those on one key is
1124/// known; but said once per key, when it first appears in `current` (and was
1125/// not already in `previous`), with the fix.
1126fn warn_about_new_ambiguous_keys(
1127    previous: &[CachedKey],
1128    current: &[CachedKey],
1129    naming: &KeyNamingBuf,
1130) {
1131    let already: HashSet<Option<&str>> = previous
1132        .iter()
1133        .filter(|k| k.ambiguous)
1134        .map(|k| k.kid.as_deref())
1135        .collect();
1136    for key in current.iter().filter(|k| k.ambiguous) {
1137        if already.contains(&key.kid.as_deref()) {
1138            continue;
1139        }
1140        let algorithms: Vec<&str> = key.algorithms.iter().map(|a| a.as_str()).collect();
1141        warn!(
1142            kid = %describe_kid(key.kid.as_deref()),
1143            algorithms = %algorithms.join(" "),
1144            "JWKS key declares no alg, so it may verify any of {} — RFC 8725 §3.1 binds a \
1145             key to one algorithm. Narrow {} to the algorithm the authorization server \
1146             signs with.",
1147            algorithms.join(", "),
1148            naming.key("algorithms")
1149        );
1150    }
1151}
1152
1153/// The usable keys in a JWK Set document: at most [`MAX_JWKS_KEYS`] entries
1154/// are considered, each through [`parse_jwks_entry`] (so [`cached_key`]'s
1155/// narrowing), and a set with none left is an error. The one path every key
1156/// set takes, fetched or seeded by
1157/// [`crate::OAuthValidatorBuilder::initial_jwks`].
1158pub(crate) fn keys_from_jwk_set(
1159    doc: &Value,
1160    allowed: &[Algorithm],
1161    naming: &KeyNamingBuf,
1162) -> Result<Vec<CachedKey>, RefreshError> {
1163    let entries = doc.get("keys").and_then(Value::as_array).ok_or_else(|| {
1164        RefreshError::new(RefreshErrorKind::Parse, "not a JWK Set (no \"keys\" array)")
1165    })?;
1166    if entries.len() > MAX_JWKS_KEYS {
1167        warn!(
1168            published = entries.len(),
1169            used = MAX_JWKS_KEYS,
1170            "JWK Set has more keys than this server will consider; the rest are ignored"
1171        );
1172    }
1173
1174    let mut keys = Vec::new();
1175    for entry in entries.iter().take(MAX_JWKS_KEYS) {
1176        if let Some(key) = parse_jwks_entry(entry, allowed) {
1177            keys.push(key);
1178        }
1179    }
1180    if keys.is_empty() {
1181        return Err(RefreshError::new(
1182            RefreshErrorKind::NoUsableKeys,
1183            format!(
1184                "the JWK Set contained no usable signature keys for {} {:?}",
1185                naming.key("algorithms"),
1186                allowed
1187            ),
1188        ));
1189    }
1190    Ok(keys)
1191}
1192
1193/// Parse a JWK Set given as JSON text (not fetched) into its usable keys:
1194/// the same [`MAX_FETCH_BYTES`] cap a fetched body is held to, then
1195/// [`keys_from_jwk_set`]. For [`crate::OAuthValidatorBuilder::initial_jwks`].
1196pub(crate) fn keys_from_jwk_set_json(
1197    json: &str,
1198    allowed: &[Algorithm],
1199    naming: &KeyNamingBuf,
1200) -> Result<Vec<CachedKey>, RefreshError> {
1201    if json.len() > MAX_FETCH_BYTES {
1202        return Err(RefreshError::new(
1203            RefreshErrorKind::Parse,
1204            format!(
1205                "it is {} bytes, over the {MAX_FETCH_BYTES}-byte cap",
1206                json.len()
1207            ),
1208        ));
1209    }
1210    let doc: Value = serde_json::from_str(json)
1211        .map_err(|e| RefreshError::new(RefreshErrorKind::Parse, context("not JSON", &e)))?;
1212    keys_from_jwk_set(&doc, allowed, naming)
1213}
1214
1215/// Build a [`CachedKey`] from one raw JWK Set entry, or `None` when the entry
1216/// is unparseable or the key must not be used (see [`cached_key`]).
1217///
1218/// Parsed one key at a time: `jsonwebtoken::jwk::JwkSet` refuses the WHOLE set
1219/// if any single key has a kty/crv/alg it does not model (an X25519 encryption
1220/// key, say), and one exotic key must not take the usable ones down with it.
1221/// Pure — no I/O, never panics on hostile input — which is what lets the fuzz
1222/// targets drive it directly.
1223pub(crate) fn parse_jwks_entry(entry: &Value, allowed: &[Algorithm]) -> Option<CachedKey> {
1224    let jwk: Jwk = match serde_json::from_value(entry.clone()) {
1225        Ok(jwk) => jwk,
1226        Err(e) => {
1227            debug!(error = %e, "Skipping a JWKS entry this server cannot parse");
1228            return None;
1229        }
1230    };
1231    cached_key(&jwk, allowed)
1232}
1233
1234/// Build a [`CachedKey`] from one JWK, or `None` when the key must not be used.
1235fn cached_key(jwk: &Jwk, allowed: &[Algorithm]) -> Option<CachedKey> {
1236    // `use: enc` keys exist in real key sets (Keycloak publishes one). An
1237    // encryption key verifying a signature is a key-confusion bug in waiting.
1238    match &jwk.common.public_key_use {
1239        None | Some(PublicKeyUse::Signature) => {}
1240        Some(_) => return None,
1241    }
1242    // `key_ops` (RFC 7517 §4.3) is the other way a JWK says what it is for.
1243    // A key whose operations do not include `verify` — `["encrypt"]`,
1244    // `["wrapKey"]` — is the same confusion as `use: enc`.
1245    if let Some(ops) = &jwk.common.key_operations
1246        && !ops.contains(&KeyOperations::Verify)
1247    {
1248        return None;
1249    }
1250    let mut algorithms = key_algorithms(&jwk.algorithm)?;
1251    // When the key names its own algorithm, that is the ONLY one it verifies. A
1252    // declared algorithm the key type cannot produce (an RSA key labelled ES256)
1253    // means the entry is broken; skipping it is safer than guessing.
1254    if let Some(declared) = &jwk.common.key_algorithm {
1255        match signing_algorithm(declared) {
1256            Some(alg) if algorithms.contains(&alg) => algorithms = vec![alg],
1257            _ => return None,
1258        }
1259    }
1260    algorithms.retain(|alg| allowed.contains(alg));
1261    if algorithms.is_empty() {
1262        return None;
1263    }
1264    let ambiguous = jwk.common.key_algorithm.is_none() && algorithms.len() > 1;
1265    if let AlgorithmParameters::RSA(rsa) = &jwk.algorithm
1266        && !rsa_components_can_verify(&rsa.n, &rsa.e)
1267    {
1268        warn!(
1269            kid = ?jwk.common.key_id.as_deref().map(for_log),
1270            "Skipping an RSA JWKS entry that cannot verify any signature (its modulus is not \
1271             2048 to 8192 bits, or its exponent is not an odd number from 3 to 2^33 - 1, \
1272             minimally encoded)"
1273        );
1274        return None;
1275    }
1276    match DecodingKey::from_jwk(jwk) {
1277        Ok(key) => Some(CachedKey {
1278            kid: jwk.common.key_id.clone(),
1279            key,
1280            algorithms,
1281            ambiguous,
1282        }),
1283        Err(e) => {
1284            warn!(
1285                kid = ?jwk.common.key_id.as_deref().map(for_log),
1286                error = %e,
1287                "Skipping unusable JWKS entry"
1288            );
1289            None
1290        }
1291    }
1292}
1293
1294/// Whether RSA components `n` and `e` (base64url, as in a JWK) could verify
1295/// any signature at all: the rules `ring`, which jsonwebtoken verifies with,
1296/// applies only at verification time — an odd modulus of 2048 to 8192 bits
1297/// exactly (no leading zero byte), and an odd exponent
1298/// from 3 to 2^33 - 1 with no leading zero byte. A key failing them was
1299/// once counted as held (`KeySetStatus::keys`, `is_ready`) while verifying
1300/// nothing; skipping it here makes the count mean usable keys.
1301fn rsa_components_can_verify(n: &str, e: &str) -> bool {
1302    let (Ok(n), Ok(e)) = (URL_SAFE_NO_PAD.decode(n), URL_SAFE_NO_PAD.decode(e)) else {
1303        return false;
1304    };
1305    // Bit length as ring counts it: the top byte must be non-zero (no
1306    // leading zero), so the length is exact.
1307    let modulus_bits = match n.first() {
1308        Some(&top) if top != 0 => (n.len() - 1) * 8 + (8 - top.leading_zeros() as usize),
1309        _ => 0,
1310    };
1311    let modulus_ok = (2048..=8192).contains(&modulus_bits) && n.last().is_some_and(|b| b & 1 == 1);
1312    let exponent_ok = (1..=5).contains(&e.len())
1313        && e[0] != 0
1314        && e[e.len() - 1] & 1 == 1
1315        && (3..=(1u64 << 33) - 1)
1316            .contains(&e.iter().fold(0u64, |acc, b| (acc << 8) | u64::from(*b)));
1317    modulus_ok && exponent_ok
1318}
1319
1320/// Find the key for `kid` that can verify `alg`.
1321///
1322/// A token header with no `kid` falls back to the single key that can verify its
1323/// `alg`, when there is exactly one. That is not laxity: with one candidate key
1324/// there is exactly one key the signature could have been made with, so the
1325/// fallback picks the same key an explicit `kid` would have. With two or more it
1326/// refuses rather than trying each, which would turn key rotation into a
1327/// signature-verification oracle.
1328fn lookup(keys: &[CachedKey], kid: Option<&str>, alg: Algorithm) -> Option<DecodingKey> {
1329    let mut candidates = keys.iter().filter(|k| k.algorithms.contains(&alg));
1330    match kid {
1331        Some(kid) => candidates
1332            .find(|k| k.kid.as_deref() == Some(kid))
1333            .map(|k| k.key.clone()),
1334        None => {
1335            let only = candidates.next()?;
1336            candidates.next().is_none().then(|| only.key.clone())
1337        }
1338    }
1339}
1340
1341/// The metadata URLs to try for `issuer`, in order: OpenID Connect Discovery
1342/// (§4: the issuer with any trailing slash removed, plus
1343/// `/.well-known/openid-configuration` — where every server this crate has been
1344/// tested against publishes, including per-application issuers like Authentik's
1345/// and Kanidm's), then RFC 8414 §3.1's form (the well-known segment inserted
1346/// between host and path).
1347pub(crate) fn discovery_urls(issuer: &str) -> Vec<String> {
1348    const OIDC: &str = "/.well-known/openid-configuration";
1349    if reqwest::Url::parse(issuer.trim()).is_err() {
1350        // Reachable only from a hand-edited `ResolvedOAuthConfig` (`resolve`
1351        // refuses it). Appending to, say, `https://` would parse with
1352        // `.well-known` as its host; the bare path names no host, and its
1353        // fetch fails closed as a relative URL.
1354        return vec![OIDC.to_string()];
1355    }
1356    let trimmed = issuer.trim_end_matches('/');
1357    let mut urls = vec![format!("{trimmed}{OIDC}")];
1358    // The RFC 8414 form takes the raw issuer apart on `://`, which only means
1359    // what the parser means for a canonically spelled URL: `https:///host/app`
1360    // would put `.well-known` where the host goes and send the fetch to a host
1361    // nobody configured. A non-canonical issuer (warned about at startup) never
1362    // matches a token's `iss` anyway, so it gets the OIDC form only, which the
1363    // parser keeps on the issuer's own host.
1364    if is_canonical_url(issuer)
1365        && let Some((scheme, rest)) = trimmed.split_once("://")
1366    {
1367        let (authority, path) = match rest.find('/') {
1368            Some(i) => (&rest[..i], &rest[i..]),
1369            None => (rest, ""),
1370        };
1371        let rfc8414 =
1372            format!("{scheme}://{authority}/.well-known/oauth-authorization-server{path}");
1373        if !urls.contains(&rfc8414) {
1374            urls.push(rfc8414);
1375        }
1376    }
1377    urls
1378}
1379
1380/// Pull `jwks_uri` out of an authorization-server metadata document, refusing a
1381/// document for a different issuer and a key URL that would downgrade transport.
1382/// `issuer_key` names the issuer setting in the error.
1383///
1384/// A plain-http `jwks_uri` is accepted only from a plain-http issuer, and —
1385/// when it points at a non-loopback host — only with `allow_insecure_http`
1386/// (named by `opt_in_key`), the same rule `resolve` applies to a configured
1387/// `jwks_uri` (RFC 8414 §2: `jwks_uri` MUST use https). Without that, a
1388/// loopback issuer, which needs no opt-in, could steer key fetches onto a
1389/// cleartext network path.
1390fn jwks_uri_from_metadata(
1391    doc: &Value,
1392    issuer: &str,
1393    issuer_key: &str,
1394    allow_insecure_http: bool,
1395    opt_in_key: &str,
1396) -> Result<String, String> {
1397    // Every URL in a message is redacted: the configured issuer may carry a
1398    // credential, and a document echoing it back may too.
1399    let shown_issuer = redact_url(issuer);
1400    let found = doc.get("issuer").and_then(Value::as_str);
1401    if found != Some(issuer) {
1402        return Err(format!(
1403            "metadata issuer {} does not match {issuer_key} {shown_issuer:?} byte-for-byte \
1404             (RFC 8414 §3.3 / OIDC Discovery §4.3: such a document must not be used)",
1405            found.map_or_else(
1406                || "(absent)".to_string(),
1407                |f| format!("{:?}", for_log(&redact_url(f)))
1408            )
1409        ));
1410    }
1411    let uri = doc
1412        .get("jwks_uri")
1413        .and_then(Value::as_str)
1414        .ok_or_else(|| "metadata has no jwks_uri".to_string())?;
1415    // Both are judged as parsed — as reqwest will fetch them — never by their
1416    // raw prefix: `http:/host`, `HTTP:\\host` and ` http://host` all go to
1417    // `http://host/`.
1418    let parsed = reqwest::Url::parse(uri.trim())
1419        .map_err(|e| context("jwks_uri is not an absolute URL", &e))?;
1420    let issuer_is_http =
1421        reqwest::Url::parse(issuer.trim()).is_ok_and(|issuer| issuer.scheme() == "http");
1422    match parsed.scheme() {
1423        "https" => {}
1424        // Plain http only when the issuer itself is plain http (a loopback test
1425        // setup); an https issuer must never hand us keys over http.
1426        "http" if issuer_is_http => {
1427            if !allow_insecure_http && parsed_plain_http_non_loopback(&parsed) {
1428                return Err(format!(
1429                    "jwks_uri {:?} uses plain http on a non-loopback host — refused \
1430                     (RFC 8414 §2) unless {opt_in_key} is set",
1431                    for_log(&redact_url(uri))
1432                ));
1433            }
1434        }
1435        other => {
1436            return Err(format!(
1437                "jwks_uri scheme {other:?} is not allowed for issuer {shown_issuer:?}"
1438            ));
1439        }
1440    }
1441    Ok(uri.to_string())
1442}
1443
1444#[cfg(test)]
1445mod tests {
1446    use super::*;
1447
1448    const OPT_IN: &str = "mcp.oauth.allow_insecure_http";
1449
1450    #[test]
1451    fn discovery_urls_follow_oidc_then_rfc_8414() {
1452        assert_eq!(
1453            discovery_urls("https://auth.example.com/application/o/wiki/"),
1454            [
1455                "https://auth.example.com/application/o/wiki/.well-known/openid-configuration",
1456                "https://auth.example.com/.well-known/oauth-authorization-server/application/o/wiki",
1457            ]
1458        );
1459        assert_eq!(
1460            discovery_urls("https://auth.example.com"),
1461            [
1462                "https://auth.example.com/.well-known/openid-configuration",
1463                "https://auth.example.com/.well-known/oauth-authorization-server",
1464            ]
1465        );
1466    }
1467
1468    #[test]
1469    fn discovered_jwks_uri_must_not_downgrade_transport() {
1470        let key = "mcp.oauth.issuer";
1471        let doc = serde_json::json!({
1472            "issuer": "https://auth.example.com",
1473            "jwks_uri": "http://auth.example.com/jwks",
1474        });
1475        assert!(
1476            jwks_uri_from_metadata(&doc, "https://auth.example.com", key, false, OPT_IN).is_err()
1477        );
1478        let doc = serde_json::json!({
1479            "issuer": "https://auth.example.com",
1480            "jwks_uri": "file:///etc/passwd",
1481        });
1482        assert!(
1483            jwks_uri_from_metadata(&doc, "https://auth.example.com", key, false, OPT_IN).is_err()
1484        );
1485        let doc = serde_json::json!({"issuer": "https://auth.example.com"});
1486        assert!(
1487            jwks_uri_from_metadata(&doc, "https://auth.example.com", key, false, OPT_IN).is_err()
1488        );
1489        // The opt-in never lets an https issuer hand out http keys.
1490        let doc = serde_json::json!({
1491            "issuer": "https://auth.example.com",
1492            "jwks_uri": "http://auth.example.com/jwks",
1493        });
1494        assert!(
1495            jwks_uri_from_metadata(&doc, "https://auth.example.com", key, true, OPT_IN).is_err()
1496        );
1497    }
1498
1499    #[test]
1500    fn a_loopback_issuer_cannot_discover_a_cleartext_non_loopback_jwks_uri() {
1501        let key = "mcp.oauth.issuer";
1502        let issuer = "http://localhost:9000/app/";
1503        let doc =
1504            serde_json::json!({"issuer": issuer, "jwks_uri": "http://idp.internal.test/jwks"});
1505        let err = jwks_uri_from_metadata(&doc, issuer, key, false, OPT_IN).unwrap_err();
1506        assert!(err.contains("plain http on a non-loopback host"), "{err}");
1507        assert!(err.contains(OPT_IN), "{err}");
1508        // With the opt-in it is accepted (and `refresh` warns about it).
1509        assert_eq!(
1510            jwks_uri_from_metadata(&doc, issuer, key, true, OPT_IN).unwrap(),
1511            "http://idp.internal.test/jwks"
1512        );
1513        // A loopback http jwks_uri never needs the opt-in.
1514        let doc = serde_json::json!({"issuer": issuer, "jwks_uri": "http://127.0.0.1:9000/jwks"});
1515        assert!(jwks_uri_from_metadata(&doc, issuer, key, false, OPT_IN).is_ok());
1516    }
1517
1518    #[test]
1519    fn a_non_canonical_cleartext_jwks_uri_is_judged_by_where_it_really_goes() {
1520        let key = "mcp.oauth.issuer";
1521        let issuer = "http://localhost:9000/app/";
1522        // Each is fetched by reqwest as http://idp.internal.test/jwks.
1523        for uri in [
1524            "http:/idp.internal.test/jwks",
1525            "http:idp.internal.test/jwks",
1526            "HTTP:\\\\idp.internal.test\\jwks",
1527            " http://idp.internal.test/jwks",
1528        ] {
1529            let doc = serde_json::json!({"issuer": issuer, "jwks_uri": uri});
1530            let err = jwks_uri_from_metadata(&doc, issuer, key, false, OPT_IN)
1531                .expect_err(&format!("{uri:?} must be refused"));
1532            assert!(
1533                err.contains("plain http on a non-loopback host"),
1534                "{uri:?}: {err}"
1535            );
1536        }
1537        // An https issuer never accepts one either, however it is spelled.
1538        let issuer = "https://auth.example.com";
1539        for uri in [
1540            "http:/idp.internal.test/jwks",
1541            "HTTP:idp.internal.test/jwks",
1542        ] {
1543            let doc = serde_json::json!({"issuer": issuer, "jwks_uri": uri});
1544            assert!(
1545                jwks_uri_from_metadata(&doc, issuer, key, true, OPT_IN).is_err(),
1546                "{uri:?}"
1547            );
1548        }
1549        // An upper-case loopback http issuer may discover a loopback http one.
1550        let issuer = "HTTP://localhost:9000/app/";
1551        let doc = serde_json::json!({"issuer": issuer, "jwks_uri": "http://127.0.0.1:9000/jwks"});
1552        assert!(jwks_uri_from_metadata(&doc, issuer, key, false, OPT_IN).is_ok());
1553    }
1554
1555    #[test]
1556    fn redirects_are_held_to_the_insecure_http_policy() {
1557        let url = |s: &str| reqwest::Url::parse(s).unwrap();
1558        let loopback = [url("http://127.0.0.1:9000/jwks")];
1559        let plain = [url("http://idp.internal.test/start")];
1560        let https = [url("https://auth.example.com/jwks")];
1561        // Non-loopback http → non-loopback http: refused without the opt-in,
1562        // warned with it.
1563        let Hop::Refuse(reason) =
1564            judge_redirect(&url("http://idp.internal.test/jwks"), &plain, false, OPT_IN)
1565        else {
1566            panic!("a cleartext non-loopback hop must be refused without the opt-in");
1567        };
1568        assert!(reason.contains(OPT_IN), "{reason}");
1569        assert_eq!(
1570            judge_redirect(&url("http://idp.internal.test/jwks"), &plain, true, OPT_IN),
1571            Hop::FollowInsecure
1572        );
1573        // A fetch that started on loopback never leaves it — https, plain
1574        // http, with or without the opt-in, and after a loopback hop too:
1575        // the proxy-free loopback client would carry the hop.
1576        for target in [
1577            "http://idp.internal.test/jwks",
1578            "https://idp.example.com/keys",
1579            "http://203.0.113.1/keys",
1580            "http://localhost.example.test/keys",
1581        ] {
1582            for opt_in in [false, true] {
1583                let Hop::Refuse(reason) = judge_redirect(&url(target), &loopback, opt_in, OPT_IN)
1584                else {
1585                    panic!("{target} after a loopback start must be refused");
1586                };
1587                assert!(
1588                    reason.contains("loopback URL to a non-loopback host"),
1589                    "{reason}"
1590                );
1591            }
1592            let chain = [loopback[0].clone(), url("http://localhost:9000/moved")];
1593            assert!(matches!(
1594                judge_redirect(&url(target), &chain, true, OPT_IN),
1595                Hop::Refuse(_)
1596            ));
1597        }
1598        // Loopback → loopback, and non-loopback → https, are plain follows.
1599        for target in [
1600            "http://localhost:9000/keys",
1601            "http://127.0.0.2:9000/keys",
1602            "http://[::1]:9000/keys",
1603            "http://app.localhost:9000/keys",
1604        ] {
1605            assert_eq!(
1606                judge_redirect(&url(target), &loopback, false, OPT_IN),
1607                Hop::Follow,
1608                "{target}"
1609            );
1610        }
1611        assert_eq!(
1612            judge_redirect(&url("https://idp.example.com/keys"), &plain, false, OPT_IN),
1613            Hop::Follow
1614        );
1615        // https → http is refused whatever the opt-in says, loopback included.
1616        for target in ["http://idp.internal.test/jwks", "http://127.0.0.1/jwks"] {
1617            assert!(matches!(
1618                judge_redirect(&url(target), &https, true, OPT_IN),
1619                Hop::Refuse(_)
1620            ));
1621        }
1622        // The hop limit still applies.
1623        let many = [
1624            url("https://a.example.com/"),
1625            url("https://b.example.com/"),
1626            url("https://c.example.com/"),
1627            url("https://d.example.com/"),
1628        ];
1629        assert_eq!(
1630            judge_redirect(&url("https://e.example.com/"), &many, false, OPT_IN),
1631            Hop::Refuse("too many redirects".to_string())
1632        );
1633    }
1634
1635    #[test]
1636    fn error_chain_matches_the_context_colon_cause_shape() {
1637        #[derive(Debug, thiserror::Error)]
1638        #[error("outer")]
1639        struct Outer(#[source] Inner);
1640        #[derive(Debug, thiserror::Error)]
1641        #[error("inner")]
1642        struct Inner;
1643        assert_eq!(context("fetching", &Outer(Inner)), "fetching: outer: inner");
1644    }
1645
1646    fn rsa_jwk(extra: Value) -> Jwk {
1647        let mut jwk = serde_json::json!({
1648            "kty": "RSA", "kid": "k", "n": crate::testing::N_A, "e": "AQAB",
1649        });
1650        for (k, v) in extra.as_object().unwrap() {
1651            jwk[k] = v.clone();
1652        }
1653        serde_json::from_value(jwk).unwrap()
1654    }
1655
1656    #[test]
1657    fn key_ops_without_verify_make_a_key_unusable() {
1658        let all: Vec<Algorithm> = crate::DEFAULT_ALGORITHMS
1659            .iter()
1660            .map(|a| crate::parse_algorithm(a).unwrap())
1661            .collect();
1662        for ops in [
1663            serde_json::json!(["encrypt"]),
1664            serde_json::json!(["encrypt", "wrapKey"]),
1665            serde_json::json!(["sign"]),
1666            serde_json::json!([]),
1667            serde_json::json!(["some-future-op"]),
1668        ] {
1669            assert!(
1670                cached_key(&rsa_jwk(serde_json::json!({ "key_ops": ops })), &all).is_none(),
1671                "key_ops {ops} must not verify"
1672            );
1673        }
1674        for ops in [
1675            serde_json::json!(["verify"]),
1676            serde_json::json!(["sign", "verify"]),
1677        ] {
1678            assert!(
1679                cached_key(&rsa_jwk(serde_json::json!({ "key_ops": ops })), &all).is_some(),
1680                "key_ops {ops} may verify"
1681            );
1682        }
1683        // Absent, as nearly every authorization server publishes it: usable.
1684        assert!(cached_key(&rsa_jwk(serde_json::json!({})), &all).is_some());
1685    }
1686
1687    fn all_algorithms() -> Vec<Algorithm> {
1688        crate::DEFAULT_ALGORITHMS
1689            .iter()
1690            .map(|a| crate::parse_algorithm(a).unwrap())
1691            .collect()
1692    }
1693
1694    fn rsa_entry(extra: Value) -> Value {
1695        let mut entry = serde_json::json!({
1696            "kty": "RSA", "kid": "k", "n": crate::testing::N_A, "e": "AQAB",
1697        });
1698        for (k, v) in extra.as_object().unwrap() {
1699            entry[k] = v.clone();
1700        }
1701        entry
1702    }
1703
1704    #[test]
1705    fn a_plain_signature_entry_is_parsed_into_a_key() {
1706        let all = all_algorithms();
1707        let key = parse_jwks_entry(&rsa_entry(serde_json::json!({})), &all).unwrap();
1708        assert_eq!(key.kid.as_deref(), Some("k"));
1709        let key = parse_jwks_entry(&rsa_entry(serde_json::json!({"use": "sig"})), &all).unwrap();
1710        assert_eq!(key.kid.as_deref(), Some("k"));
1711    }
1712
1713    #[test]
1714    fn an_encryption_use_entry_is_skipped() {
1715        let all = all_algorithms();
1716        for usage in ["enc", "something-else"] {
1717            assert!(
1718                parse_jwks_entry(&rsa_entry(serde_json::json!({ "use": usage })), &all).is_none(),
1719                "use {usage} must not verify"
1720            );
1721        }
1722    }
1723
1724    #[test]
1725    fn a_key_ops_entry_without_verify_is_skipped_at_entry_level_too() {
1726        let all = all_algorithms();
1727        let entry = rsa_entry(serde_json::json!({ "key_ops": ["encrypt"] }));
1728        assert!(parse_jwks_entry(&entry, &all).is_none());
1729    }
1730
1731    #[test]
1732    fn an_hmac_entry_is_skipped() {
1733        let all = all_algorithms();
1734        let entry = serde_json::json!({"kty": "oct", "kid": "hmac", "k": "c2VjcmV0"});
1735        assert!(parse_jwks_entry(&entry, &all).is_none());
1736    }
1737
1738    #[test]
1739    fn an_unparseable_entry_is_skipped_not_fatal() {
1740        let all = all_algorithms();
1741        for entry in [
1742            serde_json::json!({"kty": "OKP", "crv": "X25519", "kid": "x", "x": "AA"}),
1743            serde_json::json!({"kty": "no-such-type"}),
1744            serde_json::json!({"kid": "no kty at all"}),
1745            serde_json::json!("not an object"),
1746            serde_json::json!(null),
1747            serde_json::json!(42),
1748            serde_json::json!([]),
1749        ] {
1750            assert!(parse_jwks_entry(&entry, &all).is_none(), "{entry}");
1751        }
1752        // ...and the entry after one such skip is still usable.
1753        assert!(parse_jwks_entry(&rsa_entry(serde_json::json!({})), &all).is_some());
1754    }
1755
1756    #[test]
1757    fn an_entry_outside_the_allowlist_is_skipped() {
1758        let entry = rsa_entry(serde_json::json!({}));
1759        assert!(parse_jwks_entry(&entry, &[Algorithm::ES256]).is_none());
1760        assert!(parse_jwks_entry(&entry, &[]).is_none());
1761    }
1762
1763    #[test]
1764    fn an_alg_less_key_usable_under_several_algorithms_is_flagged_ambiguous() {
1765        let all: Vec<Algorithm> = crate::DEFAULT_ALGORITHMS
1766            .iter()
1767            .map(|a| crate::parse_algorithm(a).unwrap())
1768            .collect();
1769        let key = cached_key(&rsa_jwk(serde_json::json!({})), &all).unwrap();
1770        assert!(key.ambiguous);
1771        assert_eq!(key.algorithms.len(), 6);
1772        // A declared alg binds it to one algorithm.
1773        let key = cached_key(&rsa_jwk(serde_json::json!({"alg": "PS256"})), &all).unwrap();
1774        assert!(!key.ambiguous);
1775        assert_eq!(key.algorithms, [Algorithm::PS256]);
1776        // So does narrowing the allowlist to one RSA algorithm.
1777        let key = cached_key(&rsa_jwk(serde_json::json!({})), &[Algorithm::RS256]).unwrap();
1778        assert!(!key.ambiguous);
1779        assert_eq!(key.algorithms, [Algorithm::RS256]);
1780    }
1781
1782    #[test]
1783    fn an_rsa_key_that_cannot_verify_is_skipped() {
1784        let all = all_algorithms();
1785        let b64 = |bytes: &[u8]| URL_SAFE_NO_PAD.encode(bytes);
1786        let mut modulus = vec![0xc5_u8; 256];
1787        modulus[255] = 0x01;
1788        let odd_2048 = b64(&modulus);
1789        // Usable: the fixture key, and any odd 2048..=8192-bit modulus with an
1790        // odd exponent from 3 up.
1791        assert!(rsa_components_can_verify(crate::testing::N_A, "AQAB"));
1792        assert!(rsa_components_can_verify(&odd_2048, "Aw"));
1793        assert!(rsa_components_can_verify(&b64(&[0xff; 1024]), "AQAB"));
1794        for (what, n, e) in [
1795            ("empty e", odd_2048.clone(), String::new()),
1796            ("e = 1", odd_2048.clone(), b64(&[1])),
1797            ("even e", odd_2048.clone(), b64(&[0x01, 0x00, 0x00])),
1798            (
1799                "e with a leading zero",
1800                odd_2048.clone(),
1801                b64(&[0, 1, 0, 1]),
1802            ),
1803            (
1804                "e over 2^33 - 1",
1805                odd_2048.clone(),
1806                b64(&[0x02, 0, 0, 0, 1]),
1807            ),
1808            ("e over 5 bytes", odd_2048.clone(), b64(&[1, 0, 0, 0, 0, 1])),
1809            ("empty n", String::new(), "AQAB".into()),
1810            ("n under 2048 bits", b64(&[0xc5; 255]), "AQAB".into()),
1811            (
1812                "n of 2047 bits",
1813                b64(&[&[0x7f][..], &modulus[1..]].concat()),
1814                "AQAB".into(),
1815            ),
1816            (
1817                "n of 2041 bits",
1818                b64(&[&[0x01][..], &modulus[1..]].concat()),
1819                "AQAB".into(),
1820            ),
1821            ("n over 8192 bits", b64(&[0xc5; 1025]), "AQAB".into()),
1822            ("even n", b64(&[0xc4; 256]), "AQAB".into()),
1823            (
1824                "n with a leading zero",
1825                b64(&[&[0][..], &modulus[..]].concat()),
1826                "AQAB".into(),
1827            ),
1828            ("n not base64url", "!!".repeat(200), "AQAB".into()),
1829        ] {
1830            assert!(!rsa_components_can_verify(&n, &e), "{what}");
1831            let entry = serde_json::json!({"kty": "RSA", "kid": "k", "n": n, "e": e});
1832            assert!(parse_jwks_entry(&entry, &all).is_none(), "{what}");
1833        }
1834        // The key cap still counts every entry considered, usable or not.
1835        let naming = KeyNamingBuf::Dotted("oauth".into());
1836        let bad = serde_json::json!({"kty": "RSA", "kid": "bad", "n": odd_2048, "e": ""});
1837        let mut entries = vec![bad; MAX_JWKS_KEYS];
1838        entries.push(rsa_entry(serde_json::json!({})));
1839        let doc = serde_json::json!({ "keys": entries });
1840        let err = keys_from_jwk_set(&doc, &all, &naming).err().unwrap();
1841        assert_eq!(err.kind(), RefreshErrorKind::NoUsableKeys);
1842    }
1843
1844    /// The `loopback` client reaches this host for a name only its own
1845    /// resolver can map there: `.invalid` (RFC 6761 §6.4) never resolves, so
1846    /// the `normal` client, on the system resolver, fails on the very same
1847    /// URL — the success is the `.dns_resolver(..)` wiring and nothing else.
1848    #[tokio::test]
1849    async fn the_loopback_client_resolves_a_name_no_dns_would() {
1850        let jwks = crate::testing::spawn_jwks_server("200 OK", crate::testing::jwks_body()).await;
1851        let url = jwks.url.replace("127.0.0.1", "nonexistent-name.invalid");
1852        let clients = http_clients(false, OPT_IN, &FetchSettings::default()).unwrap();
1853        let fetched = clients.loopback.get(&url).send().await.unwrap();
1854        assert!(fetched.status().is_success(), "{}", fetched.status());
1855        assert_eq!(
1856            jwks.hits.load(std::sync::atomic::Ordering::SeqCst),
1857            1,
1858            "the URL's port was kept"
1859        );
1860        assert!(
1861            clients.normal.get(&url).send().await.is_err(),
1862            "the system resolver must not resolve {url}"
1863        );
1864        assert_eq!(jwks.hits.load(std::sync::atomic::Ordering::SeqCst), 1);
1865    }
1866
1867    /// The loopback client's resolver answers every name with loopback and
1868    /// never asks DNS, whatever the name.
1869    #[tokio::test]
1870    async fn the_loopback_resolver_answers_every_name_with_loopback() {
1871        use reqwest::dns::Resolve;
1872        for name in ["localhost", "idp.localhost", "example.test", "a.b.c"] {
1873            let addrs: Vec<std::net::SocketAddr> = LoopbackResolver
1874                .resolve(name.parse().unwrap())
1875                .await
1876                .unwrap()
1877                .collect();
1878            assert_eq!(addrs.len(), 2, "{name}");
1879            assert!(
1880                addrs.iter().all(|a| a.ip().is_loopback() && a.port() == 0),
1881                "{name}: {addrs:?}"
1882            );
1883        }
1884    }
1885
1886    #[test]
1887    fn background_retries_back_off_from_a_minute_to_an_hour() {
1888        assert_eq!(background_retry_delay(1), Duration::from_secs(60));
1889        assert_eq!(background_retry_delay(2), Duration::from_secs(120));
1890        assert_eq!(background_retry_delay(3), Duration::from_secs(240));
1891        assert_eq!(background_retry_delay(6), Duration::from_secs(1920));
1892        assert_eq!(background_retry_delay(7), Duration::from_secs(3600));
1893        assert_eq!(background_retry_delay(u32::MAX), Duration::from_secs(3600));
1894    }
1895
1896    #[test]
1897    fn redact_url_masks_userinfo_query_and_fragment_only() {
1898        for (raw, shown) in [
1899            // user and password
1900            (
1901                "https://alice:s3cret@idp.example.com:8443/jwks",
1902                "https://***@idp.example.com:8443/jwks",
1903            ),
1904            // user only, and password only
1905            (
1906                "https://alice@idp.example.com/jwks",
1907                "https://***@idp.example.com/jwks",
1908            ),
1909            (
1910                "https://:s3cret@idp.example.com/jwks",
1911                "https://***@idp.example.com/jwks",
1912            ),
1913            // query
1914            (
1915                "https://idp.example.com/jwks?key=t0ken",
1916                "https://idp.example.com/jwks?***",
1917            ),
1918            // all of them
1919            (
1920                "https://alice:s3cret@idp.example.com/o/app/jwks?key=t0ken#frag",
1921                "https://***@idp.example.com/o/app/jwks?***#***",
1922            ),
1923            // IPv6 host with a port
1924            (
1925                "http://alice:s3cret@[::1]:9000/jwks?key=t0ken",
1926                "http://***@[::1]:9000/jwks?***",
1927            ),
1928        ] {
1929            let redacted = redact_url(raw);
1930            assert_eq!(redacted, shown, "{raw}");
1931            for secret in ["alice", "s3cret", "t0ken", "frag"] {
1932                assert!(!redacted.contains(secret), "{raw} -> {redacted}");
1933            }
1934        }
1935        // Nothing to mask: returned exactly as given, not normalized.
1936        for raw in [
1937            "https://idp.example.com/app/",
1938            "https://IDP.example.com",
1939            "http://[::1]:9000/jwks",
1940        ] {
1941            assert_eq!(redact_url(raw), raw);
1942        }
1943        // An `@` in the query or fragment is masked with the rest of it.
1944        assert_eq!(
1945            redact_url("https://idp.example.test/jwks?u=alice:s3cret@x#y@z"),
1946            "https://idp.example.test/jwks?***#***"
1947        );
1948        // Unparseable, or an `@` the parser did not take as userinfo (which
1949        // can hide a credential in what parsed as host, port or path): a
1950        // fixed placeholder, never the input, never a panic.
1951        for raw in [
1952            "http://alice:1234/s3cret@proxy.example.test:3128",
1953            "https://idp.example.test/alice:s3cret@x/jwks",
1954            "",
1955            "not a url",
1956            "alice:s3cret@idp.example.com/jwks",
1957            "http://[::1",
1958            "https://alice:s3cret@",
1959        ] {
1960            let redacted = redact_url(raw);
1961            assert!(!redacted.contains("s3cret"), "{raw} -> {redacted}");
1962            assert_eq!(redacted, "<unparseable URL, redacted>", "{raw}");
1963        }
1964    }
1965
1966    #[test]
1967    fn keyless_retries_back_off_from_five_seconds_to_five_minutes() {
1968        let secs: Vec<u64> = (1..=9).map(|n| keyless_retry_delay(n).as_secs()).collect();
1969        assert_eq!(secs, [5, 10, 20, 40, 80, 160, 300, 300, 300]);
1970        assert_eq!(keyless_retry_delay(0), KEYLESS_RETRY_FLOOR);
1971        assert_eq!(keyless_retry_delay(u32::MAX), KEYLESS_RETRY_CAP);
1972        for n in 0..64 {
1973            assert!(
1974                keyless_retry_delay(n) >= Duration::from_secs(5),
1975                "never under the floor"
1976            );
1977        }
1978    }
1979
1980    #[test]
1981    fn refresh_error_kinds_have_stable_labels() {
1982        let labels: Vec<&str> = [
1983            RefreshErrorKind::Discovery,
1984            RefreshErrorKind::Fetch,
1985            RefreshErrorKind::Parse,
1986            RefreshErrorKind::NoUsableKeys,
1987        ]
1988        .iter()
1989        .map(|k| k.as_str())
1990        .collect();
1991        assert_eq!(labels, ["discovery", "fetch", "parse", "no_usable_keys"]);
1992        let err = RefreshError::new(RefreshErrorKind::Parse, "inner").context("outer");
1993        assert_eq!(err.to_string(), "outer: inner");
1994        assert_eq!(err.kind(), RefreshErrorKind::Parse);
1995    }
1996
1997    #[test]
1998    fn the_http_client_builds_with_the_enabled_tls_backend() {
1999        // Whichever of `rustls-tls` / `native-tls` this build enabled, the client
2000        // the validator fetches keys with must build.
2001        http_clients(false, OPT_IN, &FetchSettings::default())
2002            .expect("the JWKS HTTP clients must build");
2003    }
2004}