1use 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
23const MAX_DOCUMENT_BYTES: usize = 512 * 1024;
25const MAX_KEYS: usize = 64;
27const MIN_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
29const MAX_KEY_AGE: Duration = Duration::from_secs(3600);
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34#[non_exhaustive]
35pub enum Algorithm {
36 Rs256,
38 Rs384,
40 Rs512,
42 Ps256,
44 Ps384,
46 Ps512,
48 Es256,
50 Es384,
52 EdDsa,
54}
55
56impl Algorithm {
57 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 #[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 "EdDSA" | "Ed25519" => Self::EdDsa,
98 _ => return None,
99 })
100 }
101}
102
103#[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 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
196fn 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 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
258fn 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
267struct 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 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 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
312enum Source {
314 Static,
316 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#[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 #[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 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 .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 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 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
574fn 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 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
630struct 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 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#[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 #[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 #[must_use]
707 pub fn jwks(mut self, document: impl Into<String>) -> Self {
708 self.jwks = Some(document.into());
709 self
710 }
711
712 #[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 #[must_use]
721 pub fn leeway(mut self, leeway: Duration) -> Self {
722 self.leeway = leeway;
723 self
724 }
725
726 #[must_use]
728 pub fn http_timeout(mut self, timeout: Duration) -> Self {
729 self.http_timeout = timeout;
730 self
731 }
732
733 #[must_use]
737 pub fn min_refresh_interval(mut self, interval: Duration) -> Self {
738 self.min_refresh = interval;
739 self
740 }
741
742 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#[derive(Clone, PartialEq, Eq, thiserror::Error)]
809#[non_exhaustive]
810pub enum JwtError {
811 #[error("invalid issuer `{issuer}`: {reason}")]
813 InvalidIssuer {
814 issuer: String,
816 reason: String,
818 },
819 #[error("invalid jwks_uri `{uri}`: {reason}")]
821 InvalidJwksUri {
822 uri: String,
824 reason: String,
826 },
827 #[error("empty audience")]
829 EmptyAudience,
830 #[error("no signature algorithm is allowed")]
832 NoAlgorithms,
833 #[error("invalid JWKS: {0}")]
835 InvalidJwks(String),
836 #[error("the HTTP timeout must be positive")]
838 InvalidTimeout,
839 #[error("could not load the system's TLS roots: {0}")]
841 Tls(String),
842}
843
844impl JwtError {
845 #[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 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 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 (
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 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 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 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}