1use std::collections::HashSet;
39use std::future::Future;
40use std::pin::Pin;
41use std::sync::{Arc, RwLock};
42use std::time::{Duration, SystemTime, UNIX_EPOCH};
43
44use base64::engine::general_purpose::URL_SAFE_NO_PAD;
45use base64::Engine;
46use ring::signature;
47
48use a2a_protocol_types::error::{A2aError, A2aResult};
49
50use super::{auth_rejected, extract_bearer, AuthenticatedPrincipal};
51use crate::call_context::CallContext;
52use crate::interceptor::ServerInterceptor;
53
54const DEFAULT_LEEWAY: Duration = Duration::from_secs(60);
56
57const DEFAULT_JWKS_TTL: Duration = Duration::from_secs(3600);
60
61const MAX_JWKS_RESPONSE_SIZE: usize = 256 * 1024;
63
64const fn jwks_body_exceeds_limit(collected_len: usize, chunk_len: usize) -> bool {
71 collected_len + chunk_len > MAX_JWKS_RESPONSE_SIZE
72}
73
74fn cache_is_fresh(elapsed: Duration, ttl: Duration) -> bool {
80 elapsed < ttl
81}
82
83#[derive(Clone)]
87struct VerifyKey {
88 kid: Option<String>,
89 material: KeyMaterial,
90}
91
92#[derive(Clone)]
93enum KeyMaterial {
94 Rsa(Vec<u8>),
96 EcP256(Vec<u8>),
98}
99
100#[derive(Clone, Default)]
107pub struct Jwks {
108 keys: Vec<VerifyKey>,
109}
110
111impl std::fmt::Debug for Jwks {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.debug_struct("Jwks")
114 .field("keys", &self.keys.len())
115 .finish()
116 }
117}
118
119impl Jwks {
120 #[must_use]
122 pub const fn new() -> Self {
123 Self { keys: Vec::new() }
124 }
125
126 pub fn from_json(json: &[u8]) -> A2aResult<Self> {
139 #[derive(serde::Deserialize)]
140 struct JwkSet {
141 #[serde(default)]
142 keys: Vec<Jwk>,
143 }
144 #[derive(serde::Deserialize)]
145 struct Jwk {
146 kty: String,
147 #[serde(default)]
148 crv: Option<String>,
149 #[serde(default)]
150 kid: Option<String>,
151 #[serde(rename = "use", default)]
152 use_: Option<String>,
153 #[serde(default)]
154 n: Option<String>,
155 #[serde(default)]
156 e: Option<String>,
157 #[serde(default)]
158 x: Option<String>,
159 #[serde(default)]
160 y: Option<String>,
161 }
162
163 let set: JwkSet = serde_json::from_slice(json)
164 .map_err(|e| A2aError::invalid_params(format!("invalid JWKS JSON: {e}")))?;
165
166 let mut jwks = Self::new();
167 for k in set.keys {
168 if k.use_.as_deref() == Some("enc") {
170 continue;
171 }
172 match k.kty.as_str() {
173 "RSA" => {
174 let (Some(n), Some(e)) = (k.n.as_deref(), k.e.as_deref()) else {
175 return Err(A2aError::invalid_params("RSA JWK missing n/e"));
176 };
177 jwks = jwks.with_rsa_opt_kid(k.kid, n, e)?;
178 }
179 "EC" if k.crv.as_deref() == Some("P-256") => {
180 let (Some(x), Some(y)) = (k.x.as_deref(), k.y.as_deref()) else {
181 return Err(A2aError::invalid_params("EC JWK missing x/y"));
182 };
183 jwks = jwks.with_ec_p256_opt_kid(k.kid, x, y)?;
184 }
185 _ => {}
188 }
189 }
190 Ok(jwks)
191 }
192
193 pub fn with_rsa(self, kid: impl Into<String>, n: &str, e: &str) -> A2aResult<Self> {
199 self.with_rsa_opt_kid(Some(kid.into()), n, e)
200 }
201
202 fn with_rsa_opt_kid(mut self, kid: Option<String>, n: &str, e: &str) -> A2aResult<Self> {
203 let n = b64url(n, "RSA modulus")?;
204 let e = b64url(e, "RSA exponent")?;
205 self.keys.push(VerifyKey {
206 kid,
207 material: KeyMaterial::Rsa(rsa_pkcs1_der(&n, &e)),
208 });
209 Ok(self)
210 }
211
212 pub fn with_ec_p256(self, kid: impl Into<String>, x: &str, y: &str) -> A2aResult<Self> {
219 self.with_ec_p256_opt_kid(Some(kid.into()), x, y)
220 }
221
222 fn with_ec_p256_opt_kid(mut self, kid: Option<String>, x: &str, y: &str) -> A2aResult<Self> {
223 let x = b64url(x, "EC x")?;
224 let y = b64url(y, "EC y")?;
225 if x.len() != 32 || y.len() != 32 {
226 return Err(A2aError::invalid_params(
227 "EC P-256 coordinates must be 32 bytes each",
228 ));
229 }
230 let mut point = Vec::with_capacity(65);
231 point.push(0x04); point.extend_from_slice(&x);
233 point.extend_from_slice(&y);
234 self.keys.push(VerifyKey {
235 kid,
236 material: KeyMaterial::EcP256(point),
237 });
238 Ok(self)
239 }
240
241 fn candidates(&self, kid: Option<&str>) -> (Vec<&VerifyKey>, bool) {
255 if let Some(kid) = kid {
256 let exact: Vec<&VerifyKey> = self
257 .keys
258 .iter()
259 .filter(|k| k.kid.as_deref() == Some(kid))
260 .collect();
261 if !exact.is_empty() {
262 return (exact, true);
263 }
264 if self.keys.iter().any(|k| k.kid.is_some()) {
268 return (Vec::new(), false);
269 }
270 }
271 (self.keys.iter().collect(), false)
272 }
273
274 const fn is_empty(&self) -> bool {
275 self.keys.is_empty()
276 }
277}
278
279#[derive(Clone)]
287pub struct JwtValidator {
288 issuers: HashSet<String>,
289 audiences: HashSet<String>,
290 leeway: Duration,
291 require_exp: bool,
292 hs256_secret: Option<Arc<Vec<u8>>>,
293}
294
295impl std::fmt::Debug for JwtValidator {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 f.debug_struct("JwtValidator")
298 .field("issuers", &self.issuers)
299 .field("audiences", &self.audiences)
300 .field("leeway", &self.leeway)
301 .field("require_exp", &self.require_exp)
302 .field(
303 "hs256_secret",
304 &self.hs256_secret.as_ref().map(|_| "<redacted>"),
305 )
306 .finish()
307 }
308}
309
310impl Default for JwtValidator {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316impl JwtValidator {
317 #[must_use]
319 pub fn new() -> Self {
320 Self {
321 issuers: HashSet::new(),
322 audiences: HashSet::new(),
323 leeway: DEFAULT_LEEWAY,
324 require_exp: true,
325 hs256_secret: None,
326 }
327 }
328
329 #[must_use]
331 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
332 self.issuers.insert(issuer.into());
333 self
334 }
335
336 #[must_use]
338 pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
339 self.audiences.insert(audience.into());
340 self
341 }
342
343 #[must_use]
345 pub const fn with_leeway(mut self, leeway: Duration) -> Self {
346 self.leeway = leeway;
347 self
348 }
349
350 #[must_use]
352 pub const fn allow_missing_exp(mut self) -> Self {
353 self.require_exp = false;
354 self
355 }
356
357 #[must_use]
363 pub fn with_hs256_secret(mut self, secret: impl Into<Vec<u8>>) -> Self {
364 self.hs256_secret = Some(Arc::new(secret.into()));
365 self
366 }
367
368 fn validate(
376 &self,
377 token: &str,
378 jwks: &Jwks,
379 ) -> Result<AuthenticatedPrincipal, ValidateOutcome> {
380 let parts: Vec<&str> = token.split('.').collect();
381 if parts.len() != 3 {
382 return Err(ValidateOutcome::Rejected);
383 }
384 let (header_b64, claims_b64, sig_b64) = (parts[0], parts[1], parts[2]);
385
386 let header: JwtHeader = decode_json(header_b64).map_err(|()| ValidateOutcome::Rejected)?;
387 let signature = URL_SAFE_NO_PAD
388 .decode(sig_b64)
389 .map_err(|_| ValidateOutcome::Rejected)?;
390 let signing_input = format!("{header_b64}.{claims_b64}");
391
392 let alg = header.alg.as_str();
393 let kid_matched = match alg {
394 "HS256" => {
395 let secret = self
396 .hs256_secret
397 .as_ref()
398 .ok_or(ValidateOutcome::Rejected)?;
399 let key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, secret);
400 ring::hmac::verify(&key, signing_input.as_bytes(), &signature)
401 .map_err(|_| ValidateOutcome::Rejected)?;
402 true }
404 "RS256" | "ES256" => {
405 let (candidates, kid_matched) = jwks.candidates(header.kid.as_deref());
406 if candidates.is_empty() {
407 return Err(ValidateOutcome::KeyMiss);
409 }
410 let verified = candidates.iter().any(|key| {
411 verify_asymmetric(alg, &key.material, signing_input.as_bytes(), &signature)
412 });
413 if !verified {
414 return Err(if header.kid.is_some() && !kid_matched {
416 ValidateOutcome::KeyMiss
417 } else {
418 ValidateOutcome::Rejected
419 });
420 }
421 kid_matched
422 }
423 _ => return Err(ValidateOutcome::Rejected), };
425 let _ = kid_matched;
426
427 let claims: JwtClaims = decode_json(claims_b64).map_err(|()| ValidateOutcome::Rejected)?;
429 self.check_claims(&claims)
430 .map_err(|()| ValidateOutcome::Rejected)?;
431
432 Ok(AuthenticatedPrincipal {
433 subject: claims.sub,
434 issuer: claims.iss,
435 })
436 }
437
438 fn check_claims(&self, claims: &JwtClaims) -> Result<(), ()> {
439 let now = SystemTime::now()
440 .duration_since(UNIX_EPOCH)
441 .map_err(|_| ())?
442 .as_secs();
443 self.check_claims_at(claims, now)
444 }
445
446 fn check_claims_at(&self, claims: &JwtClaims, now: u64) -> Result<(), ()> {
451 let leeway = self.leeway.as_secs();
452
453 match claims.exp {
454 Some(exp) => {
455 if now >= exp.saturating_add(leeway) {
459 return Err(()); }
461 }
462 None if self.require_exp => return Err(()),
463 None => {}
464 }
465 if let Some(nbf) = claims.nbf {
466 if now.saturating_add(leeway) < nbf {
467 return Err(()); }
469 }
470 if !self.issuers.is_empty() {
471 match &claims.iss {
472 Some(iss) if self.issuers.contains(iss) => {}
473 _ => return Err(()),
474 }
475 }
476 if !self.audiences.is_empty() {
477 let ok = claims
478 .aud
479 .as_ref()
480 .is_some_and(|aud| aud.iter().any(|a| self.audiences.contains(a)));
481 if !ok {
482 return Err(());
483 }
484 }
485 Ok(())
486 }
487}
488
489#[cfg_attr(test, derive(Debug))]
491enum ValidateOutcome {
492 Rejected,
494 KeyMiss,
497}
498
499#[derive(serde::Deserialize)]
500struct JwtHeader {
501 alg: String,
502 #[serde(default)]
503 kid: Option<String>,
504}
505
506#[derive(serde::Deserialize)]
507struct JwtClaims {
508 #[serde(default)]
509 iss: Option<String>,
510 #[serde(default)]
511 sub: Option<String>,
512 #[serde(default, deserialize_with = "de_aud")]
513 aud: Option<Vec<String>>,
514 #[serde(default)]
515 exp: Option<u64>,
516 #[serde(default)]
517 nbf: Option<u64>,
518}
519
520fn de_aud<'de, D>(de: D) -> Result<Option<Vec<String>>, D::Error>
522where
523 D: serde::Deserializer<'de>,
524{
525 #[derive(serde::Deserialize)]
526 #[serde(untagged)]
527 enum Aud {
528 One(String),
529 Many(Vec<String>),
530 }
531 Ok(
532 <Option<Aud> as serde::Deserialize>::deserialize(de)?.map(|a| match a {
533 Aud::One(s) => vec![s],
534 Aud::Many(v) => v,
535 }),
536 )
537}
538
539enum KeySource {
543 Static(Jwks),
545 Remote(Box<RemoteJwks>),
548}
549
550struct RemoteJwks {
551 url: String,
552 ttl: Duration,
553 cache: RwLock<Option<CachedJwks>>,
554 refresh_lock: tokio::sync::Mutex<()>,
555 client: JwksHttpClient,
556}
557
558struct CachedJwks {
559 jwks: Jwks,
560 fetched_at: std::time::Instant,
561}
562
563pub struct JwtAuthInterceptor {
568 validator: JwtValidator,
569 keys: KeySource,
570}
571
572impl std::fmt::Debug for JwtAuthInterceptor {
573 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574 f.debug_struct("JwtAuthInterceptor")
575 .field("validator", &self.validator)
576 .field(
577 "keys",
578 &match &self.keys {
579 KeySource::Static(_) => "static",
580 KeySource::Remote(_) => "remote-jwks",
581 },
582 )
583 .finish()
584 }
585}
586
587impl JwtAuthInterceptor {
588 #[must_use]
593 pub const fn new(validator: JwtValidator, jwks: Jwks) -> Self {
594 Self {
595 validator,
596 keys: KeySource::Static(jwks),
597 }
598 }
599
600 #[must_use]
606 pub fn from_jwks_url(validator: JwtValidator, jwks_url: impl Into<String>) -> Self {
607 Self {
608 validator,
609 keys: KeySource::Remote(Box::new(RemoteJwks {
610 url: jwks_url.into(),
611 ttl: DEFAULT_JWKS_TTL,
612 cache: RwLock::new(None),
613 refresh_lock: tokio::sync::Mutex::new(()),
614 client: build_jwks_client(),
615 })),
616 }
617 }
618
619 #[cfg(feature = "tls-rustls")]
624 #[must_use]
625 pub fn from_jwks_url_with_tls_config(
626 validator: JwtValidator,
627 jwks_url: impl Into<String>,
628 tls_config: rustls::ClientConfig,
629 ) -> Self {
630 let https = hyper_rustls::HttpsConnectorBuilder::new()
631 .with_tls_config(tls_config)
632 .https_or_http()
633 .enable_http1()
634 .enable_http2()
635 .build();
636 Self {
637 validator,
638 keys: KeySource::Remote(Box::new(RemoteJwks {
639 url: jwks_url.into(),
640 ttl: DEFAULT_JWKS_TTL,
641 cache: RwLock::new(None),
642 refresh_lock: tokio::sync::Mutex::new(()),
643 client: Client::builder(TokioExecutor::new()).build(https),
644 })),
645 }
646 }
647
648 pub async fn from_oidc_issuer(issuer: &str, validator: JwtValidator) -> A2aResult<Self> {
657 let jwks_url = discover_jwks_uri(issuer).await?;
658 Ok(Self::from_jwks_url(validator, jwks_url))
659 }
660
661 #[must_use]
663 pub fn with_jwks_ttl(mut self, ttl: Duration) -> Self {
664 if let KeySource::Remote(ref mut r) = self.keys {
665 r.ttl = ttl;
666 }
667 self
668 }
669
670 async fn authenticate(&self, ctx: &CallContext) -> A2aResult<AuthenticatedPrincipal> {
671 let header = ctx
672 .http_headers()
673 .get("authorization")
674 .ok_or_else(auth_rejected)?;
675 let token = extract_bearer(header).ok_or_else(auth_rejected)?;
676
677 match &self.keys {
678 KeySource::Static(jwks) => self
679 .validator
680 .validate(token, jwks)
681 .map_err(|_| auth_rejected()),
682 KeySource::Remote(remote) => {
683 let jwks = remote.get(false).await?;
684 match self.validator.validate(token, &jwks) {
685 Ok(principal) => Ok(principal),
686 Err(ValidateOutcome::KeyMiss) => {
687 let fresh = remote.get(true).await?;
689 self.validator
690 .validate(token, &fresh)
691 .map_err(|_| auth_rejected())
692 }
693 Err(ValidateOutcome::Rejected) => Err(auth_rejected()),
694 }
695 }
696 }
697 }
698}
699
700impl ServerInterceptor for JwtAuthInterceptor {
701 fn before<'a>(
702 &'a self,
703 ctx: &'a CallContext,
704 ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
705 Box::pin(async move {
706 self.authenticate(ctx).await.map(|_principal| ())
709 })
710 }
711
712 fn after<'a>(
713 &'a self,
714 _ctx: &'a CallContext,
715 ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
716 Box::pin(async move { Ok(()) })
717 }
718
719 fn authenticates(&self) -> bool {
720 true
721 }
722}
723
724impl RemoteJwks {
725 async fn get(&self, force: bool) -> A2aResult<Jwks> {
727 if !force {
728 if let Some(jwks) = self.cached_fresh() {
729 return Ok(jwks);
730 }
731 }
732 let _guard = self.refresh_lock.lock().await;
733 if !force {
736 if let Some(jwks) = self.cached_fresh() {
737 return Ok(jwks);
738 }
739 }
740 let jwks = self.fetch().await?;
741 *self
742 .cache
743 .write()
744 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(CachedJwks {
745 jwks: jwks.clone(),
746 fetched_at: std::time::Instant::now(),
747 });
748 Ok(jwks)
749 }
750
751 fn cached_fresh(&self) -> Option<Jwks> {
752 let guard = self
753 .cache
754 .read()
755 .unwrap_or_else(std::sync::PoisonError::into_inner);
756 guard.as_ref().and_then(|c| {
757 if cache_is_fresh(c.fetched_at.elapsed(), self.ttl) {
758 Some(c.jwks.clone())
759 } else {
760 None
761 }
762 })
763 }
764
765 async fn fetch(&self) -> A2aResult<Jwks> {
766 let body = http_get_json(&self.client, &self.url, "JWKS").await?;
767 let jwks = Jwks::from_json(&body)?;
768 if jwks.is_empty() {
769 return Err(A2aError::internal("JWKS endpoint returned no usable keys"));
770 }
771 Ok(jwks)
772 }
773}
774
775use http_body_util::Full;
778use hyper::body::Bytes;
779use hyper_util::client::legacy::connect::HttpConnector;
780use hyper_util::client::legacy::Client;
781use hyper_util::rt::TokioExecutor;
782
783#[cfg(not(feature = "tls-rustls"))]
784type JwksHttpClient = Client<HttpConnector, Full<Bytes>>;
785#[cfg(feature = "tls-rustls")]
786type JwksHttpClient = Client<hyper_rustls::HttpsConnector<HttpConnector>, Full<Bytes>>;
787
788#[cfg(not(feature = "tls-rustls"))]
789fn build_jwks_client() -> JwksHttpClient {
790 let mut connector = HttpConnector::new();
791 connector.set_connect_timeout(Some(Duration::from_secs(10)));
792 Client::builder(TokioExecutor::new()).build(connector)
793}
794
795#[cfg(feature = "tls-rustls")]
796fn build_jwks_client() -> JwksHttpClient {
797 let mut roots = rustls::RootCertStore::empty();
798 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
799 let tls = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(
800 rustls::crypto::ring::default_provider(),
801 ))
802 .with_safe_default_protocol_versions()
803 .expect("ring provider supports the default protocol versions")
804 .with_root_certificates(roots)
805 .with_no_client_auth();
806 let https = hyper_rustls::HttpsConnectorBuilder::new()
807 .with_tls_config(tls)
808 .https_or_http()
809 .enable_http1()
810 .enable_http2()
811 .build();
812 Client::builder(TokioExecutor::new()).build(https)
813}
814
815async fn http_get_json(client: &JwksHttpClient, url: &str, what: &str) -> A2aResult<Vec<u8>> {
816 use http_body_util::BodyExt;
817
818 let req = hyper::Request::builder()
819 .method(hyper::Method::GET)
820 .uri(url)
821 .header("accept", "application/json")
822 .body(Full::new(Bytes::new()))
823 .map_err(|e| A2aError::internal(format!("{what} request build failed: {e}")))?;
824
825 let resp = tokio::time::timeout(Duration::from_secs(30), client.request(req))
826 .await
827 .map_err(|_| A2aError::internal(format!("{what} request timed out")))?
828 .map_err(|e| A2aError::internal(format!("{what} request failed: {e}")))?;
829
830 if !resp.status().is_success() {
831 return Err(A2aError::internal(format!(
832 "{what} endpoint returned HTTP {}",
833 resp.status()
834 )));
835 }
836
837 let mut collected: Vec<u8> = Vec::new();
839 let mut body = resp.into_body();
840 while let Some(frame) = body.frame().await {
841 let frame =
842 frame.map_err(|e| A2aError::internal(format!("{what} body read failed: {e}")))?;
843 if let Some(chunk) = frame.data_ref() {
844 if jwks_body_exceeds_limit(collected.len(), chunk.len()) {
845 return Err(A2aError::internal(format!("{what} response too large")));
846 }
847 collected.extend_from_slice(chunk);
848 }
849 }
850 Ok(collected)
851}
852
853async fn discover_jwks_uri(issuer: &str) -> A2aResult<String> {
855 #[derive(serde::Deserialize)]
856 struct Discovery {
857 jwks_uri: Option<String>,
858 }
859 let url = format!(
860 "{}/.well-known/openid-configuration",
861 issuer.trim_end_matches('/')
862 );
863 let client = build_jwks_client();
864 let body = http_get_json(&client, &url, "OIDC discovery").await?;
865 let doc: Discovery = serde_json::from_slice(&body)
866 .map_err(|e| A2aError::internal(format!("OIDC discovery returned invalid JSON: {e}")))?;
867 doc.jwks_uri
868 .ok_or_else(|| A2aError::internal("OIDC discovery document has no jwks_uri"))
869}
870
871fn verify_asymmetric(alg: &str, key: &KeyMaterial, msg: &[u8], sig: &[u8]) -> bool {
874 match (alg, key) {
875 ("RS256", KeyMaterial::Rsa(der)) => signature::UnparsedPublicKey::new(
876 &signature::RSA_PKCS1_2048_8192_SHA256,
877 der.as_slice(),
878 )
879 .verify(msg, sig)
880 .is_ok(),
881 ("ES256", KeyMaterial::EcP256(point)) => {
882 signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_FIXED, point.as_slice())
883 .verify(msg, sig)
884 .is_ok()
885 }
886 _ => false,
889 }
890}
891
892fn rsa_pkcs1_der(n: &[u8], e: &[u8]) -> Vec<u8> {
898 let mut body = der_uint(n);
899 body.extend(der_uint(e));
900 der_tlv(0x30, &body) }
902
903fn der_uint(bytes: &[u8]) -> Vec<u8> {
906 let start = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len());
909 let trimmed = &bytes[start..];
910 let mut content = Vec::with_capacity(trimmed.len() + 1);
911 if trimmed.first().is_none_or(|&b| b & 0x80 != 0) {
912 content.push(0x00);
913 }
914 content.extend_from_slice(trimmed);
915 der_tlv(0x02, &content)
916}
917
918fn der_tlv(tag: u8, content: &[u8]) -> Vec<u8> {
920 let mut out = vec![tag];
921 let len = content.len();
922 if len < 0x80 {
923 #[allow(clippy::cast_possible_truncation)]
924 out.push(len as u8);
925 } else {
926 let len_bytes = len.to_be_bytes();
927 let first_nonzero = len_bytes
930 .iter()
931 .position(|&b| b != 0)
932 .expect("len >= 0x80 has a non-zero big-endian byte");
933 let significant = &len_bytes[first_nonzero..];
934 #[allow(clippy::cast_possible_truncation)]
939 out.push(0x80 + significant.len() as u8);
940 out.extend_from_slice(significant);
941 }
942 out.extend_from_slice(content);
943 out
944}
945
946fn b64url(s: &str, what: &str) -> A2aResult<Vec<u8>> {
947 URL_SAFE_NO_PAD
948 .decode(s)
949 .map_err(|e| A2aError::invalid_params(format!("invalid base64url {what}: {e}")))
950}
951
952fn decode_json<T: serde::de::DeserializeOwned>(b64: &str) -> Result<T, ()> {
953 let bytes = URL_SAFE_NO_PAD.decode(b64).map_err(|_| ())?;
954 serde_json::from_slice(&bytes).map_err(|_| ())
955}
956
957#[cfg(test)]
960mod tests {
961 use super::*;
962
963 include!("jwt_test_vectors.rs");
967
968 fn ctx_bearer(token: &str) -> CallContext {
969 CallContext::new("message/send")
970 .with_http_header("authorization", format!("Bearer {token}"))
971 }
972
973 fn base_validator() -> JwtValidator {
974 JwtValidator::new()
975 .with_issuer("https://issuer.test")
976 .with_audience("a2a-agent")
977 }
978
979 #[tokio::test]
982 async fn hs256_valid_and_rejections() {
983 let secret = URL_SAFE_NO_PAD.decode(HS256_SECRET_B64).unwrap();
984 let v = base_validator().with_hs256_secret(secret);
985 let i = JwtAuthInterceptor::new(v, Jwks::new());
986
987 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_ok());
988 assert!(i.before(&ctx_bearer(HS256_EXPIRED)).await.is_err());
989 assert!(i.before(&ctx_bearer(HS256_WRONG_SECRET)).await.is_err());
990 }
991
992 #[tokio::test]
993 async fn hs256_without_configured_secret_is_rejected() {
994 let i = JwtAuthInterceptor::new(base_validator(), Jwks::new());
997 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_err());
998 }
999
1000 fn rsa_jwks() -> Jwks {
1003 Jwks::new().with_rsa("rk1", RS256_N, RS256_E).unwrap()
1004 }
1005
1006 #[tokio::test]
1007 async fn rs256_valid_and_rejections() {
1008 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1009
1010 assert!(i.before(&ctx_bearer(RS256_VALID)).await.is_ok());
1011 assert!(i.before(&ctx_bearer(RS256_EXPIRED)).await.is_err());
1012 assert!(i.before(&ctx_bearer(RS256_WRONG_KEY)).await.is_err());
1013 assert!(i.before(&ctx_bearer(RS256_WRONG_ISS)).await.is_err());
1014 assert!(i.before(&ctx_bearer(RS256_WRONG_AUD)).await.is_err());
1015 assert!(i.before(&ctx_bearer(RS256_UNKNOWN_KID)).await.is_err());
1016 }
1017
1018 #[tokio::test]
1019 async fn rs256_from_jwks_json_roundtrip() {
1020 let jwks_json = format!(
1021 r#"{{"keys":[{{"kty":"RSA","kid":"rk1","use":"sig","n":"{RS256_N}","e":"{RS256_E}"}}]}}"#
1022 );
1023 let jwks = Jwks::from_json(jwks_json.as_bytes()).unwrap();
1024 let i = JwtAuthInterceptor::new(base_validator(), jwks);
1025 assert!(i.before(&ctx_bearer(RS256_VALID)).await.is_ok());
1026 }
1027
1028 #[tokio::test]
1029 async fn algorithm_confusion_rejected() {
1030 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1036 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_err());
1037 }
1038
1039 #[test]
1040 fn alg_none_is_rejected() {
1041 let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"none","typ":"JWT"}"#);
1043 let claims = URL_SAFE_NO_PAD
1044 .encode(br#"{"iss":"https://issuer.test","aud":"a2a-agent","exp":253402300799}"#);
1045 let token = format!("{header}.{claims}.");
1046 let outcome = base_validator().validate(&token, &rsa_jwks());
1047 assert!(matches!(outcome, Err(ValidateOutcome::Rejected)));
1048 }
1049
1050 #[tokio::test]
1053 async fn es256_valid_and_expired() {
1054 let jwks = Jwks::new().with_ec_p256("ek1", ES256_X, ES256_Y).unwrap();
1055 let i = JwtAuthInterceptor::new(base_validator(), jwks);
1056 assert!(i.before(&ctx_bearer(ES256_VALID)).await.is_ok());
1057 assert!(i.before(&ctx_bearer(ES256_EXPIRED)).await.is_err());
1058 }
1059
1060 #[tokio::test]
1063 async fn audience_and_issuer_optional_when_unset() {
1064 let secret = URL_SAFE_NO_PAD.decode(HS256_SECRET_B64).unwrap();
1066 let v = JwtValidator::new().with_hs256_secret(secret);
1067 let i = JwtAuthInterceptor::new(v, Jwks::new());
1068 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_ok());
1070 }
1071
1072 #[tokio::test]
1073 async fn missing_authorization_header_rejected() {
1074 let secret = URL_SAFE_NO_PAD.decode(HS256_SECRET_B64).unwrap();
1075 let v = base_validator().with_hs256_secret(secret);
1076 let i = JwtAuthInterceptor::new(v, Jwks::new());
1077 assert!(i.before(&CallContext::new("m")).await.is_err());
1078 assert!(i
1079 .before(&CallContext::new("m").with_http_header("authorization", "Basic x"))
1080 .await
1081 .is_err());
1082 }
1083
1084 #[test]
1087 fn der_uint_prepends_zero_when_high_bit_set() {
1088 assert_eq!(der_uint(&[0x80]), vec![0x02, 0x02, 0x00, 0x80]);
1090 assert_eq!(der_uint(&[0x7f]), vec![0x02, 0x01, 0x7f]);
1092 assert_eq!(der_uint(&[0x00, 0x01]), vec![0x02, 0x01, 0x01]);
1094 }
1095
1096 #[test]
1097 fn der_tlv_long_form_length() {
1098 let content = vec![0xabu8; 300];
1099 let tlv = der_tlv(0x04, &content);
1100 assert_eq!(&tlv[..4], &[0x04, 0x82, 0x01, 0x2c]);
1102 assert_eq!(tlv.len(), 4 + 300);
1103 }
1104
1105 #[test]
1108 fn jwks_skips_enc_and_unknown_keys() {
1109 let json = format!(
1110 r#"{{"keys":[
1111 {{"kty":"RSA","kid":"enc1","use":"enc","n":"{RS256_N}","e":"{RS256_E}"}},
1112 {{"kty":"oct","kid":"sym","k":"abc"}},
1113 {{"kty":"EC","crv":"P-384","kid":"e384","x":"{ES256_X}","y":"{ES256_Y}"}},
1114 {{"kty":"RSA","kid":"sig1","use":"sig","n":"{RS256_N}","e":"{RS256_E}"}}
1115 ]}}"#
1116 );
1117 let jwks = Jwks::from_json(json.as_bytes()).unwrap();
1118 assert_eq!(jwks.keys.len(), 1);
1120 assert_eq!(jwks.keys[0].kid.as_deref(), Some("sig1"));
1121 }
1122
1123 #[test]
1126 fn jwks_is_empty_reflects_key_count() {
1127 assert!(Jwks::new().is_empty(), "a fresh key set is empty");
1128 assert!(!rsa_jwks().is_empty(), "a key set with a key is not empty");
1129 }
1130
1131 #[test]
1132 fn jwks_from_json_loads_ec_p256_key() {
1133 let json = format!(
1137 r#"{{"keys":[{{"kty":"EC","crv":"P-256","kid":"ek1","x":"{ES256_X}","y":"{ES256_Y}"}}]}}"#
1138 );
1139 let jwks = Jwks::from_json(json.as_bytes()).unwrap();
1140 assert_eq!(jwks.keys.len(), 1, "the P-256 key must be loaded");
1141 assert_eq!(jwks.keys[0].kid.as_deref(), Some("ek1"));
1142 }
1143
1144 #[test]
1145 fn ec_p256_rejects_wrong_length_coordinate() {
1146 let ok_y = ES256_Y;
1149 let short_x = URL_SAFE_NO_PAD.encode([0u8; 31]);
1150 assert!(
1151 Jwks::new().with_ec_p256("k", &short_x, ok_y).is_err(),
1152 "a 31-byte x coordinate must be rejected"
1153 );
1154 let long_y = URL_SAFE_NO_PAD.encode([0u8; 33]);
1155 assert!(
1156 Jwks::new().with_ec_p256("k", ES256_X, &long_y).is_err(),
1157 "a 33-byte y coordinate must be rejected"
1158 );
1159 assert!(Jwks::new().with_ec_p256("k", ES256_X, ES256_Y).is_ok());
1161 }
1162
1163 #[test]
1166 fn debug_impls_render_type_and_redact_secrets() {
1167 let jwks_dbg = format!("{:?}", rsa_jwks());
1171 assert!(jwks_dbg.contains("Jwks"), "Jwks Debug: {jwks_dbg}");
1172 assert!(jwks_dbg.contains("keys"), "Jwks Debug lists key count");
1173
1174 let secret = b"super-secret-value-1234567890";
1175 let validator = base_validator().with_hs256_secret(secret.to_vec());
1176 let v_dbg = format!("{validator:?}");
1177 assert!(
1178 v_dbg.contains("JwtValidator"),
1179 "JwtValidator Debug: {v_dbg}"
1180 );
1181 assert!(v_dbg.contains("redacted"), "the secret must be redacted");
1182 assert!(
1183 !v_dbg.contains("super-secret"),
1184 "the raw HS256 secret must never appear in Debug output"
1185 );
1186
1187 let interceptor = JwtAuthInterceptor::new(validator, rsa_jwks());
1188 let i_dbg = format!("{interceptor:?}");
1189 assert!(
1190 i_dbg.contains("JwtAuthInterceptor"),
1191 "JwtAuthInterceptor Debug: {i_dbg}"
1192 );
1193 assert!(i_dbg.contains("static"), "static key source is labelled");
1194 }
1195
1196 fn claims_at(exp: Option<u64>, nbf: Option<u64>) -> JwtClaims {
1199 JwtClaims {
1200 iss: None,
1201 sub: None,
1202 aud: None,
1203 exp,
1204 nbf,
1205 }
1206 }
1207
1208 #[test]
1209 fn check_claims_require_exp_boundary() {
1210 let strict = JwtValidator::new();
1212 assert!(
1213 strict
1214 .check_claims_at(&claims_at(None, None), 1_000)
1215 .is_err(),
1216 "no exp must be rejected when exp is required"
1217 );
1218 let lax = JwtValidator::new().allow_missing_exp();
1220 assert!(
1221 lax.check_claims_at(&claims_at(None, None), 1_000).is_ok(),
1222 "no exp must be accepted when exp is optional"
1223 );
1224 assert!(
1226 strict
1227 .check_claims_at(&claims_at(Some(2_000), None), 1_000)
1228 .is_ok(),
1229 "unexpired token passes"
1230 );
1231 assert!(
1232 strict
1233 .check_claims_at(&claims_at(Some(500), None), 1_000)
1234 .is_err(),
1235 "expired token fails (now past exp + leeway)"
1236 );
1237 }
1238
1239 #[test]
1240 fn check_claims_nbf_boundary_is_strict() {
1241 let v = JwtValidator::new()
1243 .allow_missing_exp()
1244 .with_leeway(std::time::Duration::ZERO);
1245 assert!(
1247 v.check_claims_at(&claims_at(None, Some(2_000)), 1_000)
1248 .is_err(),
1249 "a token whose nbf is in the future must be rejected"
1250 );
1251 assert!(
1253 v.check_claims_at(&claims_at(None, Some(1_000)), 1_000)
1254 .is_ok(),
1255 "a token is valid at exactly its nbf instant"
1256 );
1257 assert!(
1259 v.check_claims_at(&claims_at(None, Some(500)), 1_000)
1260 .is_ok(),
1261 "a token whose nbf is in the past is valid"
1262 );
1263 }
1264
1265 #[test]
1266 fn check_claims_exp_boundary_is_fail_closed() {
1267 let v = JwtValidator::new().with_leeway(std::time::Duration::ZERO);
1270 assert!(
1271 v.check_claims_at(&claims_at(Some(999), None), 1_000)
1272 .is_err(),
1273 "a token past its exp is expired"
1274 );
1275 assert!(
1277 v.check_claims_at(&claims_at(Some(1_000), None), 1_000)
1278 .is_err(),
1279 "a token is expired at exactly its exp instant"
1280 );
1281 assert!(
1283 v.check_claims_at(&claims_at(Some(1_001), None), 1_000)
1284 .is_ok(),
1285 "a token strictly before its exp is valid"
1286 );
1287 let lenient = JwtValidator::new().with_leeway(std::time::Duration::from_secs(60));
1289 assert!(
1290 lenient
1291 .check_claims_at(&claims_at(Some(1_000), None), 1_059)
1292 .is_ok(),
1293 "within leeway of exp: still valid"
1294 );
1295 assert!(
1296 lenient
1297 .check_claims_at(&claims_at(Some(1_000), None), 1_060)
1298 .is_err(),
1299 "at exactly exp + leeway: expired (fail-closed)"
1300 );
1301 }
1302
1303 #[test]
1304 fn cached_jwks_freshness_is_strict() {
1305 let ttl = std::time::Duration::from_secs(3600);
1306 assert!(
1307 cache_is_fresh(std::time::Duration::from_secs(3599), ttl),
1308 "an entry younger than its TTL is fresh"
1309 );
1310 assert!(
1312 !cache_is_fresh(ttl, ttl),
1313 "an entry at exactly its TTL is stale"
1314 );
1315 assert!(
1316 !cache_is_fresh(std::time::Duration::from_secs(3601), ttl),
1317 "an entry past its TTL is stale"
1318 );
1319 }
1320
1321 #[test]
1324 fn matching_kid_bad_signature_is_rejected_not_keymiss() {
1325 let outcome = base_validator().validate(RS256_WRONG_KEY, &rsa_jwks());
1329 assert!(
1330 matches!(outcome, Err(ValidateOutcome::Rejected)),
1331 "matched-kid bad-signature must be Rejected, got {outcome:?}"
1332 );
1333 }
1334
1335 #[test]
1336 fn no_kid_bad_signature_is_rejected_not_keymiss() {
1337 let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"RS256","typ":"JWT"}"#);
1340 let parts: Vec<&str> = RS256_VALID.split('.').collect();
1341 let token = format!("{header}.{}.{}", parts[1], parts[2]);
1344 let outcome = base_validator().validate(&token, &rsa_jwks());
1345 assert!(
1346 matches!(outcome, Err(ValidateOutcome::Rejected)),
1347 "no-kid bad-signature must be Rejected, got {outcome:?}"
1348 );
1349 }
1350
1351 #[test]
1352 fn unknown_kid_is_keymiss() {
1353 let outcome = base_validator().validate(RS256_UNKNOWN_KID, &rsa_jwks());
1355 assert!(
1356 matches!(outcome, Err(ValidateOutcome::KeyMiss)),
1357 "unknown-kid must be KeyMiss, got {outcome:?}"
1358 );
1359 }
1360
1361 #[test]
1364 fn der_tlv_short_and_long_form_lengths() {
1365 assert_eq!(&der_tlv(0x04, &[0u8; 5])[..2], &[0x04, 0x05]);
1367 assert_eq!(&der_tlv(0x04, &[0u8; 127])[..2], &[0x04, 0x7f]);
1368 assert_eq!(&der_tlv(0x04, &[0u8; 128])[..3], &[0x04, 0x81, 0x80]);
1371 assert_eq!(&der_tlv(0x04, &[0u8; 300])[..4], &[0x04, 0x82, 0x01, 0x2c]);
1373 assert_eq!(der_tlv(0x02, &[0xAA, 0xBB]), vec![0x02, 0x02, 0xAA, 0xBB]);
1375 }
1376
1377 #[test]
1380 fn jwks_body_size_limit() {
1381 assert!(!jwks_body_exceeds_limit(0, 200_000));
1384 assert!(!jwks_body_exceeds_limit(0, 256 * 1024));
1385 assert!(jwks_body_exceeds_limit(0, 256 * 1024 + 1));
1387 assert!(jwks_body_exceeds_limit(256 * 1024, 1));
1388 }
1389
1390 #[test]
1400 fn jwt_interceptor_declares_that_it_authenticates() {
1401 let interceptor = JwtAuthInterceptor::new(base_validator(), Jwks::new());
1402 assert!(
1403 interceptor.authenticates(),
1404 "a JWT auth interceptor must declare itself as one"
1405 );
1406
1407 let mut chain = crate::interceptor::ServerInterceptorChain::new();
1408 chain.push(std::sync::Arc::new(JwtAuthInterceptor::new(
1409 base_validator(),
1410 Jwks::new(),
1411 )));
1412 assert!(
1413 chain.has_authenticator(),
1414 "a chain guarded by a JWT interceptor must satisfy the \
1415 extended-agent-card authentication requirement"
1416 );
1417 }
1418}