#![cfg(all(feature = "auth-jwt", feature = "tls-rustls"))]
use std::sync::Arc;
use bytes::Bytes;
use http_body_util::Full;
use hyper::service::service_fn;
use hyper::Response;
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
use a2a_protocol_server::auth::jwt::{JwtAuthInterceptor, JwtValidator};
use a2a_protocol_server::call_context::CallContext;
use a2a_protocol_server::interceptor::ServerInterceptor;
#[allow(dead_code)]
mod vectors {
include!("../src/auth/jwt_test_vectors.rs");
}
use vectors::{RS256_E, RS256_N, RS256_VALID, RS256_WRONG_KEY};
struct TestCerts {
ca_cert_der: rustls_pki_types::CertificateDer<'static>,
server_cert_der: rustls_pki_types::CertificateDer<'static>,
server_key_der: rustls_pki_types::PrivateKeyDer<'static>,
}
fn generate_test_certs(san: &str) -> TestCerts {
let mut ca_params = rcgen::CertificateParams::new(vec![]).unwrap();
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
ca_params
.distinguished_name
.push(rcgen::DnType::CommonName, "JWKS E2E Test CA");
let ca_key = rcgen::KeyPair::generate().unwrap();
let ca_cert = ca_params.self_signed(&ca_key).unwrap();
let ca_issuer = rcgen::Issuer::new(ca_params, ca_key);
let mut server_params = rcgen::CertificateParams::new(vec![san.into()]).unwrap();
server_params
.distinguished_name
.push(rcgen::DnType::CommonName, san);
let server_key = rcgen::KeyPair::generate().unwrap();
let server_cert = server_params.signed_by(&server_key, &ca_issuer).unwrap();
TestCerts {
ca_cert_der: ca_cert.der().clone(),
server_cert_der: server_cert.der().clone(),
server_key_der: rustls_pki_types::PrivateKeyDer::try_from(server_key.serialize_der())
.unwrap(),
}
}
fn ring_provider() -> Arc<rustls::crypto::CryptoProvider> {
Arc::new(rustls::crypto::ring::default_provider())
}
async fn start_https_jwks(certs: &TestCerts, jwks_body: String) -> std::net::SocketAddr {
let server_config = rustls::ServerConfig::builder_with_provider(ring_provider())
.with_safe_default_protocol_versions()
.expect("ring provider supports default protocol versions")
.with_no_client_auth()
.with_single_cert(
vec![certs.server_cert_der.clone()],
certs.server_key_der.clone_key(),
)
.expect("build server TLS config");
let acceptor = TlsAcceptor::from(Arc::new(server_config));
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("local addr");
tokio::spawn(async move {
loop {
let Ok((stream, _)) = listener.accept().await else {
break;
};
let acceptor = acceptor.clone();
let jwks_body = jwks_body.clone();
tokio::spawn(async move {
let Ok(tls_stream) = acceptor.accept(stream).await else {
return;
};
let io = TokioIo::new(tls_stream);
let service = service_fn(move |_req| {
let body = jwks_body.clone();
async move {
Ok::<_, std::convert::Infallible>(
Response::builder()
.status(200)
.header("content-type", "application/json")
.body(Full::new(Bytes::from(body)))
.unwrap(),
)
}
});
let _ = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection(io, service)
.await;
});
}
});
addr
}
fn client_config_trusting(ca: &rustls_pki_types::CertificateDer<'static>) -> rustls::ClientConfig {
let mut roots = rustls::RootCertStore::empty();
roots.add(ca.clone()).expect("add test CA");
rustls::ClientConfig::builder_with_provider(ring_provider())
.with_safe_default_protocol_versions()
.expect("ring provider supports default protocol versions")
.with_root_certificates(roots)
.with_no_client_auth()
}
fn jwks_json(kid: &str) -> String {
format!(
r#"{{"keys":[{{"kty":"RSA","kid":"{kid}","use":"sig","n":"{RS256_N}","e":"{RS256_E}"}}]}}"#
)
}
fn validator() -> JwtValidator {
JwtValidator::new()
.with_issuer("https://issuer.test")
.with_audience("a2a-agent")
}
fn ctx_with_token(token: &str) -> CallContext {
let mut headers = std::collections::HashMap::new();
headers.insert("authorization".to_owned(), format!("Bearer {token}"));
CallContext::new("SendMessage").with_http_headers(headers)
}
#[tokio::test]
async fn jwks_fetched_over_real_tls_validates_tokens() {
let certs = generate_test_certs("localhost");
let addr = start_https_jwks(&certs, jwks_json("rk1")).await;
let interceptor = JwtAuthInterceptor::from_jwks_url_with_tls_config(
validator(),
format!("https://localhost:{}/jwks", addr.port()),
client_config_trusting(&certs.ca_cert_der),
);
let ok = interceptor.before(&ctx_with_token(RS256_VALID)).await;
assert!(
ok.is_ok(),
"valid token must pass after a TLS JWKS fetch: {ok:?}"
);
let rejected = interceptor.before(&ctx_with_token(RS256_WRONG_KEY)).await;
assert!(
rejected.is_err(),
"wrong-key token must be rejected: {rejected:?}"
);
}
#[tokio::test]
async fn jwks_from_untrusted_certificate_fails_closed() {
let server_certs = generate_test_certs("localhost");
let addr = start_https_jwks(&server_certs, jwks_json("rk1")).await;
let other_ca = generate_test_certs("localhost");
let interceptor = JwtAuthInterceptor::from_jwks_url_with_tls_config(
validator(),
format!("https://localhost:{}/jwks", addr.port()),
client_config_trusting(&other_ca.ca_cert_der),
);
let result = interceptor.before(&ctx_with_token(RS256_VALID)).await;
assert!(
result.is_err(),
"an untrusted JWKS endpoint must fail closed: {result:?}"
);
}