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}