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 let principal = self.authenticate(ctx).await?;
707
708 if let Some(subject) = principal.subject {
719 ctx.set_caller_identity(subject);
720 }
721 Ok(())
722 })
723 }
724
725 fn after<'a>(
726 &'a self,
727 _ctx: &'a CallContext,
728 ) -> Pin<Box<dyn Future<Output = A2aResult<()>> + Send + 'a>> {
729 Box::pin(async move { Ok(()) })
730 }
731
732 fn authenticates(&self) -> bool {
733 true
734 }
735}
736
737impl RemoteJwks {
738 async fn get(&self, force: bool) -> A2aResult<Jwks> {
740 if !force {
741 if let Some(jwks) = self.cached_fresh() {
742 return Ok(jwks);
743 }
744 }
745 let _guard = self.refresh_lock.lock().await;
746 if !force {
749 if let Some(jwks) = self.cached_fresh() {
750 return Ok(jwks);
751 }
752 }
753 let jwks = self.fetch().await?;
754 *self
755 .cache
756 .write()
757 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(CachedJwks {
758 jwks: jwks.clone(),
759 fetched_at: std::time::Instant::now(),
760 });
761 Ok(jwks)
762 }
763
764 fn cached_fresh(&self) -> Option<Jwks> {
765 let guard = self
766 .cache
767 .read()
768 .unwrap_or_else(std::sync::PoisonError::into_inner);
769 guard.as_ref().and_then(|c| {
770 if cache_is_fresh(c.fetched_at.elapsed(), self.ttl) {
771 Some(c.jwks.clone())
772 } else {
773 None
774 }
775 })
776 }
777
778 async fn fetch(&self) -> A2aResult<Jwks> {
779 let body = http_get_json(&self.client, &self.url, "JWKS").await?;
780 let jwks = Jwks::from_json(&body)?;
781 if jwks.is_empty() {
782 return Err(A2aError::internal("JWKS endpoint returned no usable keys"));
783 }
784 Ok(jwks)
785 }
786}
787
788use http_body_util::Full;
791use hyper::body::Bytes;
792use hyper_util::client::legacy::connect::HttpConnector;
793use hyper_util::client::legacy::Client;
794use hyper_util::rt::TokioExecutor;
795
796const JWKS_FETCH_BUDGET: Duration = Duration::from_secs(30);
814
815#[cfg(not(feature = "tls-rustls"))]
816type JwksHttpClient = Client<HttpConnector, Full<Bytes>>;
817#[cfg(feature = "tls-rustls")]
818type JwksHttpClient = Client<hyper_rustls::HttpsConnector<HttpConnector>, Full<Bytes>>;
819
820#[cfg(not(feature = "tls-rustls"))]
821fn build_jwks_client() -> JwksHttpClient {
822 let mut connector = HttpConnector::new();
823 connector.set_connect_timeout(Some(Duration::from_secs(10)));
824 Client::builder(TokioExecutor::new()).build(connector)
825}
826
827#[cfg(feature = "tls-rustls")]
828fn build_jwks_client() -> JwksHttpClient {
829 let mut roots = rustls::RootCertStore::empty();
830 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
831 let tls = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(
832 rustls::crypto::ring::default_provider(),
833 ))
834 .with_safe_default_protocol_versions()
835 .expect("ring provider supports the default protocol versions")
836 .with_root_certificates(roots)
837 .with_no_client_auth();
838 let https = hyper_rustls::HttpsConnectorBuilder::new()
839 .with_tls_config(tls)
840 .https_or_http()
841 .enable_http1()
842 .enable_http2()
843 .build();
844 Client::builder(TokioExecutor::new()).build(https)
845}
846
847async fn http_get_json(client: &JwksHttpClient, url: &str, what: &str) -> A2aResult<Vec<u8>> {
848 use http_body_util::BodyExt;
849
850 let req = hyper::Request::builder()
851 .method(hyper::Method::GET)
852 .uri(url)
853 .header("accept", "application/json")
854 .body(Full::new(Bytes::new()))
855 .map_err(|e| A2aError::internal(format!("{what} request build failed: {e}")))?;
856
857 let deadline = tokio::time::Instant::now() + JWKS_FETCH_BUDGET;
860
861 let resp = tokio::time::timeout_at(deadline, client.request(req))
862 .await
863 .map_err(|_| A2aError::internal(format!("{what} request timed out")))?
864 .map_err(|e| A2aError::internal(format!("{what} request failed: {e}")))?;
865
866 if !resp.status().is_success() {
867 return Err(A2aError::internal(format!(
868 "{what} endpoint returned HTTP {}",
869 resp.status()
870 )));
871 }
872
873 let mut body = resp.into_body();
876 let accumulate = async {
877 let mut collected: Vec<u8> = Vec::new();
878 while let Some(frame) = body.frame().await {
879 let frame =
880 frame.map_err(|e| A2aError::internal(format!("{what} body read failed: {e}")))?;
881 if let Some(chunk) = frame.data_ref() {
882 if jwks_body_exceeds_limit(collected.len(), chunk.len()) {
883 return Err(A2aError::internal(format!("{what} response too large")));
884 }
885 collected.extend_from_slice(chunk);
886 }
887 }
888 Ok(collected)
889 };
890
891 tokio::time::timeout_at(deadline, accumulate)
892 .await
893 .map_err(|_| A2aError::internal(format!("{what} body read timed out")))?
894}
895
896async fn discover_jwks_uri(issuer: &str) -> A2aResult<String> {
898 #[derive(serde::Deserialize)]
899 struct Discovery {
900 jwks_uri: Option<String>,
901 }
902 let url = format!(
903 "{}/.well-known/openid-configuration",
904 issuer.trim_end_matches('/')
905 );
906 let client = build_jwks_client();
907 let body = http_get_json(&client, &url, "OIDC discovery").await?;
908 let doc: Discovery = serde_json::from_slice(&body)
909 .map_err(|e| A2aError::internal(format!("OIDC discovery returned invalid JSON: {e}")))?;
910 doc.jwks_uri
911 .ok_or_else(|| A2aError::internal("OIDC discovery document has no jwks_uri"))
912}
913
914fn verify_asymmetric(alg: &str, key: &KeyMaterial, msg: &[u8], sig: &[u8]) -> bool {
917 match (alg, key) {
918 ("RS256", KeyMaterial::Rsa(der)) => signature::UnparsedPublicKey::new(
919 &signature::RSA_PKCS1_2048_8192_SHA256,
920 der.as_slice(),
921 )
922 .verify(msg, sig)
923 .is_ok(),
924 ("ES256", KeyMaterial::EcP256(point)) => {
925 signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_FIXED, point.as_slice())
926 .verify(msg, sig)
927 .is_ok()
928 }
929 _ => false,
932 }
933}
934
935fn rsa_pkcs1_der(n: &[u8], e: &[u8]) -> Vec<u8> {
941 let mut body = der_uint(n);
942 body.extend(der_uint(e));
943 der_tlv(0x30, &body) }
945
946fn der_uint(bytes: &[u8]) -> Vec<u8> {
949 let start = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len());
952 let trimmed = &bytes[start..];
953 let mut content = Vec::with_capacity(trimmed.len() + 1);
954 if trimmed.first().is_none_or(|&b| b & 0x80 != 0) {
955 content.push(0x00);
956 }
957 content.extend_from_slice(trimmed);
958 der_tlv(0x02, &content)
959}
960
961fn der_tlv(tag: u8, content: &[u8]) -> Vec<u8> {
963 let mut out = vec![tag];
964 let len = content.len();
965 if len < 0x80 {
966 #[allow(clippy::cast_possible_truncation)]
967 out.push(len as u8);
968 } else {
969 let len_bytes = len.to_be_bytes();
970 let first_nonzero = len_bytes
973 .iter()
974 .position(|&b| b != 0)
975 .expect("len >= 0x80 has a non-zero big-endian byte");
976 let significant = &len_bytes[first_nonzero..];
977 #[allow(clippy::cast_possible_truncation)]
982 out.push(0x80 + significant.len() as u8);
983 out.extend_from_slice(significant);
984 }
985 out.extend_from_slice(content);
986 out
987}
988
989fn b64url(s: &str, what: &str) -> A2aResult<Vec<u8>> {
990 URL_SAFE_NO_PAD
991 .decode(s)
992 .map_err(|e| A2aError::invalid_params(format!("invalid base64url {what}: {e}")))
993}
994
995fn decode_json<T: serde::de::DeserializeOwned>(b64: &str) -> Result<T, ()> {
996 let bytes = URL_SAFE_NO_PAD.decode(b64).map_err(|_| ())?;
997 serde_json::from_slice(&bytes).map_err(|_| ())
998}
999
1000#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 include!("jwt_test_vectors.rs");
1010
1011 fn ctx_bearer(token: &str) -> CallContext {
1012 CallContext::new("message/send")
1013 .with_http_header("authorization", format!("Bearer {token}"))
1014 }
1015
1016 fn base_validator() -> JwtValidator {
1017 JwtValidator::new()
1018 .with_issuer("https://issuer.test")
1019 .with_audience("a2a-agent")
1020 }
1021
1022 #[tokio::test]
1025 async fn hs256_valid_and_rejections() {
1026 let secret = URL_SAFE_NO_PAD.decode(HS256_SECRET_B64).unwrap();
1027 let v = base_validator().with_hs256_secret(secret);
1028 let i = JwtAuthInterceptor::new(v, Jwks::new());
1029
1030 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_ok());
1031 assert!(i.before(&ctx_bearer(HS256_EXPIRED)).await.is_err());
1032 assert!(i.before(&ctx_bearer(HS256_WRONG_SECRET)).await.is_err());
1033 }
1034
1035 #[tokio::test]
1036 async fn hs256_without_configured_secret_is_rejected() {
1037 let i = JwtAuthInterceptor::new(base_validator(), Jwks::new());
1040 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_err());
1041 }
1042
1043 fn rsa_jwks() -> Jwks {
1046 Jwks::new().with_rsa("rk1", RS256_N, RS256_E).unwrap()
1047 }
1048
1049 #[tokio::test]
1050 async fn rs256_valid_and_rejections() {
1051 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1052
1053 assert!(i.before(&ctx_bearer(RS256_VALID)).await.is_ok());
1054 assert!(i.before(&ctx_bearer(RS256_EXPIRED)).await.is_err());
1055 assert!(i.before(&ctx_bearer(RS256_WRONG_KEY)).await.is_err());
1056 assert!(i.before(&ctx_bearer(RS256_WRONG_ISS)).await.is_err());
1057 assert!(i.before(&ctx_bearer(RS256_WRONG_AUD)).await.is_err());
1058 assert!(i.before(&ctx_bearer(RS256_UNKNOWN_KID)).await.is_err());
1059 }
1060
1061 #[tokio::test]
1069 async fn a_validated_token_records_its_subject_as_the_caller() {
1070 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1071
1072 let ctx = ctx_bearer(RS256_VALID);
1073 i.before(&ctx).await.expect("the vector token is valid");
1074
1075 let identity = ctx
1076 .caller_identity()
1077 .expect("a validated token establishes an identity");
1078 assert!(
1079 !RS256_VALID.contains(identity),
1080 "the identity must be the subject, not a slice of the token"
1081 );
1082 }
1083
1084 #[tokio::test]
1087 async fn a_rejected_token_records_no_caller() {
1088 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1089
1090 let ctx = ctx_bearer(RS256_EXPIRED);
1091 i.before(&ctx)
1092 .await
1093 .expect_err("expired tokens are refused");
1094
1095 assert_eq!(ctx.caller_identity(), None);
1096 }
1097
1098 #[tokio::test]
1099 async fn rs256_from_jwks_json_roundtrip() {
1100 let jwks_json = format!(
1101 r#"{{"keys":[{{"kty":"RSA","kid":"rk1","use":"sig","n":"{RS256_N}","e":"{RS256_E}"}}]}}"#
1102 );
1103 let jwks = Jwks::from_json(jwks_json.as_bytes()).unwrap();
1104 let i = JwtAuthInterceptor::new(base_validator(), jwks);
1105 assert!(i.before(&ctx_bearer(RS256_VALID)).await.is_ok());
1106 }
1107
1108 #[tokio::test]
1109 async fn algorithm_confusion_rejected() {
1110 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1116 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_err());
1117 }
1118
1119 #[test]
1120 fn alg_none_is_rejected() {
1121 let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"none","typ":"JWT"}"#);
1123 let claims = URL_SAFE_NO_PAD
1124 .encode(br#"{"iss":"https://issuer.test","aud":"a2a-agent","exp":253402300799}"#);
1125 let token = format!("{header}.{claims}.");
1126 let outcome = base_validator().validate(&token, &rsa_jwks());
1127 assert!(matches!(outcome, Err(ValidateOutcome::Rejected)));
1128 }
1129
1130 #[tokio::test]
1133 async fn es256_valid_and_expired() {
1134 let jwks = Jwks::new().with_ec_p256("ek1", ES256_X, ES256_Y).unwrap();
1135 let i = JwtAuthInterceptor::new(base_validator(), jwks);
1136 assert!(i.before(&ctx_bearer(ES256_VALID)).await.is_ok());
1137 assert!(i.before(&ctx_bearer(ES256_EXPIRED)).await.is_err());
1138 }
1139
1140 #[tokio::test]
1143 async fn audience_and_issuer_optional_when_unset() {
1144 let secret = URL_SAFE_NO_PAD.decode(HS256_SECRET_B64).unwrap();
1146 let v = JwtValidator::new().with_hs256_secret(secret);
1147 let i = JwtAuthInterceptor::new(v, Jwks::new());
1148 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_ok());
1150 }
1151
1152 #[tokio::test]
1153 async fn missing_authorization_header_rejected() {
1154 let secret = URL_SAFE_NO_PAD.decode(HS256_SECRET_B64).unwrap();
1155 let v = base_validator().with_hs256_secret(secret);
1156 let i = JwtAuthInterceptor::new(v, Jwks::new());
1157 assert!(i.before(&CallContext::new("m")).await.is_err());
1158 assert!(i
1159 .before(&CallContext::new("m").with_http_header("authorization", "Basic x"))
1160 .await
1161 .is_err());
1162 }
1163
1164 #[test]
1167 fn der_uint_prepends_zero_when_high_bit_set() {
1168 assert_eq!(der_uint(&[0x80]), vec![0x02, 0x02, 0x00, 0x80]);
1170 assert_eq!(der_uint(&[0x7f]), vec![0x02, 0x01, 0x7f]);
1172 assert_eq!(der_uint(&[0x00, 0x01]), vec![0x02, 0x01, 0x01]);
1174 }
1175
1176 #[test]
1177 fn der_tlv_long_form_length() {
1178 let content = vec![0xabu8; 300];
1179 let tlv = der_tlv(0x04, &content);
1180 assert_eq!(&tlv[..4], &[0x04, 0x82, 0x01, 0x2c]);
1182 assert_eq!(tlv.len(), 4 + 300);
1183 }
1184
1185 #[test]
1188 fn jwks_skips_enc_and_unknown_keys() {
1189 let json = format!(
1190 r#"{{"keys":[
1191 {{"kty":"RSA","kid":"enc1","use":"enc","n":"{RS256_N}","e":"{RS256_E}"}},
1192 {{"kty":"oct","kid":"sym","k":"abc"}},
1193 {{"kty":"EC","crv":"P-384","kid":"e384","x":"{ES256_X}","y":"{ES256_Y}"}},
1194 {{"kty":"RSA","kid":"sig1","use":"sig","n":"{RS256_N}","e":"{RS256_E}"}}
1195 ]}}"#
1196 );
1197 let jwks = Jwks::from_json(json.as_bytes()).unwrap();
1198 assert_eq!(jwks.keys.len(), 1);
1200 assert_eq!(jwks.keys[0].kid.as_deref(), Some("sig1"));
1201 }
1202
1203 #[test]
1206 fn jwks_is_empty_reflects_key_count() {
1207 assert!(Jwks::new().is_empty(), "a fresh key set is empty");
1208 assert!(!rsa_jwks().is_empty(), "a key set with a key is not empty");
1209 }
1210
1211 #[test]
1212 fn jwks_from_json_loads_ec_p256_key() {
1213 let json = format!(
1217 r#"{{"keys":[{{"kty":"EC","crv":"P-256","kid":"ek1","x":"{ES256_X}","y":"{ES256_Y}"}}]}}"#
1218 );
1219 let jwks = Jwks::from_json(json.as_bytes()).unwrap();
1220 assert_eq!(jwks.keys.len(), 1, "the P-256 key must be loaded");
1221 assert_eq!(jwks.keys[0].kid.as_deref(), Some("ek1"));
1222 }
1223
1224 #[test]
1225 fn ec_p256_rejects_wrong_length_coordinate() {
1226 let ok_y = ES256_Y;
1229 let short_x = URL_SAFE_NO_PAD.encode([0u8; 31]);
1230 assert!(
1231 Jwks::new().with_ec_p256("k", &short_x, ok_y).is_err(),
1232 "a 31-byte x coordinate must be rejected"
1233 );
1234 let long_y = URL_SAFE_NO_PAD.encode([0u8; 33]);
1235 assert!(
1236 Jwks::new().with_ec_p256("k", ES256_X, &long_y).is_err(),
1237 "a 33-byte y coordinate must be rejected"
1238 );
1239 assert!(Jwks::new().with_ec_p256("k", ES256_X, ES256_Y).is_ok());
1241 }
1242
1243 #[test]
1246 fn debug_impls_render_type_and_redact_secrets() {
1247 let jwks_dbg = format!("{:?}", rsa_jwks());
1251 assert!(jwks_dbg.contains("Jwks"), "Jwks Debug: {jwks_dbg}");
1252 assert!(jwks_dbg.contains("keys"), "Jwks Debug lists key count");
1253
1254 let secret = b"super-secret-value-1234567890";
1255 let validator = base_validator().with_hs256_secret(secret.to_vec());
1256 let v_dbg = format!("{validator:?}");
1257 assert!(
1258 v_dbg.contains("JwtValidator"),
1259 "JwtValidator Debug: {v_dbg}"
1260 );
1261 assert!(v_dbg.contains("redacted"), "the secret must be redacted");
1262 assert!(
1263 !v_dbg.contains("super-secret"),
1264 "the raw HS256 secret must never appear in Debug output"
1265 );
1266
1267 let interceptor = JwtAuthInterceptor::new(validator, rsa_jwks());
1268 let i_dbg = format!("{interceptor:?}");
1269 assert!(
1270 i_dbg.contains("JwtAuthInterceptor"),
1271 "JwtAuthInterceptor Debug: {i_dbg}"
1272 );
1273 assert!(i_dbg.contains("static"), "static key source is labelled");
1274 }
1275
1276 fn claims_at(exp: Option<u64>, nbf: Option<u64>) -> JwtClaims {
1279 JwtClaims {
1280 iss: None,
1281 sub: None,
1282 aud: None,
1283 exp,
1284 nbf,
1285 }
1286 }
1287
1288 #[test]
1289 fn check_claims_require_exp_boundary() {
1290 let strict = JwtValidator::new();
1292 assert!(
1293 strict
1294 .check_claims_at(&claims_at(None, None), 1_000)
1295 .is_err(),
1296 "no exp must be rejected when exp is required"
1297 );
1298 let lax = JwtValidator::new().allow_missing_exp();
1300 assert!(
1301 lax.check_claims_at(&claims_at(None, None), 1_000).is_ok(),
1302 "no exp must be accepted when exp is optional"
1303 );
1304 assert!(
1306 strict
1307 .check_claims_at(&claims_at(Some(2_000), None), 1_000)
1308 .is_ok(),
1309 "unexpired token passes"
1310 );
1311 assert!(
1312 strict
1313 .check_claims_at(&claims_at(Some(500), None), 1_000)
1314 .is_err(),
1315 "expired token fails (now past exp + leeway)"
1316 );
1317 }
1318
1319 #[test]
1320 fn check_claims_nbf_boundary_is_strict() {
1321 let v = JwtValidator::new()
1323 .allow_missing_exp()
1324 .with_leeway(std::time::Duration::ZERO);
1325 assert!(
1327 v.check_claims_at(&claims_at(None, Some(2_000)), 1_000)
1328 .is_err(),
1329 "a token whose nbf is in the future must be rejected"
1330 );
1331 assert!(
1333 v.check_claims_at(&claims_at(None, Some(1_000)), 1_000)
1334 .is_ok(),
1335 "a token is valid at exactly its nbf instant"
1336 );
1337 assert!(
1339 v.check_claims_at(&claims_at(None, Some(500)), 1_000)
1340 .is_ok(),
1341 "a token whose nbf is in the past is valid"
1342 );
1343 }
1344
1345 #[test]
1346 fn check_claims_exp_boundary_is_fail_closed() {
1347 let v = JwtValidator::new().with_leeway(std::time::Duration::ZERO);
1350 assert!(
1351 v.check_claims_at(&claims_at(Some(999), None), 1_000)
1352 .is_err(),
1353 "a token past its exp is expired"
1354 );
1355 assert!(
1357 v.check_claims_at(&claims_at(Some(1_000), None), 1_000)
1358 .is_err(),
1359 "a token is expired at exactly its exp instant"
1360 );
1361 assert!(
1363 v.check_claims_at(&claims_at(Some(1_001), None), 1_000)
1364 .is_ok(),
1365 "a token strictly before its exp is valid"
1366 );
1367 let lenient = JwtValidator::new().with_leeway(std::time::Duration::from_secs(60));
1369 assert!(
1370 lenient
1371 .check_claims_at(&claims_at(Some(1_000), None), 1_059)
1372 .is_ok(),
1373 "within leeway of exp: still valid"
1374 );
1375 assert!(
1376 lenient
1377 .check_claims_at(&claims_at(Some(1_000), None), 1_060)
1378 .is_err(),
1379 "at exactly exp + leeway: expired (fail-closed)"
1380 );
1381 }
1382
1383 #[test]
1384 fn cached_jwks_freshness_is_strict() {
1385 let ttl = std::time::Duration::from_secs(3600);
1386 assert!(
1387 cache_is_fresh(std::time::Duration::from_secs(3599), ttl),
1388 "an entry younger than its TTL is fresh"
1389 );
1390 assert!(
1392 !cache_is_fresh(ttl, ttl),
1393 "an entry at exactly its TTL is stale"
1394 );
1395 assert!(
1396 !cache_is_fresh(std::time::Duration::from_secs(3601), ttl),
1397 "an entry past its TTL is stale"
1398 );
1399 }
1400
1401 #[test]
1404 fn matching_kid_bad_signature_is_rejected_not_keymiss() {
1405 let outcome = base_validator().validate(RS256_WRONG_KEY, &rsa_jwks());
1409 assert!(
1410 matches!(outcome, Err(ValidateOutcome::Rejected)),
1411 "matched-kid bad-signature must be Rejected, got {outcome:?}"
1412 );
1413 }
1414
1415 #[test]
1416 fn no_kid_bad_signature_is_rejected_not_keymiss() {
1417 let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"RS256","typ":"JWT"}"#);
1420 let parts: Vec<&str> = RS256_VALID.split('.').collect();
1421 let token = format!("{header}.{}.{}", parts[1], parts[2]);
1424 let outcome = base_validator().validate(&token, &rsa_jwks());
1425 assert!(
1426 matches!(outcome, Err(ValidateOutcome::Rejected)),
1427 "no-kid bad-signature must be Rejected, got {outcome:?}"
1428 );
1429 }
1430
1431 #[test]
1432 fn unknown_kid_is_keymiss() {
1433 let outcome = base_validator().validate(RS256_UNKNOWN_KID, &rsa_jwks());
1435 assert!(
1436 matches!(outcome, Err(ValidateOutcome::KeyMiss)),
1437 "unknown-kid must be KeyMiss, got {outcome:?}"
1438 );
1439 }
1440
1441 #[test]
1444 fn der_tlv_short_and_long_form_lengths() {
1445 assert_eq!(&der_tlv(0x04, &[0u8; 5])[..2], &[0x04, 0x05]);
1447 assert_eq!(&der_tlv(0x04, &[0u8; 127])[..2], &[0x04, 0x7f]);
1448 assert_eq!(&der_tlv(0x04, &[0u8; 128])[..3], &[0x04, 0x81, 0x80]);
1451 assert_eq!(&der_tlv(0x04, &[0u8; 300])[..4], &[0x04, 0x82, 0x01, 0x2c]);
1453 assert_eq!(der_tlv(0x02, &[0xAA, 0xBB]), vec![0x02, 0x02, 0xAA, 0xBB]);
1455 }
1456
1457 #[test]
1460 fn jwks_body_size_limit() {
1461 assert!(!jwks_body_exceeds_limit(0, 200_000));
1464 assert!(!jwks_body_exceeds_limit(0, 256 * 1024));
1465 assert!(jwks_body_exceeds_limit(0, 256 * 1024 + 1));
1467 assert!(jwks_body_exceeds_limit(256 * 1024, 1));
1468 }
1469
1470 #[test]
1480 fn jwt_interceptor_declares_that_it_authenticates() {
1481 let interceptor = JwtAuthInterceptor::new(base_validator(), Jwks::new());
1482 assert!(
1483 interceptor.authenticates(),
1484 "a JWT auth interceptor must declare itself as one"
1485 );
1486
1487 let mut chain = crate::interceptor::ServerInterceptorChain::new();
1488 chain.push(std::sync::Arc::new(JwtAuthInterceptor::new(
1489 base_validator(),
1490 Jwks::new(),
1491 )));
1492 assert!(
1493 chain.has_authenticator(),
1494 "a chain guarded by a JWT interceptor must satisfy the \
1495 extended-agent-card authentication requirement"
1496 );
1497 }
1498}