turul-a2a-auth 0.1.17

Auth middleware for turul-a2a: Bearer/JWT and API Key
Documentation

turul-a2a-auth

Authentication middleware for the turul-a2a server framework.

Two ready-made middlewares implementing the A2aMiddleware trait:

  • BearerMiddleware — JWT verification with JWKS caching, backed by turul-jwt-validator.
  • ApiKeyMiddleware — Static API key enforcement on a configurable header (default X-API-Key).

Both populate RequestContext.identity with AuthIdentity::Authenticated, which the framework surfaces to executors via ExecutionContext. Both also publish a SecurityContribution so the framework merges scheme declarations into the agent card automatically.

API key

use std::collections::HashMap;
use std::sync::Arc;
use turul_a2a::A2aServer;
use turul_a2a_auth::{ApiKeyMiddleware, RedactedApiKeyLookup};

let mut keys = HashMap::new();
keys.insert("ak_live_abc123".into(), "alice".into());
keys.insert("ak_live_def456".into(), "bob".into());

let lookup = Arc::new(RedactedApiKeyLookup::new(keys));
let auth = ApiKeyMiddleware::new(lookup, "X-API-Key");

A2aServer::builder()
    .executor(MyAgent)
    .middleware(Arc::new(auth))
    .build()?;

RedactedApiKeyLookup is a reference implementation whose Debug output never contains key material — important if you ever log the middleware. Plug in your own ApiKeyLookup for database-backed resolution.

use async_trait::async_trait;
use turul_a2a_auth::ApiKeyLookup;

struct DbKeyLookup { /* ... */ }

#[async_trait]
impl ApiKeyLookup for DbKeyLookup {
    async fn lookup(&self, key: &str) -> Option<String> {
        // Resolve key → owner string, or None if invalid.
        // None triggers a 401 with no information leak about which key was tried.
        unimplemented!()
    }
}

Bearer JWT

use std::sync::Arc;
use turul_a2a::A2aServer;
use turul_a2a_auth::BearerMiddleware;
use turul_jwt_validator::JwtValidator;

let validator = Arc::new(
    JwtValidator::new(/* issuer, JWKS URL, audience, … */)
);

let auth = BearerMiddleware::new(validator)
    .with_principal_claim("sub")              // default
    .with_required_scopes(vec!["a2a.read".into()]);

A2aServer::builder()
    .executor(MyAgent)
    .middleware(Arc::new(auth))
    .build()?;

JWKS is fetched and cached by JwtValidator. The middleware extracts the principal from the configured claim (default sub) and rejects empty/whitespace values.

Validator failures collapse to InvalidToken deliberately — the underlying error never reaches the response, to avoid leaking JWKS URLs or token fragments via error_description.

Reading identity in your executor

use turul_a2a::prelude::*;
use turul_a2a::middleware::AuthIdentity;

#[async_trait::async_trait]
impl AgentExecutor for MyAgent {
    async fn execute(&self, task: &mut Task, message: &Message, ctx: &ExecutionContext)
        -> Result<(), A2aError>
    {
        let owner = match &ctx.identity {
            AuthIdentity::Authenticated { owner, .. } => owner.as_str(),
            AuthIdentity::Anonymous => "anonymous",
        };
        task.push_text_artifact("ok", "Reply", format!("hi {owner}"));
        task.complete();
        Ok(())
    }

    fn agent_card(&self) -> turul_a2a_proto::AgentCard {
        AgentCardBuilder::new("My Agent", "1.0.0").build().unwrap()
    }
}

For Bearer, the full claims set is also available as AuthIdentity::Authenticated { claims, .. } (a serde_json::Value).

Security declarations on the agent card

Each middleware publishes a SecurityContribution so the framework's agent card auto-includes the matching SecurityScheme and any required scopes. Adopters do not need to hand-write security_schemes on the card; the framework merges contributions at build time.

Failure modes

Cause HTTP status Auth header behaviour
No Authorization / API key header 401 WWW-Authenticate challenge
Invalid token / unknown key 401 error="invalid_token" (Bearer) or no detail (API key)
Empty principal claim 401 error="invalid_token"
Required scope missing (Bearer) 403 error="insufficient_scope"

Errors are emitted at the transport layer (outside A2aError) so they never leak through the JSON-RPC envelope.

Examples

  • examples/auth-agent — runnable agent with API key middleware.
  • examples/skill-security-agent — declares per-skill security requirements on the card.

See the workspace README for the project overview and crate map.

License

Licensed under either MIT or Apache 2.0 at your option.