Skip to content

feat(server): support A2A protocol#2656

Open
Tyooughtul wants to merge 2 commits intoapache:masterfrom
Tyooughtul:feat/server/a2a-jwt-jwks
Open

feat(server): support A2A protocol#2656
Tyooughtul wants to merge 2 commits intoapache:masterfrom
Tyooughtul:feat/server/a2a-jwt-jwks

Conversation

@Tyooughtul
Copy link

Which issue does this PR close?

Closes #1762

Rationale

A2A protocol requires JWKS support to enable secure agent authentication with multiple identity providers. This change allows agents from different tenants to authenticate using their own public keys, and supports key rotation without requiring server restarts.

What changed?

Added JWKS support for secure agent-to-agent authentication. The implementation includes a JwksClient that fetches and caches public keys from JWKS endpoints, integrated JWKS into JwtManager for multi-tenant agent authentication, and updated HTTP middleware to support asynchronous JWT decoding. Also added TrustedIssuerConfig to support configuring multiple trusted issuers.

Local Execution

  • Passed
  • Pre-commit hooks ran

AI Usage

  1. Which tools? Grok fast
  2. Scope of usage?
  • I use ai for write test case and running scripts.
  • Some config code to test code:
# Trusted issuers for A2A (Application-to-Application) authentication
[[http.jwt.trusted_issuers]]
issuer = "test-issuer"
jwks_url = "http://127.0.0.1:8081/.well-known/jwks.json"
audience = "iggy.apache.org"
  • Some debug! to help me find bugs。
  1. How did you verify the generated code works correctly?
  • Compile successfully with cargo check --package server and cargo build --package server.
  • Test case passed.
  1. Can you explain every line of the code if asked? Yes

@Tyooughtul Tyooughtul closed this Jan 31, 2026
@Tyooughtul Tyooughtul reopened this Jan 31, 2026
@hubcio
Copy link
Contributor

hubcio commented Jan 31, 2026

hey! thanks for contribution - we'll check this after the weekend.


#[derive(Debug, Deserialize)]
struct Jwk {
kty: String,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make it an enum, you could use strum with all the helpers like here:

#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize)]
#[strum(serialize_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum McpTransport {
    #[default]
    #[strum(to_string = "http")]
    Http,
    #[strum(to_string = "stdio")]
    Stdio,
}

Then the match you do later on becomes easier.

