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;
45use base64::engine::general_purpose::URL_SAFE_NO_PAD;
46use ring::signature;
47
48use a2a_protocol_types::error::{A2aError, A2aResult};
49
50use super::{AuthenticatedPrincipal, BEARER_CHALLENGE, auth_rejected, extract_bearer};
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 pub(crate) 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 && now.saturating_add(leeway) < nbf
467 {
468 return Err(()); }
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))]
491pub(crate) enum 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(BEARER_CHALLENGE))?;
675 let token = extract_bearer(header).ok_or_else(|| auth_rejected(BEARER_CHALLENGE))?;
676
677 match &self.keys {
678 KeySource::Static(jwks) => self
679 .validator
680 .validate(token, jwks)
681 .map_err(|_| auth_rejected(BEARER_CHALLENGE)),
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(BEARER_CHALLENGE))
692 }
693 Err(ValidateOutcome::Rejected) => Err(auth_rejected(BEARER_CHALLENGE)),
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 && let Some(jwks) = self.cached_fresh() {
741 return Ok(jwks);
742 }
743 let _guard = self.refresh_lock.lock().await;
744 if !force && let Some(jwks) = self.cached_fresh() {
747 return Ok(jwks);
748 }
749 let jwks = self.fetch().await?;
750 *self
751 .cache
752 .write()
753 .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(CachedJwks {
754 jwks: jwks.clone(),
755 fetched_at: std::time::Instant::now(),
756 });
757 Ok(jwks)
758 }
759
760 fn cached_fresh(&self) -> Option<Jwks> {
761 let guard = self
762 .cache
763 .read()
764 .unwrap_or_else(std::sync::PoisonError::into_inner);
765 guard.as_ref().and_then(|c| {
766 if cache_is_fresh(c.fetched_at.elapsed(), self.ttl) {
767 Some(c.jwks.clone())
768 } else {
769 None
770 }
771 })
772 }
773
774 async fn fetch(&self) -> A2aResult<Jwks> {
775 let body = http_get_json(&self.client, &self.url, "JWKS").await?;
776 let jwks = Jwks::from_json(&body)?;
777 if jwks.is_empty() {
778 return Err(A2aError::internal("JWKS endpoint returned no usable keys"));
779 }
780 Ok(jwks)
781 }
782}
783
784use http_body_util::Full;
787use hyper::body::Bytes;
788use hyper_util::client::legacy::Client;
789use hyper_util::client::legacy::connect::HttpConnector;
790use hyper_util::rt::TokioExecutor;
791
792const JWKS_FETCH_BUDGET: Duration = Duration::from_secs(30);
810
811#[cfg(not(feature = "tls-rustls"))]
812type JwksHttpClient = Client<HttpConnector, Full<Bytes>>;
813#[cfg(feature = "tls-rustls")]
814type JwksHttpClient = Client<hyper_rustls::HttpsConnector<HttpConnector>, Full<Bytes>>;
815
816fn build_jwks_client() -> JwksHttpClient {
822 #[cfg(not(feature = "tls-rustls"))]
823 {
824 let mut connector = HttpConnector::new();
825 connector.set_connect_timeout(Some(Duration::from_secs(10)));
826 Client::builder(TokioExecutor::new()).build(connector)
827 }
828 #[cfg(feature = "tls-rustls")]
829 {
830 let mut roots = rustls::RootCertStore::empty();
831 roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
832 let tls = rustls::ClientConfig::builder_with_provider(std::sync::Arc::new(
833 rustls::crypto::ring::default_provider(),
834 ))
835 .with_safe_default_protocol_versions()
836 .expect("ring provider supports the default protocol versions")
837 .with_root_certificates(roots)
838 .with_no_client_auth();
839 let https = hyper_rustls::HttpsConnectorBuilder::new()
840 .with_tls_config(tls)
841 .https_or_http()
842 .enable_http1()
843 .enable_http2()
844 .build();
845 Client::builder(TokioExecutor::new()).build(https)
846 }
847}
848
849async fn http_get_json(client: &JwksHttpClient, url: &str, what: &str) -> A2aResult<Vec<u8>> {
850 use http_body_util::BodyExt;
851
852 let req = hyper::Request::builder()
853 .method(hyper::Method::GET)
854 .uri(url)
855 .header("accept", "application/json")
856 .body(Full::new(Bytes::new()))
857 .map_err(|e| A2aError::internal(format!("{what} request build failed: {e}")))?;
858
859 let deadline = tokio::time::Instant::now() + JWKS_FETCH_BUDGET;
862
863 let resp = tokio::time::timeout_at(deadline, client.request(req))
864 .await
865 .map_err(|_| A2aError::internal(format!("{what} request timed out")))?
866 .map_err(|e| A2aError::internal(format!("{what} request failed: {e}")))?;
867
868 if !resp.status().is_success() {
869 return Err(A2aError::internal(format!(
870 "{what} endpoint returned HTTP {}",
871 resp.status()
872 )));
873 }
874
875 let mut body = resp.into_body();
878 let accumulate = async {
879 let mut collected: Vec<u8> = Vec::new();
880 while let Some(frame) = body.frame().await {
881 let frame =
882 frame.map_err(|e| A2aError::internal(format!("{what} body read failed: {e}")))?;
883 if let Some(chunk) = frame.data_ref() {
884 if jwks_body_exceeds_limit(collected.len(), chunk.len()) {
885 return Err(A2aError::internal(format!("{what} response too large")));
886 }
887 collected.extend_from_slice(chunk);
888 }
889 }
890 Ok(collected)
891 };
892
893 tokio::time::timeout_at(deadline, accumulate)
894 .await
895 .map_err(|_| A2aError::internal(format!("{what} body read timed out")))?
896}
897
898async fn discover_jwks_uri(issuer: &str) -> A2aResult<String> {
900 #[derive(serde::Deserialize)]
901 struct Discovery {
902 jwks_uri: Option<String>,
903 }
904 let url = format!(
905 "{}/.well-known/openid-configuration",
906 issuer.trim_end_matches('/')
907 );
908 let client = build_jwks_client();
909 let body = http_get_json(&client, &url, "OIDC discovery").await?;
910 let doc: Discovery = serde_json::from_slice(&body)
911 .map_err(|e| A2aError::internal(format!("OIDC discovery returned invalid JSON: {e}")))?;
912 doc.jwks_uri
913 .ok_or_else(|| A2aError::internal("OIDC discovery document has no jwks_uri"))
914}
915
916fn verify_asymmetric(alg: &str, key: &KeyMaterial, msg: &[u8], sig: &[u8]) -> bool {
919 match (alg, key) {
920 ("RS256", KeyMaterial::Rsa(der)) => signature::UnparsedPublicKey::new(
921 &signature::RSA_PKCS1_2048_8192_SHA256,
922 der.as_slice(),
923 )
924 .verify(msg, sig)
925 .is_ok(),
926 ("ES256", KeyMaterial::EcP256(point)) => {
927 signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_FIXED, point.as_slice())
928 .verify(msg, sig)
929 .is_ok()
930 }
931 _ => false,
934 }
935}
936
937fn rsa_pkcs1_der(n: &[u8], e: &[u8]) -> Vec<u8> {
943 let mut body = der_uint(n);
944 body.extend(der_uint(e));
945 der_tlv(0x30, &body) }
947
948fn der_uint(bytes: &[u8]) -> Vec<u8> {
951 let start = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len());
954 let trimmed = &bytes[start..];
955 let mut content = Vec::with_capacity(trimmed.len() + 1);
956 if trimmed.first().is_none_or(|&b| b & 0x80 != 0) {
957 content.push(0x00);
958 }
959 content.extend_from_slice(trimmed);
960 der_tlv(0x02, &content)
961}
962
963fn der_tlv(tag: u8, content: &[u8]) -> Vec<u8> {
965 let mut out = vec![tag];
966 let len = content.len();
967 if len < 0x80 {
968 #[allow(clippy::cast_possible_truncation)]
969 out.push(len as u8);
970 } else {
971 let len_bytes = len.to_be_bytes();
972 let first_nonzero = len_bytes
975 .iter()
976 .position(|&b| b != 0)
977 .expect("len >= 0x80 has a non-zero big-endian byte");
978 let significant = &len_bytes[first_nonzero..];
979 #[allow(clippy::cast_possible_truncation)]
984 out.push(0x80 + significant.len() as u8);
985 out.extend_from_slice(significant);
986 }
987 out.extend_from_slice(content);
988 out
989}
990
991fn b64url(s: &str, what: &str) -> A2aResult<Vec<u8>> {
992 URL_SAFE_NO_PAD
993 .decode(s)
994 .map_err(|e| A2aError::invalid_params(format!("invalid base64url {what}: {e}")))
995}
996
997fn decode_json<T: serde::de::DeserializeOwned>(b64: &str) -> Result<T, ()> {
998 let bytes = URL_SAFE_NO_PAD.decode(b64).map_err(|_| ())?;
999 serde_json::from_slice(&bytes).map_err(|_| ())
1000}
1001
1002#[cfg(test)]
1005mod tests {
1006 use super::*;
1007
1008 include!("jwt_test_vectors.rs");
1012
1013 fn ctx_bearer(token: &str) -> CallContext {
1014 CallContext::new("message/send")
1015 .with_http_header("authorization", format!("Bearer {token}"))
1016 }
1017
1018 fn base_validator() -> JwtValidator {
1019 JwtValidator::new()
1020 .with_issuer("https://issuer.test")
1021 .with_audience("a2a-agent")
1022 }
1023
1024 #[tokio::test]
1027 async fn hs256_valid_and_rejections() {
1028 let secret = URL_SAFE_NO_PAD.decode(HS256_SECRET_B64).unwrap();
1029 let v = base_validator().with_hs256_secret(secret);
1030 let i = JwtAuthInterceptor::new(v, Jwks::new());
1031
1032 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_ok());
1033 assert!(i.before(&ctx_bearer(HS256_EXPIRED)).await.is_err());
1034 assert!(i.before(&ctx_bearer(HS256_WRONG_SECRET)).await.is_err());
1035 }
1036
1037 #[tokio::test]
1038 async fn hs256_without_configured_secret_is_rejected() {
1039 let i = JwtAuthInterceptor::new(base_validator(), Jwks::new());
1042 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_err());
1043 }
1044
1045 fn rsa_jwks() -> Jwks {
1048 Jwks::new().with_rsa("rk1", RS256_N, RS256_E).unwrap()
1049 }
1050
1051 #[tokio::test]
1052 async fn rs256_valid_and_rejections() {
1053 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1054
1055 assert!(i.before(&ctx_bearer(RS256_VALID)).await.is_ok());
1056 assert!(i.before(&ctx_bearer(RS256_EXPIRED)).await.is_err());
1057 assert!(i.before(&ctx_bearer(RS256_WRONG_KEY)).await.is_err());
1058 assert!(i.before(&ctx_bearer(RS256_WRONG_ISS)).await.is_err());
1059 assert!(i.before(&ctx_bearer(RS256_WRONG_AUD)).await.is_err());
1060 assert!(i.before(&ctx_bearer(RS256_UNKNOWN_KID)).await.is_err());
1061 }
1062
1063 #[tokio::test]
1071 async fn a_validated_token_records_its_subject_as_the_caller() {
1072 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1073
1074 let ctx = ctx_bearer(RS256_VALID);
1075 i.before(&ctx).await.expect("the vector token is valid");
1076
1077 let identity = ctx
1078 .caller_identity()
1079 .expect("a validated token establishes an identity");
1080 assert!(
1081 !RS256_VALID.contains(identity),
1082 "the identity must be the subject, not a slice of the token"
1083 );
1084 }
1085
1086 #[tokio::test]
1089 async fn a_rejected_token_records_no_caller() {
1090 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1091
1092 let ctx = ctx_bearer(RS256_EXPIRED);
1093 i.before(&ctx)
1094 .await
1095 .expect_err("expired tokens are refused");
1096
1097 assert_eq!(ctx.caller_identity(), None);
1098 }
1099
1100 #[tokio::test]
1101 async fn rs256_from_jwks_json_roundtrip() {
1102 let jwks_json = format!(
1103 r#"{{"keys":[{{"kty":"RSA","kid":"rk1","use":"sig","n":"{RS256_N}","e":"{RS256_E}"}}]}}"#
1104 );
1105 let jwks = Jwks::from_json(jwks_json.as_bytes()).unwrap();
1106 let i = JwtAuthInterceptor::new(base_validator(), jwks);
1107 assert!(i.before(&ctx_bearer(RS256_VALID)).await.is_ok());
1108 }
1109
1110 #[tokio::test]
1111 async fn algorithm_confusion_rejected() {
1112 let i = JwtAuthInterceptor::new(base_validator(), rsa_jwks());
1118 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_err());
1119 }
1120
1121 #[test]
1122 fn alg_none_is_rejected() {
1123 let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"none","typ":"JWT"}"#);
1125 let claims = URL_SAFE_NO_PAD
1126 .encode(br#"{"iss":"https://issuer.test","aud":"a2a-agent","exp":253402300799}"#);
1127 let token = format!("{header}.{claims}.");
1128 let outcome = base_validator().validate(&token, &rsa_jwks());
1129 assert!(matches!(outcome, Err(ValidateOutcome::Rejected)));
1130 }
1131
1132 #[tokio::test]
1135 async fn es256_valid_and_expired() {
1136 let jwks = Jwks::new().with_ec_p256("ek1", ES256_X, ES256_Y).unwrap();
1137 let i = JwtAuthInterceptor::new(base_validator(), jwks);
1138 assert!(i.before(&ctx_bearer(ES256_VALID)).await.is_ok());
1139 assert!(i.before(&ctx_bearer(ES256_EXPIRED)).await.is_err());
1140 }
1141
1142 #[tokio::test]
1145 async fn audience_and_issuer_optional_when_unset() {
1146 let secret = URL_SAFE_NO_PAD.decode(HS256_SECRET_B64).unwrap();
1148 let v = JwtValidator::new().with_hs256_secret(secret);
1149 let i = JwtAuthInterceptor::new(v, Jwks::new());
1150 assert!(i.before(&ctx_bearer(HS256_VALID)).await.is_ok());
1152 }
1153
1154 #[tokio::test]
1155 async fn missing_authorization_header_rejected() {
1156 let secret = URL_SAFE_NO_PAD.decode(HS256_SECRET_B64).unwrap();
1157 let v = base_validator().with_hs256_secret(secret);
1158 let i = JwtAuthInterceptor::new(v, Jwks::new());
1159 assert!(i.before(&CallContext::new("m")).await.is_err());
1160 assert!(
1161 i.before(&CallContext::new("m").with_http_header("authorization", "Basic x"))
1162 .await
1163 .is_err()
1164 );
1165 }
1166
1167 #[test]
1170 fn der_uint_prepends_zero_when_high_bit_set() {
1171 assert_eq!(der_uint(&[0x80]), vec![0x02, 0x02, 0x00, 0x80]);
1173 assert_eq!(der_uint(&[0x7f]), vec![0x02, 0x01, 0x7f]);
1175 assert_eq!(der_uint(&[0x00, 0x01]), vec![0x02, 0x01, 0x01]);
1177 }
1178
1179 #[test]
1180 fn der_tlv_long_form_length() {
1181 let content = vec![0xabu8; 300];
1182 let tlv = der_tlv(0x04, &content);
1183 assert_eq!(&tlv[..4], &[0x04, 0x82, 0x01, 0x2c]);
1185 assert_eq!(tlv.len(), 4 + 300);
1186 }
1187
1188 #[test]
1191 fn jwks_skips_enc_and_unknown_keys() {
1192 let json = format!(
1193 r#"{{"keys":[
1194 {{"kty":"RSA","kid":"enc1","use":"enc","n":"{RS256_N}","e":"{RS256_E}"}},
1195 {{"kty":"oct","kid":"sym","k":"abc"}},
1196 {{"kty":"EC","crv":"P-384","kid":"e384","x":"{ES256_X}","y":"{ES256_Y}"}},
1197 {{"kty":"RSA","kid":"sig1","use":"sig","n":"{RS256_N}","e":"{RS256_E}"}}
1198 ]}}"#
1199 );
1200 let jwks = Jwks::from_json(json.as_bytes()).unwrap();
1201 assert_eq!(jwks.keys.len(), 1);
1203 assert_eq!(jwks.keys[0].kid.as_deref(), Some("sig1"));
1204 }
1205
1206 #[test]
1209 fn jwks_is_empty_reflects_key_count() {
1210 assert!(Jwks::new().is_empty(), "a fresh key set is empty");
1211 assert!(!rsa_jwks().is_empty(), "a key set with a key is not empty");
1212 }
1213
1214 #[test]
1215 fn jwks_from_json_loads_ec_p256_key() {
1216 let json = format!(
1220 r#"{{"keys":[{{"kty":"EC","crv":"P-256","kid":"ek1","x":"{ES256_X}","y":"{ES256_Y}"}}]}}"#
1221 );
1222 let jwks = Jwks::from_json(json.as_bytes()).unwrap();
1223 assert_eq!(jwks.keys.len(), 1, "the P-256 key must be loaded");
1224 assert_eq!(jwks.keys[0].kid.as_deref(), Some("ek1"));
1225 }
1226
1227 #[test]
1228 fn ec_p256_rejects_wrong_length_coordinate() {
1229 let ok_y = ES256_Y;
1232 let short_x = URL_SAFE_NO_PAD.encode([0u8; 31]);
1233 assert!(
1234 Jwks::new().with_ec_p256("k", &short_x, ok_y).is_err(),
1235 "a 31-byte x coordinate must be rejected"
1236 );
1237 let long_y = URL_SAFE_NO_PAD.encode([0u8; 33]);
1238 assert!(
1239 Jwks::new().with_ec_p256("k", ES256_X, &long_y).is_err(),
1240 "a 33-byte y coordinate must be rejected"
1241 );
1242 assert!(Jwks::new().with_ec_p256("k", ES256_X, ES256_Y).is_ok());
1244 }
1245
1246 #[test]
1249 fn debug_impls_render_type_and_redact_secrets() {
1250 let jwks_dbg = format!("{:?}", rsa_jwks());
1254 assert!(jwks_dbg.contains("Jwks"), "Jwks Debug: {jwks_dbg}");
1255 assert!(jwks_dbg.contains("keys"), "Jwks Debug lists key count");
1256
1257 let secret = b"super-secret-value-1234567890";
1258 let validator = base_validator().with_hs256_secret(secret.to_vec());
1259 let v_dbg = format!("{validator:?}");
1260 assert!(
1261 v_dbg.contains("JwtValidator"),
1262 "JwtValidator Debug: {v_dbg}"
1263 );
1264 assert!(v_dbg.contains("redacted"), "the secret must be redacted");
1265 assert!(
1266 !v_dbg.contains("super-secret"),
1267 "the raw HS256 secret must never appear in Debug output"
1268 );
1269
1270 let interceptor = JwtAuthInterceptor::new(validator, rsa_jwks());
1271 let i_dbg = format!("{interceptor:?}");
1272 assert!(
1273 i_dbg.contains("JwtAuthInterceptor"),
1274 "JwtAuthInterceptor Debug: {i_dbg}"
1275 );
1276 assert!(i_dbg.contains("static"), "static key source is labelled");
1277 }
1278
1279 fn claims_at(exp: Option<u64>, nbf: Option<u64>) -> JwtClaims {
1282 JwtClaims {
1283 iss: None,
1284 sub: None,
1285 aud: None,
1286 exp,
1287 nbf,
1288 }
1289 }
1290
1291 #[test]
1292 fn check_claims_require_exp_boundary() {
1293 let strict = JwtValidator::new();
1295 assert!(
1296 strict
1297 .check_claims_at(&claims_at(None, None), 1_000)
1298 .is_err(),
1299 "no exp must be rejected when exp is required"
1300 );
1301 let lax = JwtValidator::new().allow_missing_exp();
1303 assert!(
1304 lax.check_claims_at(&claims_at(None, None), 1_000).is_ok(),
1305 "no exp must be accepted when exp is optional"
1306 );
1307 assert!(
1309 strict
1310 .check_claims_at(&claims_at(Some(2_000), None), 1_000)
1311 .is_ok(),
1312 "unexpired token passes"
1313 );
1314 assert!(
1315 strict
1316 .check_claims_at(&claims_at(Some(500), None), 1_000)
1317 .is_err(),
1318 "expired token fails (now past exp + leeway)"
1319 );
1320 }
1321
1322 #[test]
1323 fn check_claims_nbf_boundary_is_strict() {
1324 let v = JwtValidator::new()
1326 .allow_missing_exp()
1327 .with_leeway(std::time::Duration::ZERO);
1328 assert!(
1330 v.check_claims_at(&claims_at(None, Some(2_000)), 1_000)
1331 .is_err(),
1332 "a token whose nbf is in the future must be rejected"
1333 );
1334 assert!(
1336 v.check_claims_at(&claims_at(None, Some(1_000)), 1_000)
1337 .is_ok(),
1338 "a token is valid at exactly its nbf instant"
1339 );
1340 assert!(
1342 v.check_claims_at(&claims_at(None, Some(500)), 1_000)
1343 .is_ok(),
1344 "a token whose nbf is in the past is valid"
1345 );
1346 }
1347
1348 #[test]
1349 fn check_claims_exp_boundary_is_fail_closed() {
1350 let v = JwtValidator::new().with_leeway(std::time::Duration::ZERO);
1353 assert!(
1354 v.check_claims_at(&claims_at(Some(999), None), 1_000)
1355 .is_err(),
1356 "a token past its exp is expired"
1357 );
1358 assert!(
1360 v.check_claims_at(&claims_at(Some(1_000), None), 1_000)
1361 .is_err(),
1362 "a token is expired at exactly its exp instant"
1363 );
1364 assert!(
1366 v.check_claims_at(&claims_at(Some(1_001), None), 1_000)
1367 .is_ok(),
1368 "a token strictly before its exp is valid"
1369 );
1370 let lenient = JwtValidator::new().with_leeway(std::time::Duration::from_secs(60));
1372 assert!(
1373 lenient
1374 .check_claims_at(&claims_at(Some(1_000), None), 1_059)
1375 .is_ok(),
1376 "within leeway of exp: still valid"
1377 );
1378 assert!(
1379 lenient
1380 .check_claims_at(&claims_at(Some(1_000), None), 1_060)
1381 .is_err(),
1382 "at exactly exp + leeway: expired (fail-closed)"
1383 );
1384 }
1385
1386 #[test]
1387 fn cached_jwks_freshness_is_strict() {
1388 let ttl = std::time::Duration::from_secs(3600);
1389 assert!(
1390 cache_is_fresh(std::time::Duration::from_secs(3599), ttl),
1391 "an entry younger than its TTL is fresh"
1392 );
1393 assert!(
1395 !cache_is_fresh(ttl, ttl),
1396 "an entry at exactly its TTL is stale"
1397 );
1398 assert!(
1399 !cache_is_fresh(std::time::Duration::from_secs(3601), ttl),
1400 "an entry past its TTL is stale"
1401 );
1402 }
1403
1404 #[test]
1407 fn matching_kid_bad_signature_is_rejected_not_keymiss() {
1408 let outcome = base_validator().validate(RS256_WRONG_KEY, &rsa_jwks());
1412 assert!(
1413 matches!(outcome, Err(ValidateOutcome::Rejected)),
1414 "matched-kid bad-signature must be Rejected, got {outcome:?}"
1415 );
1416 }
1417
1418 #[test]
1419 fn no_kid_bad_signature_is_rejected_not_keymiss() {
1420 let header = URL_SAFE_NO_PAD.encode(br#"{"alg":"RS256","typ":"JWT"}"#);
1423 let parts: Vec<&str> = RS256_VALID.split('.').collect();
1424 let token = format!("{header}.{}.{}", parts[1], parts[2]);
1427 let outcome = base_validator().validate(&token, &rsa_jwks());
1428 assert!(
1429 matches!(outcome, Err(ValidateOutcome::Rejected)),
1430 "no-kid bad-signature must be Rejected, got {outcome:?}"
1431 );
1432 }
1433
1434 #[test]
1435 fn unknown_kid_is_keymiss() {
1436 let outcome = base_validator().validate(RS256_UNKNOWN_KID, &rsa_jwks());
1438 assert!(
1439 matches!(outcome, Err(ValidateOutcome::KeyMiss)),
1440 "unknown-kid must be KeyMiss, got {outcome:?}"
1441 );
1442 }
1443
1444 #[test]
1447 fn der_tlv_short_and_long_form_lengths() {
1448 assert_eq!(&der_tlv(0x04, &[0u8; 5])[..2], &[0x04, 0x05]);
1450 assert_eq!(&der_tlv(0x04, &[0u8; 127])[..2], &[0x04, 0x7f]);
1451 assert_eq!(&der_tlv(0x04, &[0u8; 128])[..3], &[0x04, 0x81, 0x80]);
1454 assert_eq!(&der_tlv(0x04, &[0u8; 300])[..4], &[0x04, 0x82, 0x01, 0x2c]);
1456 assert_eq!(der_tlv(0x02, &[0xAA, 0xBB]), vec![0x02, 0x02, 0xAA, 0xBB]);
1458 }
1459
1460 #[test]
1463 fn jwks_body_size_limit() {
1464 assert!(!jwks_body_exceeds_limit(0, 200_000));
1467 assert!(!jwks_body_exceeds_limit(0, 256 * 1024));
1468 assert!(jwks_body_exceeds_limit(0, 256 * 1024 + 1));
1470 assert!(jwks_body_exceeds_limit(256 * 1024, 1));
1471 }
1472
1473 #[test]
1483 fn jwt_interceptor_declares_that_it_authenticates() {
1484 let interceptor = JwtAuthInterceptor::new(base_validator(), Jwks::new());
1485 assert!(
1486 interceptor.authenticates(),
1487 "a JWT auth interceptor must declare itself as one"
1488 );
1489
1490 let mut chain = crate::interceptor::ServerInterceptorChain::new();
1491 chain.push(std::sync::Arc::new(JwtAuthInterceptor::new(
1492 base_validator(),
1493 Jwks::new(),
1494 )));
1495 assert!(
1496 chain.has_authenticator(),
1497 "a chain guarded by a JWT interceptor must satisfy the \
1498 extended-agent-card authentication requirement"
1499 );
1500 }
1501}