Skip to main content

flymcprust_core/auth/
jwt.rs

1//! Built-in verifier for JWT access tokens (RFC 9068), signed with keys the
2//! authorization server publishes as a JSON Web Key Set (JWKS).
3//!
4//! Only asymmetric signatures are supported (RSA, RSA-PSS, ECDSA P-256 and
5//! P-384, Ed25519); `none` and HMAC algorithms do not exist here, so they
6//! cannot be accepted by mistake. Keys come only from the configured source:
7//! the `jku`, `x5u` and `jwk` header parameters of a token are ignored.
8
9use std::fmt;
10use std::sync::{Arc, PoisonError, RwLock};
11use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12
13use base64::Engine as _;
14use base64::engine::general_purpose::URL_SAFE_NO_PAD;
15use bytes::Bytes;
16use http_body_util::{BodyExt as _, Empty, Limited};
17use ring::signature::{self, RsaPublicKeyComponents, UnparsedPublicKey};
18use serde_json::{Map, Value};
19
20use super::{Principal, TokenError, TokenVerifier, VerifyFuture};
21use crate::transport::oauth::canonical_url;
22
23/// Largest metadata or JWKS document accepted from the network.
24const MAX_DOCUMENT_BYTES: usize = 512 * 1024;
25/// Most keys kept from one JWKS.
26const MAX_KEYS: usize = 64;
27/// Default for how often keys may be refetched when tokens name unknown keys.
28const MIN_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
29/// Keys older than this are refreshed on the next verification.
30const MAX_KEY_AGE: Duration = Duration::from_secs(3600);
31
32/// A JWS signature algorithm accepted by [`JwtVerifier`].
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum Algorithm {
36    /// RSASSA-PKCS1-v1_5 with SHA-256 (`RS256`).
37    Rs256,
38    /// RSASSA-PKCS1-v1_5 with SHA-384 (`RS384`).
39    Rs384,
40    /// RSASSA-PKCS1-v1_5 with SHA-512 (`RS512`).
41    Rs512,
42    /// RSASSA-PSS with SHA-256 (`PS256`).
43    Ps256,
44    /// RSASSA-PSS with SHA-384 (`PS384`).
45    Ps384,
46    /// RSASSA-PSS with SHA-512 (`PS512`).
47    Ps512,
48    /// ECDSA on P-256 with SHA-256 (`ES256`).
49    Es256,
50    /// ECDSA on P-384 with SHA-384 (`ES384`).
51    Es384,
52    /// Ed25519 (`EdDSA` or `Ed25519`).
53    EdDsa,
54}
55
56impl Algorithm {
57    /// Every supported algorithm (the default allowlist).
58    pub const ALL: [Self; 9] = [
59        Self::Rs256,
60        Self::Rs384,
61        Self::Rs512,
62        Self::Ps256,
63        Self::Ps384,
64        Self::Ps512,
65        Self::Es256,
66        Self::Es384,
67        Self::EdDsa,
68    ];
69
70    /// The JWS name (`alg`).
71    #[must_use]
72    pub fn name(self) -> &'static str {
73        match self {
74            Self::Rs256 => "RS256",
75            Self::Rs384 => "RS384",
76            Self::Rs512 => "RS512",
77            Self::Ps256 => "PS256",
78            Self::Ps384 => "PS384",
79            Self::Ps512 => "PS512",
80            Self::Es256 => "ES256",
81            Self::Es384 => "ES384",
82            Self::EdDsa => "EdDSA",
83        }
84    }
85
86    fn from_name(name: &str) -> Option<Self> {
87        Some(match name {
88            "RS256" => Self::Rs256,
89            "RS384" => Self::Rs384,
90            "RS512" => Self::Rs512,
91            "PS256" => Self::Ps256,
92            "PS384" => Self::Ps384,
93            "PS512" => Self::Ps512,
94            "ES256" => Self::Es256,
95            "ES384" => Self::Es384,
96            // RFC 9864 adds the fully-specified name.
97            "EdDSA" | "Ed25519" => Self::EdDsa,
98            _ => return None,
99        })
100    }
101}
102
103/// Public key material of one JWK.
104#[derive(Clone, PartialEq, Eq)]
105enum Material {
106    Rsa { n: Vec<u8>, e: Vec<u8> },
107    P256(Vec<u8>),
108    P384(Vec<u8>),
109    Ed25519(Vec<u8>),
110}
111
112#[derive(Clone, PartialEq, Eq)]
113struct Key {
114    kid: Option<String>,
115    alg: Option<Algorithm>,
116    material: Material,
117}
118
119impl fmt::Debug for Key {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        let kind = match self.material {
122            Material::Rsa { .. } => "RSA",
123            Material::P256(_) => "P-256",
124            Material::P384(_) => "P-384",
125            Material::Ed25519(_) => "Ed25519",
126        };
127        f.debug_struct("Key")
128            .field("kid", &self.kid)
129            .field("kind", &kind)
130            .finish_non_exhaustive()
131    }
132}
133
134impl Key {
135    /// Whether this key may verify a signature made with `alg`.
136    fn fits(&self, alg: Algorithm) -> bool {
137        if self.alg.is_some_and(|a| a != alg) {
138            return false;
139        }
140        matches!(
141            (&self.material, alg),
142            (
143                Material::Rsa { .. },
144                Algorithm::Rs256
145                    | Algorithm::Rs384
146                    | Algorithm::Rs512
147                    | Algorithm::Ps256
148                    | Algorithm::Ps384
149                    | Algorithm::Ps512
150            ) | (Material::P256(_), Algorithm::Es256)
151                | (Material::P384(_), Algorithm::Es384)
152                | (Material::Ed25519(_), Algorithm::EdDsa)
153        )
154    }
155
156    fn verify(&self, alg: Algorithm, message: &[u8], sig: &[u8]) -> bool {
157        let rsa = |params: &'static signature::RsaParameters, n: &[u8], e: &[u8]| {
158            RsaPublicKeyComponents { n, e }
159                .verify(params, message, sig)
160                .is_ok()
161        };
162        match (&self.material, alg) {
163            (Material::Rsa { n, e }, alg) => match alg {
164                Algorithm::Rs256 => rsa(&signature::RSA_PKCS1_2048_8192_SHA256, n, e),
165                Algorithm::Rs384 => rsa(&signature::RSA_PKCS1_2048_8192_SHA384, n, e),
166                Algorithm::Rs512 => rsa(&signature::RSA_PKCS1_2048_8192_SHA512, n, e),
167                Algorithm::Ps256 => rsa(&signature::RSA_PSS_2048_8192_SHA256, n, e),
168                Algorithm::Ps384 => rsa(&signature::RSA_PSS_2048_8192_SHA384, n, e),
169                Algorithm::Ps512 => rsa(&signature::RSA_PSS_2048_8192_SHA512, n, e),
170                _ => false,
171            },
172            (Material::P256(point), Algorithm::Es256) => {
173                UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_FIXED, point)
174                    .verify(message, sig)
175                    .is_ok()
176            }
177            (Material::P384(point), Algorithm::Es384) => {
178                UnparsedPublicKey::new(&signature::ECDSA_P384_SHA384_FIXED, point)
179                    .verify(message, sig)
180                    .is_ok()
181            }
182            (Material::Ed25519(x), Algorithm::EdDsa) => {
183                UnparsedPublicKey::new(&signature::ED25519, x)
184                    .verify(message, sig)
185                    .is_ok()
186            }
187            _ => false,
188        }
189    }
190}
191
192fn b64(value: Option<&Value>) -> Option<Vec<u8>> {
193    URL_SAFE_NO_PAD.decode(value?.as_str()?).ok()
194}
195
196/// Parses one JWK; `None` for keys that cannot verify signatures here
197/// (encryption keys, unknown types or algorithms, malformed values).
198fn parse_key(jwk: &Value) -> Option<Key> {
199    let field = |name: &str| jwk.get(name);
200    if field("use").is_some_and(|u| u.as_str() != Some("sig")) {
201        return None;
202    }
203    let alg = match field("alg") {
204        None => None,
205        Some(alg) => Some(Algorithm::from_name(alg.as_str()?)?),
206    };
207    let material = match field("kty")?.as_str()? {
208        "RSA" => {
209            let n = b64(field("n"))?;
210            let e = b64(field("e"))?;
211            // At least 2048 bits (leading zero bytes aside).
212            let bits = n.iter().skip_while(|b| **b == 0).count() * 8;
213            if bits < 2048 || e.is_empty() {
214                return None;
215            }
216            Material::Rsa { n, e }
217        }
218        "EC" => {
219            let x = b64(field("x"))?;
220            let y = b64(field("y"))?;
221            let size = match field("crv")?.as_str()? {
222                "P-256" => 32,
223                "P-384" => 48,
224                _ => return None,
225            };
226            if x.len() != size || y.len() != size {
227                return None;
228            }
229            let mut point = Vec::with_capacity(1 + 2 * size);
230            point.push(0x04);
231            point.extend_from_slice(&x);
232            point.extend_from_slice(&y);
233            if size == 32 {
234                Material::P256(point)
235            } else {
236                Material::P384(point)
237            }
238        }
239        "OKP" => {
240            if field("crv")?.as_str()? != "Ed25519" {
241                return None;
242            }
243            let x = b64(field("x"))?;
244            if x.len() != 32 {
245                return None;
246            }
247            Material::Ed25519(x)
248        }
249        _ => return None,
250    };
251    Some(Key {
252        kid: field("kid").and_then(Value::as_str).map(str::to_owned),
253        alg,
254        material,
255    })
256}
257
258/// The usable signing keys of a JWKS document.
259fn parse_jwks(document: &Value) -> Vec<Key> {
260    document
261        .get("keys")
262        .and_then(Value::as_array)
263        .map(|keys| keys.iter().filter_map(parse_key).take(MAX_KEYS).collect())
264        .unwrap_or_default()
265}
266
267/// A token split into its parts, with the header checked.
268struct Parsed<'a> {
269    alg: Algorithm,
270    kid: Option<String>,
271    signing_input: &'a str,
272    signature: Vec<u8>,
273    payload: &'a str,
274}
275
276fn parse_token<'a>(token: &'a str, allowed: &[Algorithm]) -> Result<Parsed<'a>, TokenError> {
277    let malformed = || TokenError::new("malformed JWT");
278    let (signing_input, signature) = token.rsplit_once('.').ok_or_else(malformed)?;
279    let (header, payload) = signing_input.split_once('.').ok_or_else(malformed)?;
280    if payload.contains('.') {
281        // Five parts: an encrypted token (JWE).
282        return Err(TokenError::new("encrypted tokens are not supported"));
283    }
284    let header: Map<String, Value> = URL_SAFE_NO_PAD
285        .decode(header)
286        .ok()
287        .and_then(|bytes| serde_json::from_slice(&bytes).ok())
288        .ok_or_else(malformed)?;
289    if header.contains_key("crit") {
290        // RFC 7515 §4.1.11: unknown critical extensions must be rejected.
291        return Err(TokenError::new(
292            "critical header parameters are not supported",
293        ));
294    }
295    let name = header
296        .get("alg")
297        .and_then(Value::as_str)
298        .ok_or_else(malformed)?;
299    let alg = Algorithm::from_name(name)
300        .filter(|alg| allowed.contains(alg))
301        .ok_or_else(|| TokenError::new(format!("algorithm not allowed: {name:.16}")))?;
302    let signature = URL_SAFE_NO_PAD.decode(signature).map_err(|_| malformed())?;
303    Ok(Parsed {
304        alg,
305        kid: header.get("kid").and_then(Value::as_str).map(str::to_owned),
306        signing_input,
307        signature,
308        payload,
309    })
310}
311
312/// Where the keys come from.
313enum Source {
314    /// Keys given at build time.
315    Static,
316    /// Keys fetched over HTTPS; `jwks_uri` is discovered when not given.
317    Remote(Fetcher),
318}
319
320struct RefreshState {
321    last_attempt: Option<Instant>,
322    jwks_uri: Option<String>,
323}
324
325struct Inner {
326    issuer: String,
327    audience: String,
328    algorithms: Vec<Algorithm>,
329    leeway: u64,
330    min_refresh: Duration,
331    keys: RwLock<(Arc<[Key]>, Option<Instant>)>,
332    source: Source,
333    refresh: tokio::sync::Mutex<RefreshState>,
334}
335
336/// Verifies JWT access tokens: signature (with the issuer's published
337/// keys), issuer, audience, expiry and not-before.
338///
339/// The token's `sub` becomes the [`Principal`] subject, and its `scope`
340/// (space-separated) or `scp` claim the granted scopes.
341///
342/// ```no_run
343/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
344/// use flymcprust_core::auth::JwtVerifier;
345/// use flymcprust_core::transport::http::OAuth;
346///
347/// let resource = "https://mcp.example.com/mcp";
348/// let verifier = JwtVerifier::builder("https://auth.example.com", resource).build()?;
349/// let oauth = OAuth::builder(resource, verifier)
350///     .authorization_server("https://auth.example.com")
351///     .build()?;
352/// # Ok(()) }
353/// ```
354#[derive(Clone)]
355pub struct JwtVerifier {
356    inner: Arc<Inner>,
357}
358
359impl fmt::Debug for JwtVerifier {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        f.debug_struct("JwtVerifier")
362            .field("issuer", &self.inner.issuer)
363            .field("audience", &self.inner.audience)
364            .field("algorithms", &self.inner.algorithms)
365            .finish_non_exhaustive()
366    }
367}
368
369impl JwtVerifier {
370    /// Starts a verifier for tokens issued by `issuer` (exactly as in the
371    /// tokens' `iss` claim) for `audience` (this server's canonical URI,
372    /// the `resource` of [`OAuth`](crate::transport::http::OAuth)).
373    ///
374    /// By default the signing keys are discovered from the issuer's
375    /// metadata (RFC 8414, then OpenID Connect Discovery).
376    #[must_use]
377    pub fn builder(issuer: impl Into<String>, audience: impl Into<String>) -> JwtVerifierBuilder {
378        JwtVerifierBuilder {
379            issuer: issuer.into(),
380            audience: audience.into(),
381            jwks_uri: None,
382            jwks: None,
383            algorithms: Algorithm::ALL.to_vec(),
384            leeway: Duration::from_secs(60),
385            http_timeout: Duration::from_secs(5),
386            min_refresh: MIN_REFRESH_INTERVAL,
387        }
388    }
389
390    async fn verify_token(&self, token: &str) -> Result<Principal, TokenError> {
391        let inner = &self.inner;
392        let parsed = parse_token(token, &inner.algorithms)?;
393
394        let mut keys = self.keys(false).await;
395        if candidates(&keys, &parsed).is_empty() && matches!(inner.source, Source::Remote(_)) {
396            // Perhaps the issuer rotated its keys.
397            keys = self.keys(true).await;
398        }
399        let candidates = candidates(&keys, &parsed);
400        if candidates.is_empty() {
401            return Err(TokenError::new("no key matches the token"));
402        }
403        let message = parsed.signing_input.as_bytes();
404        if !candidates
405            .iter()
406            .any(|key| key.verify(parsed.alg, message, &parsed.signature))
407        {
408            return Err(TokenError::new("bad signature"));
409        }
410
411        let claims: Map<String, Value> = URL_SAFE_NO_PAD
412            .decode(parsed.payload)
413            .ok()
414            .and_then(|bytes| serde_json::from_slice(&bytes).ok())
415            .ok_or_else(|| TokenError::new("malformed claims"))?;
416        self.check_claims(claims)
417    }
418
419    fn check_claims(&self, claims: Map<String, Value>) -> Result<Principal, TokenError> {
420        let inner = &self.inner;
421        if claims.get("iss").and_then(Value::as_str) != Some(inner.issuer.as_str()) {
422            return Err(TokenError::new("wrong issuer"));
423        }
424        let audience_ok = match claims.get("aud") {
425            Some(Value::String(aud)) => *aud == inner.audience,
426            Some(Value::Array(auds)) => auds
427                .iter()
428                .any(|a| a.as_str() == Some(inner.audience.as_str())),
429            _ => false,
430        };
431        if !audience_ok {
432            return Err(TokenError::new("wrong audience"));
433        }
434        let now = SystemTime::now()
435            .duration_since(UNIX_EPOCH)
436            .map_or(0, |d| d.as_secs());
437        let time = |name: &str| -> Result<Option<u64>, TokenError> {
438            match claims.get(name) {
439                None => Ok(None),
440                Some(value) => value
441                    .as_f64()
442                    .filter(|t| t.is_finite() && *t >= 0.0)
443                    // Whole seconds; fractions only matter within the leeway.
444                    .map(|t| Some(t as u64))
445                    .ok_or_else(|| TokenError::new(format!("malformed `{name}`"))),
446            }
447        };
448        let exp = time("exp")?.ok_or_else(|| TokenError::new("missing `exp`"))?;
449        if now >= exp.saturating_add(inner.leeway) {
450            return Err(TokenError::new("expired"));
451        }
452        if time("nbf")?.is_some_and(|nbf| now.saturating_add(inner.leeway) < nbf) {
453            return Err(TokenError::new("not yet valid"));
454        }
455        let subject = claims
456            .get("sub")
457            .and_then(Value::as_str)
458            .filter(|s| !s.is_empty())
459            .ok_or_else(|| TokenError::new("missing `sub`"))?
460            .to_owned();
461
462        let mut scopes: Vec<String> = Vec::new();
463        if let Some(scope) = claims.get("scope").and_then(Value::as_str) {
464            scopes.extend(scope.split_whitespace().map(str::to_owned));
465        }
466        match claims.get("scp") {
467            Some(Value::String(scp)) => scopes.extend(scp.split_whitespace().map(str::to_owned)),
468            Some(Value::Array(scp)) => {
469                scopes.extend(scp.iter().filter_map(Value::as_str).map(str::to_owned));
470            }
471            _ => {}
472        }
473        Ok(Principal::new(subject)
474            .with_issuer(inner.issuer.clone())
475            .with_scopes(scopes)
476            .with_claims(claims))
477    }
478
479    /// The current keys, refreshed when stale or (`force`) when a token
480    /// names an unknown key. Fetch failures keep the previous keys.
481    async fn keys(&self, force: bool) -> Arc<[Key]> {
482        let inner = &self.inner;
483        let current = || {
484            inner
485                .keys
486                .read()
487                .unwrap_or_else(PoisonError::into_inner)
488                .clone()
489        };
490        let (keys, fetched) = current();
491        let Source::Remote(fetcher) = &inner.source else {
492            return keys;
493        };
494        let stale = fetched.is_none_or(|at| at.elapsed() >= MAX_KEY_AGE);
495        if !stale && !force {
496            return keys;
497        }
498
499        // One fetch at a time. A routine refresh does not wait for one in
500        // progress (the current keys still work); a token naming an unknown
501        // key waits for its result.
502        let mut state = match inner.refresh.try_lock() {
503            Ok(state) => state,
504            Err(_) if !force && !keys.is_empty() => return keys,
505            Err(_) => inner.refresh.lock().await,
506        };
507        let (keys, fetched) = current();
508        let fresh_enough = fetched.is_some_and(|at| at.elapsed() < MAX_KEY_AGE);
509        if (fresh_enough && !force)
510            || state
511                .last_attempt
512                .is_some_and(|at| at.elapsed() < inner.min_refresh)
513        {
514            return keys;
515        }
516        state.last_attempt = Some(Instant::now());
517        match self.fetch_keys(fetcher, &mut state).await {
518            Ok(new_keys) => {
519                let new_keys: Arc<[Key]> = new_keys.into();
520                *inner.keys.write().unwrap_or_else(PoisonError::into_inner) =
521                    (Arc::clone(&new_keys), Some(Instant::now()));
522                new_keys
523            }
524            Err(error) => {
525                tracing::warn!(
526                    issuer = %inner.issuer,
527                    "could not fetch the issuer's signing keys: {error}\n  = help: check that \
528                     the authorization server is reachable and serves its metadata and JWKS; \
529                     or set JwtVerifierBuilder::jwks_uri(..)"
530                );
531                keys
532            }
533        }
534    }
535
536    async fn fetch_keys(
537        &self,
538        fetcher: &Fetcher,
539        state: &mut RefreshState,
540    ) -> Result<Vec<Key>, String> {
541        let jwks_uri = match &state.jwks_uri {
542            Some(uri) => uri.clone(),
543            None => {
544                let uri = discover_jwks_uri(fetcher, &self.inner.issuer).await?;
545                state.jwks_uri = Some(uri.clone());
546                uri
547            }
548        };
549        let document = fetcher.get_json(&jwks_uri).await?;
550        let keys = parse_jwks(&document);
551        if keys.is_empty() {
552            return Err(format!("{jwks_uri} has no usable signing keys"));
553        }
554        Ok(keys)
555    }
556}
557
558impl TokenVerifier for JwtVerifier {
559    fn verify<'a>(&'a self, token: &'a str) -> VerifyFuture<'a> {
560        Box::pin(self.verify_token(token))
561    }
562}
563
564fn candidates<'k>(keys: &'k [Key], token: &Parsed<'_>) -> Vec<&'k Key> {
565    keys.iter()
566        .filter(|key| key.fits(token.alg))
567        .filter(|key| match &token.kid {
568            Some(kid) => key.kid.as_deref() == Some(kid.as_str()),
569            None => true,
570        })
571        .collect()
572}
573
574/// The metadata URLs to try for an issuer, in the order the MCP
575/// specification gives (RFC 8414 first, then OpenID Connect Discovery).
576fn metadata_urls(issuer: &str) -> Vec<String> {
577    let after_scheme = issuer.find("://").map_or(0, |i| i + 3);
578    let path_start = issuer
579        .get(after_scheme..)
580        .and_then(|rest| rest.find('/'))
581        .map_or(issuer.len(), |i| i + after_scheme);
582    let (origin, path) = issuer.split_at(path_start);
583    let path = path.trim_end_matches('/');
584    if path.is_empty() {
585        vec![
586            format!("{origin}/.well-known/oauth-authorization-server"),
587            format!("{origin}/.well-known/openid-configuration"),
588        ]
589    } else {
590        vec![
591            format!("{origin}/.well-known/oauth-authorization-server{path}"),
592            format!("{origin}/.well-known/openid-configuration{path}"),
593            format!("{origin}{path}/.well-known/openid-configuration"),
594        ]
595    }
596}
597
598async fn discover_jwks_uri(fetcher: &Fetcher, issuer: &str) -> Result<String, String> {
599    let mut last_error = String::from("no metadata URL");
600    for url in metadata_urls(issuer) {
601        let metadata = match fetcher.get_json(&url).await {
602            Ok(metadata) => metadata,
603            Err(error) => {
604                last_error = error;
605                continue;
606            }
607        };
608        // RFC 8414 §3.3: the document must name the issuer it was fetched for.
609        if metadata.get("issuer").and_then(Value::as_str) != Some(issuer) {
610            return Err(format!("{url} names another issuer"));
611        }
612        let uri = metadata
613            .get("jwks_uri")
614            .and_then(Value::as_str)
615            .ok_or_else(|| format!("{url} has no `jwks_uri`"))?;
616        canonical_url(uri).map_err(|reason| format!("invalid `jwks_uri` {uri}: {reason}"))?;
617        if issuer.starts_with("https://") && !uri.starts_with("https://") {
618            return Err(format!("{url} names a `jwks_uri` without https"));
619        }
620        return Ok(uri.to_owned());
621    }
622    Err(last_error)
623}
624
625type Client = hyper_util::client::legacy::Client<
626    hyper_rustls::HttpsConnector<hyper_util::client::legacy::connect::HttpConnector>,
627    Empty<Bytes>,
628>;
629
630/// Fetches JSON documents over HTTPS (HTTP only on localhost), without
631/// following redirects, with a size limit and a timeout.
632struct Fetcher {
633    client: Client,
634    timeout: Duration,
635}
636
637impl Fetcher {
638    fn new(timeout: Duration) -> Result<Self, JwtError> {
639        let provider = Arc::new(rustls::crypto::ring::default_provider());
640        let connector = hyper_rustls::HttpsConnectorBuilder::new()
641            .with_provider_and_native_roots(provider)
642            .map_err(|error| JwtError::Tls(error.to_string()))?
643            .https_or_http()
644            .enable_http1()
645            .build();
646        let client =
647            hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
648                .build(connector);
649        Ok(Self { client, timeout })
650    }
651
652    async fn get_json(&self, url: &str) -> Result<Value, String> {
653        // Plain HTTP only for localhost (canonical_url enforces it).
654        canonical_url(url).map_err(|reason| format!("{url}: {reason}"))?;
655        let request = http::Request::get(url)
656            .header(http::header::ACCEPT, "application/json")
657            .header(http::header::USER_AGENT, "flymcprust")
658            .body(Empty::new())
659            .map_err(|error| format!("{url}: {error}"))?;
660        let fetch = async {
661            let response = self
662                .client
663                .request(request)
664                .await
665                .map_err(|error| format!("{url}: {error}"))?;
666            if response.status() != http::StatusCode::OK {
667                return Err(format!("{url}: HTTP {}", response.status()));
668            }
669            let body = Limited::new(response.into_body(), MAX_DOCUMENT_BYTES)
670                .collect()
671                .await
672                .map_err(|error| format!("{url}: {error}"))?
673                .to_bytes();
674            serde_json::from_slice(&body).map_err(|error| format!("{url}: invalid JSON: {error}"))
675        };
676        tokio::time::timeout(self.timeout, fetch)
677            .await
678            .map_err(|_| format!("{url}: no answer within {:?}", self.timeout))?
679    }
680}
681
682/// Builder of [`JwtVerifier`].
683#[derive(Debug)]
684pub struct JwtVerifierBuilder {
685    issuer: String,
686    audience: String,
687    jwks_uri: Option<String>,
688    jwks: Option<String>,
689    algorithms: Vec<Algorithm>,
690    leeway: Duration,
691    http_timeout: Duration,
692    min_refresh: Duration,
693}
694
695impl JwtVerifierBuilder {
696    /// Fetches keys from this URL instead of discovering it from the
697    /// issuer's metadata.
698    #[must_use]
699    pub fn jwks_uri(mut self, uri: impl Into<String>) -> Self {
700        self.jwks_uri = Some(uri.into());
701        self
702    }
703
704    /// Uses these keys (a JWKS document, `{"keys": [..]}`) and never fetches
705    /// any. Rotating them then requires a restart.
706    #[must_use]
707    pub fn jwks(mut self, document: impl Into<String>) -> Self {
708        self.jwks = Some(document.into());
709        self
710    }
711
712    /// The accepted signature algorithms. Default: all of [`Algorithm::ALL`].
713    #[must_use]
714    pub fn algorithms(mut self, algorithms: impl IntoIterator<Item = Algorithm>) -> Self {
715        self.algorithms = algorithms.into_iter().collect();
716        self
717    }
718
719    /// Tolerated clock difference for `exp` and `nbf`. Default: 60 s.
720    #[must_use]
721    pub fn leeway(mut self, leeway: Duration) -> Self {
722        self.leeway = leeway;
723        self
724    }
725
726    /// Time allowed for each metadata or JWKS request. Default: 5 s.
727    #[must_use]
728    pub fn http_timeout(mut self, timeout: Duration) -> Self {
729        self.http_timeout = timeout;
730        self
731    }
732
733    /// Minimum time between two key fetches triggered by tokens that name
734    /// unknown keys, so forged tokens cannot make the server hammer the
735    /// authorization server. Default: 30 s.
736    #[must_use]
737    pub fn min_refresh_interval(mut self, interval: Duration) -> Self {
738        self.min_refresh = interval;
739        self
740    }
741
742    /// Validates the settings.
743    ///
744    /// # Errors
745    ///
746    /// Fails when the issuer or `jwks_uri` is not a valid URL (`https`, or
747    /// `http` on localhost), the audience is empty, no algorithm is allowed,
748    /// static keys contain no usable signing key, or the system's TLS roots
749    /// cannot be loaded.
750    pub fn build(self) -> Result<JwtVerifier, JwtError> {
751        canonical_url(&self.issuer).map_err(|reason| JwtError::InvalidIssuer {
752            issuer: self.issuer.clone(),
753            reason,
754        })?;
755        if self.audience.trim().is_empty() {
756            return Err(JwtError::EmptyAudience);
757        }
758        if self.algorithms.is_empty() {
759            return Err(JwtError::NoAlgorithms);
760        }
761        if self.http_timeout.is_zero() {
762            return Err(JwtError::InvalidTimeout);
763        }
764        let (keys, source, jwks_uri) = match self.jwks {
765            Some(document) => {
766                let document: Value = serde_json::from_str(&document)
767                    .map_err(|error| JwtError::InvalidJwks(error.to_string()))?;
768                let keys = parse_jwks(&document);
769                if keys.is_empty() {
770                    return Err(JwtError::InvalidJwks("no usable signing key".to_owned()));
771                }
772                (keys, Source::Static, None)
773            }
774            None => {
775                if let Some(uri) = &self.jwks_uri {
776                    canonical_url(uri).map_err(|reason| JwtError::InvalidJwksUri {
777                        uri: uri.clone(),
778                        reason,
779                    })?;
780                }
781                (
782                    Vec::new(),
783                    Source::Remote(Fetcher::new(self.http_timeout)?),
784                    self.jwks_uri,
785                )
786            }
787        };
788        let fetched = matches!(source, Source::Static).then(Instant::now);
789        Ok(JwtVerifier {
790            inner: Arc::new(Inner {
791                issuer: self.issuer,
792                audience: self.audience,
793                algorithms: self.algorithms,
794                leeway: self.leeway.as_secs(),
795                min_refresh: self.min_refresh,
796                keys: RwLock::new((keys.into(), fetched)),
797                source,
798                refresh: tokio::sync::Mutex::new(RefreshState {
799                    last_attempt: None,
800                    jwks_uri,
801                }),
802            }),
803        })
804    }
805}
806
807/// Invalid [`JwtVerifier`] settings.
808#[derive(Clone, PartialEq, Eq, thiserror::Error)]
809#[non_exhaustive]
810pub enum JwtError {
811    /// The issuer is not a valid URL.
812    #[error("invalid issuer `{issuer}`: {reason}")]
813    InvalidIssuer {
814        /// The configured value.
815        issuer: String,
816        /// What is wrong with it.
817        reason: String,
818    },
819    /// The JWKS URL is invalid.
820    #[error("invalid jwks_uri `{uri}`: {reason}")]
821    InvalidJwksUri {
822        /// The configured value.
823        uri: String,
824        /// What is wrong with it.
825        reason: String,
826    },
827    /// The audience is empty.
828    #[error("empty audience")]
829    EmptyAudience,
830    /// No algorithm is allowed.
831    #[error("no signature algorithm is allowed")]
832    NoAlgorithms,
833    /// The static JWKS is invalid.
834    #[error("invalid JWKS: {0}")]
835    InvalidJwks(String),
836    /// The HTTP timeout is zero.
837    #[error("the HTTP timeout must be positive")]
838    InvalidTimeout,
839    /// The system's TLS root certificates could not be loaded.
840    #[error("could not load the system's TLS roots: {0}")]
841    Tls(String),
842}
843
844impl JwtError {
845    /// How to fix the error.
846    #[must_use]
847    pub fn help(&self) -> &'static str {
848        match self {
849            Self::InvalidIssuer { .. } => {
850                "use the issuer exactly as it appears in the tokens' `iss` claim, such as \
851                 `https://auth.example.com` (https; http only on localhost)"
852            }
853            Self::InvalidJwksUri { .. } => {
854                "use the authorization server's JWKS URL (https; http only on localhost)"
855            }
856            Self::EmptyAudience => {
857                "use this server's canonical URI, the same value given to OAuth::builder"
858            }
859            Self::NoAlgorithms => "allow at least one algorithm, or keep the default",
860            Self::InvalidJwks(_) => {
861                "pass a JWKS document such as {\"keys\": [{\"kty\": \"RSA\", \"n\": .., \"e\": ..}]} \
862                 with signing keys (RSA of at least 2048 bits, P-256, P-384 or Ed25519)"
863            }
864            Self::InvalidTimeout => "use a timeout such as `Duration::from_secs(5)`",
865            Self::Tls(_) => {
866                "install the operating system's CA certificates (for example the \
867                 `ca-certificates` package), or pass static keys with `.jwks(..)`"
868            }
869        }
870    }
871}
872
873impl fmt::Debug for JwtError {
874    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
875        write!(f, "{self}\n  = help: {}", self.help())
876    }
877}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882    use ring::rand::SystemRandom;
883    use ring::signature::{EcdsaKeyPair, Ed25519KeyPair, KeyPair as _};
884    use serde_json::json;
885
886    const ISSUER: &str = "https://auth.example.com";
887    const AUDIENCE: &str = "https://mcp.example.com/mcp";
888
889    fn enc(value: &Value) -> String {
890        URL_SAFE_NO_PAD.encode(serde_json::to_vec(value).unwrap())
891    }
892
893    fn now() -> u64 {
894        SystemTime::now()
895            .duration_since(UNIX_EPOCH)
896            .unwrap()
897            .as_secs()
898    }
899
900    fn claims() -> Value {
901        json!({ "iss": ISSUER, "aud": AUDIENCE, "sub": "alice",
902                "exp": now() + 300, "scope": "mcp files:read" })
903    }
904
905    struct Signer {
906        ed: Ed25519KeyPair,
907        ec: EcdsaKeyPair,
908    }
909
910    impl Signer {
911        fn new() -> Self {
912            let rng = SystemRandom::new();
913            let ed = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
914            let ec =
915                EcdsaKeyPair::generate_pkcs8(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
916                    .unwrap();
917            Self {
918                ed: Ed25519KeyPair::from_pkcs8(ed.as_ref()).unwrap(),
919                ec: EcdsaKeyPair::from_pkcs8(
920                    &signature::ECDSA_P256_SHA256_FIXED_SIGNING,
921                    ec.as_ref(),
922                    &rng,
923                )
924                .unwrap(),
925            }
926        }
927
928        fn jwks(&self) -> String {
929            let point = self.ec.public_key().as_ref();
930            json!({ "keys": [
931                { "kty": "OKP", "crv": "Ed25519", "kid": "ed",
932                  "x": URL_SAFE_NO_PAD.encode(self.ed.public_key().as_ref()) },
933                { "kty": "EC", "crv": "P-256", "kid": "ec", "use": "sig",
934                  "x": URL_SAFE_NO_PAD.encode(&point[1..33]),
935                  "y": URL_SAFE_NO_PAD.encode(&point[33..65]) },
936                { "kty": "EC", "crv": "P-256", "kid": "enc", "use": "enc",
937                  "x": URL_SAFE_NO_PAD.encode(&point[1..33]),
938                  "y": URL_SAFE_NO_PAD.encode(&point[33..65]) },
939            ] })
940            .to_string()
941        }
942
943        fn sign(&self, header: &Value, claims: &Value) -> String {
944            let input = format!("{}.{}", enc(header), enc(claims));
945            let sig = match header["alg"].as_str().unwrap() {
946                "EdDSA" => self.ed.sign(input.as_bytes()).as_ref().to_vec(),
947                "ES256" => self
948                    .ec
949                    .sign(&SystemRandom::new(), input.as_bytes())
950                    .unwrap()
951                    .as_ref()
952                    .to_vec(),
953                _ => b"forged".to_vec(),
954            };
955            format!("{input}.{}", URL_SAFE_NO_PAD.encode(sig))
956        }
957    }
958
959    fn verifier(signer: &Signer) -> JwtVerifier {
960        JwtVerifier::builder(ISSUER, AUDIENCE)
961            .jwks(signer.jwks())
962            .build()
963            .unwrap()
964    }
965
966    async fn check(v: &JwtVerifier, token: &str) -> Result<Principal, String> {
967        v.verify(token).await.map_err(|e| e.reason().to_owned())
968    }
969
970    #[tokio::test]
971    async fn valid_tokens_become_principals() {
972        let signer = Signer::new();
973        let v = verifier(&signer);
974        for header in [
975            json!({ "alg": "EdDSA", "kid": "ed", "typ": "at+jwt" }),
976            json!({ "alg": "ES256", "kid": "ec" }),
977            // No `kid`: any fitting key is tried.
978            json!({ "alg": "ES256" }),
979        ] {
980            let principal = check(&v, &signer.sign(&header, &claims())).await.unwrap();
981            assert_eq!(principal.subject(), "alice");
982            assert_eq!(principal.issuer(), Some(ISSUER));
983            assert!(principal.has_scope("files:read"));
984            assert_eq!(principal.claims()["aud"], AUDIENCE);
985        }
986        // `scp` arrays (Entra ID) and audience arrays.
987        let mut c = claims();
988        c["scp"] = json!(["a", "b"]);
989        c["aud"] = json!(["other", AUDIENCE]);
990        let p = check(&v, &signer.sign(&json!({ "alg": "EdDSA" }), &c))
991            .await
992            .unwrap();
993        assert!(p.has_scope("a") && p.has_scope("mcp"));
994    }
995
996    #[tokio::test]
997    async fn bad_tokens_are_rejected() {
998        let signer = Signer::new();
999        let v = verifier(&signer);
1000        let ed = json!({ "alg": "EdDSA", "kid": "ed" });
1001        let with = |name: &str, value: Value| {
1002            let mut c = claims();
1003            c[name] = value;
1004            signer.sign(&ed, &c)
1005        };
1006        let cases = [
1007            (
1008                with("iss", json!("https://evil.example.com")),
1009                "wrong issuer",
1010            ),
1011            (
1012                with("aud", json!("https://other.example.com")),
1013                "wrong audience",
1014            ),
1015            (with("exp", json!(now() - 120)), "expired"),
1016            (with("nbf", json!(now() + 600)), "not yet valid"),
1017            (with("sub", json!("")), "missing `sub`"),
1018            (with("exp", json!("tomorrow")), "malformed `exp`"),
1019            (
1020                signer.sign(&json!({ "alg": "none" }), &claims()),
1021                "algorithm not allowed: none",
1022            ),
1023            (
1024                signer.sign(&json!({ "alg": "HS256" }), &claims()),
1025                "algorithm not allowed: HS256",
1026            ),
1027            (
1028                signer.sign(&json!({ "alg": "EdDSA", "kid": "missing" }), &claims()),
1029                "no key matches the token",
1030            ),
1031            // The encryption-only key is never used.
1032            (
1033                signer.sign(&json!({ "alg": "ES256", "kid": "enc" }), &claims()),
1034                "no key matches the token",
1035            ),
1036            (
1037                signer.sign(&json!({ "alg": "EdDSA", "crit": ["exp"] }), &claims()),
1038                "critical header parameters are not supported",
1039            ),
1040            ("a.b".to_owned(), "malformed JWT"),
1041            ("a.b.c.d.e".to_owned(), "encrypted tokens are not supported"),
1042        ];
1043        for (token, reason) in cases {
1044            assert_eq!(check(&v, &token).await.unwrap_err(), reason, "{token}");
1045        }
1046
1047        // A valid token with a tampered payload.
1048        let token = signer.sign(&ed, &claims());
1049        let mut parts: Vec<&str> = token.split('.').collect();
1050        let mut forged = claims();
1051        forged["sub"] = json!("admin");
1052        let forged = enc(&forged);
1053        parts[1] = &forged;
1054        assert_eq!(
1055            check(&v, &parts.join(".")).await.unwrap_err(),
1056            "bad signature"
1057        );
1058
1059        // An ES256 signature presented as EdDSA (algorithm confusion).
1060        let es = signer.sign(&json!({ "alg": "ES256", "kid": "ec" }), &claims());
1061        let sig = es.rsplit_once('.').unwrap().1;
1062        let input = format!("{}.{}", enc(&json!({ "alg": "EdDSA" })), enc(&claims()));
1063        assert_eq!(
1064            check(&v, &format!("{input}.{sig}")).await.unwrap_err(),
1065            "bad signature"
1066        );
1067    }
1068
1069    #[tokio::test]
1070    async fn the_allowlist_is_enforced() {
1071        let signer = Signer::new();
1072        let v = JwtVerifier::builder(ISSUER, AUDIENCE)
1073            .jwks(signer.jwks())
1074            .algorithms([Algorithm::Es256])
1075            .build()
1076            .unwrap();
1077        let token = signer.sign(&json!({ "alg": "EdDSA" }), &claims());
1078        assert_eq!(
1079            check(&v, &token).await.unwrap_err(),
1080            "algorithm not allowed: EdDSA"
1081        );
1082    }
1083
1084    #[test]
1085    fn weak_and_foreign_keys_are_ignored() {
1086        let short_rsa =
1087            json!({ "kty": "RSA", "n": URL_SAFE_NO_PAD.encode([1u8; 128]), "e": "AQAB" });
1088        let rsa = json!({ "kty": "RSA", "n": URL_SAFE_NO_PAD.encode([1u8; 256]), "e": "AQAB" });
1089        let hmac = json!({ "kty": "oct", "k": "c2VjcmV0" });
1090        let x448 = json!({ "kty": "OKP", "crv": "X448", "x": "AA" });
1091        let unknown_alg = json!({ "kty": "RSA", "alg": "RS1", "n": URL_SAFE_NO_PAD.encode([1u8; 256]), "e": "AQAB" });
1092        let keys = parse_jwks(&json!({ "keys": [short_rsa, rsa, hmac, x448, unknown_alg] }));
1093        assert_eq!(keys.len(), 1);
1094        assert!(keys[0].fits(Algorithm::Ps256));
1095        assert!(!keys[0].fits(Algorithm::Es256));
1096    }
1097
1098    #[test]
1099    fn settings_are_validated() {
1100        let build = |issuer: &str, audience: &str| {
1101            JwtVerifier::builder(issuer, audience)
1102                .jwks(r#"{"keys":[]}"#)
1103                .build()
1104        };
1105        assert!(matches!(
1106            build("http://auth.example.com", AUDIENCE),
1107            Err(JwtError::InvalidIssuer { .. })
1108        ));
1109        assert_eq!(build(ISSUER, " ").unwrap_err(), JwtError::EmptyAudience);
1110        assert!(matches!(
1111            build(ISSUER, AUDIENCE),
1112            Err(JwtError::InvalidJwks(_))
1113        ));
1114        assert_eq!(
1115            JwtVerifier::builder(ISSUER, AUDIENCE)
1116                .algorithms([])
1117                .build()
1118                .unwrap_err(),
1119            JwtError::NoAlgorithms
1120        );
1121    }
1122
1123    #[test]
1124    fn rfc_example_keys_parse() {
1125        // The keys of the `verify_jwt` fuzz target.
1126        let keys = parse_jwks(&json!({ "keys": [
1127            { "kty": "OKP", "crv": "Ed25519", "kid": "ed",
1128              "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" },
1129            { "kty": "EC", "crv": "P-256", "kid": "ec",
1130              "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
1131              "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0" },
1132        ] }));
1133        assert_eq!(keys.len(), 2);
1134    }
1135
1136    #[test]
1137    fn discovery_urls_follow_the_specification() {
1138        assert_eq!(
1139            metadata_urls("https://auth.example.com"),
1140            [
1141                "https://auth.example.com/.well-known/oauth-authorization-server",
1142                "https://auth.example.com/.well-known/openid-configuration",
1143            ]
1144        );
1145        assert_eq!(
1146            metadata_urls("https://auth.example.com/tenant1"),
1147            [
1148                "https://auth.example.com/.well-known/oauth-authorization-server/tenant1",
1149                "https://auth.example.com/.well-known/openid-configuration/tenant1",
1150                "https://auth.example.com/tenant1/.well-known/openid-configuration",
1151            ]
1152        );
1153    }
1154}