kid: &str,
) -> Result<DecodingKey, anyhow::Error> {
if let Err(e) = self.refresh_keys(issuer, jwks_url).await {
return Err(anyhow::anyhow!("Failed to refresh keys: {}", e));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the existing custom error enums we already have - feel free to add new custom variants if needed or reuse existing ones.


#[derive(Debug, Clone)]
pub struct JwksClient {
cache: Arc<IggyRwLock<HashMap<CacheKey, DecodingKey>>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just use DashMap instead?

.ok_or_else(|| anyhow::anyhow!("Key not found in cache after refresh"))
}

async fn refresh_keys(&self, issuer: &str, jwks_url: &str) -> Result<(), anyhow::Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here regarding all the errors, don't use anyhow, please fix in all places returning errors.


for key in jwks.keys {
if let Some(kid) = key.kid {
let decoding_key: DecodingKey = match key.kty.as_str() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you could match using the previously added enum, also makes sense to make the string lowercase first.

if let Some(config) = self.trusted_issuer.get(&insecure.claims.iss) {
debug!("Found trusted issuer config: {}", config.issuer);
if let Some(kid_str) = kid.as_deref() {
if let Some(decoding_key) = self
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please try to avoid multiple levels of if statements, better to return/break quickly whenever possible with let Some() or let Ok() patterns.

@spetz
Copy link
Contributor

spetz commented Feb 2, 2026

Thank you for the contribution, made a few comments here and there :)

Is that all required to fully support A2A, as you wrote that I'd close #1762 which is the full integration?

Also, is there a way to do the proper integration/e2e testing like e..g for existing MCP runtime to ensure it works well with A2A as the full transport?

rustls-pemfile = { workspace = true }
send_wrapper = { workspace = true }
serde = { workspace = true }
serde_json.workspace = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stick to serde_json = { workspace = true } etc. everywhere


Ok(())
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please extend this file with unit tests matching the existing conventions to ensure that at least the logic JwksClientlikerefresh_keys` or so is correct.

@Tyooughtul
Copy link
Author

Thanks for the review! All comments are clear and I will address every point as suggested.
I think this PR covers the full A2A support as mentioned in #1762. I will also add the corresponding integration/e2e tests for the MCP runtime & A2A.

@hubcio
Copy link
Contributor

hubcio commented Feb 3, 2026

when testing, see how iggy_harness macro is used for connectors in #2667 or mcp (already merged). we're in the middle of refactor to use it everywhere, so it'd be great if you could use it in your tests (assuming you'll write some tests for this A2A).

@hubcio hubcio changed the title feat(server): Support A2A protocol (apache#1762) feat(server): support A2A protocol Feb 3, 2026
@codecov
Copy link

codecov bot commented Feb 6, 2026

Codecov Report

❌ Patch coverage is 79.52381% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.60%. Comparing base (b53ef6d) to head (b85afac).

Files with missing lines Patch % Lines
core/server/src/http/jwt/jwks.rs 81.69% 20 Missing and 6 partials ⚠️
core/server/src/http/jwt/jwt_manager.rs 75.00% 14 Missing and 1 partial ⚠️
core/server/src/configs/defaults.rs 0.00% 1 Missing ⚠️
core/server/src/http/jwt/middleware.rs 85.71% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2656      +/-   ##
==========================================
+ Coverage   69.50%   69.60%   +0.09%     
==========================================
  Files         568      569       +1     
  Lines       55238    55430     +192     
  Branches    55238    55430     +192     
==========================================
+ Hits        38394    38581     +187     
  Misses      14967    14967              
- Partials     1877     1882       +5     
Flag Coverage Δ
rust 69.60% <79.52%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
core/common/src/error/iggy_error.rs 100.00% <ø> (ø)
core/server/src/configs/http.rs 66.66% <ø> (ø)
core/server/src/configs/defaults.rs 0.00% <0.00%> (ø)
core/server/src/http/jwt/middleware.rs 80.00% <85.71%> (+12.69%) ⬆️
core/server/src/http/jwt/jwt_manager.rs 61.25% <75.00%> (+7.12%) ⬆️
core/server/src/http/jwt/jwks.rs 81.69% <81.69%> (ø)

... and 10 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@hubcio
Copy link
Contributor

hubcio commented Feb 6, 2026

It looks like something went wrong with your rebase:
image

@Tyooughtul Tyooughtul force-pushed the feat/server/a2a-jwt-jwks branch from 95c2994 to bd98498 Compare February 6, 2026 16:05
- Support JWKS for A2A compliant secure agent authentication
- Enable key rotation without restarting the server
- Allow agents from different tenants to publish to the same Iggy bus
…ness macro

Extend `#[iggy_harness]` with `jwks_server(...)` attribute to support
declarative JWKS mock server setup, as suggested in review to follow
the harness macro convention used for MCP and connectors.
- Fix the problem as suggested
- Add `jwks_server(store_path = "...")` attribute to #[iggy_harness]
- Add `config_path` to server(...) for custom TOML via IGGY_CONFIG_PATH
- Start WireMock MockServer and inject trusted issuer env vars before
  server startup
- Add ServerHandle::add_env() for pre-start env var injection
- Add 4 e2e tests: valid_token, expired_token, unknown_issuer,
  missing_token with RSA key pair and JWKS fixtures
@Tyooughtul Tyooughtul force-pushed the feat/server/a2a-jwt-jwks branch from bd98498 to b85afac Compare February 6, 2026 16:11
@Tyooughtul
Copy link
Author

It looks like something went wrong with your rebase: image

😱 sorry, I fetched the wrong branch. I have corrected it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integration with Google Agent2Agent Protocol

3 participants