Authentication¶
A2A v1.0 supports several authentication schemes. Phase 1 of the mesh A2A surface ships bearer only on both producer and consumer side. OAuth and mTLS are future work.
Bearer on the consumer side¶
Each runtime exposes a small bearer-credential type that resolves the token at call time (so a rotated env-var picks up the new value without re-decorating or restarting the agent).
import mesh
@app.tool()
@mesh.a2a_consumer(
capability="forecast",
a2a_url="https://upstream.example.com/agents/forecast",
a2a_skill_id="forecast-7day",
auth=mesh.A2ABearer(token_env="UPSTREAM_TOKEN"),
)
async def forecast(zip: str, _a2a: mesh.A2AClient = None) -> dict:
...
mesh.A2ABearer requires exactly one of token_env (env-var name; resolved per call) or token (literal) — supplying both raises RuntimeError, and supplying neither also raises. Omit the entire auth= argument from @mesh.a2a_consumer for upstreams that don't require authentication.
agent.addTool({
name: "forecast",
capability: "forecast",
tags: ["a2a-bridge"],
a2aConfig: {
url: "https://upstream.example.com/agents/forecast",
skillId: "forecast-7day",
auth: { tokenEnv: "UPSTREAM_TOKEN" },
},
parameters: z.object({ zip: z.string() }),
execute: async ({ zip }, ..._injected) => {
const a2a = _injected[0] as A2AClient;
...
},
});
auth accepts { tokenEnv: "..." } or { token: "..." } (literal). Mutually exclusive.
@MeshTool(capability = "forecast", tags = {"a2a-bridge"})
@A2AConsumer(
url = "https://upstream.example.com/agents/forecast",
skillId = "forecast-7day",
authBearerEnv = "UPSTREAM_TOKEN"
)
public Map<String, Object> forecast(
@Param("zip") String zip,
A2AClient a2a) throws Exception {
...
}
@A2AConsumer exposes authBearerEnv() (env-var name) and authBearerToken() (literal). Mutually exclusive — set zero or one, never both. Boot fails with a clear error if both are set.
Resolution at call time, not decoration time¶
All three runtimes resolve the env var at header-build time, NOT at decorator / annotation processing. Rotation flow:
- Set
UPSTREAM_TOKEN=v1and start the consumer agent. - Issue a few calls — the consumer's outbound A2A requests carry
Authorization: Bearer v1. - Update
UPSTREAM_TOKEN=v2(e.g. via a Kubernetes secret reload, a Vault sidecar, orexport). - Next call — the consumer's outbound request carries
Authorization: Bearer v2. No restart needed.
If the env var is unset and no literal token was supplied, the consumer raises a clear error at call time (A2ABearer: no token available ...) instead of silently sending an unauthenticated request.
Mutual exclusion¶
Across all three runtimes: literal token and env-var token are mutually exclusive. If you opt into bearer auth, supply exactly one — never both. To declare a no-auth upstream, omit the auth parameter entirely (@mesh.a2a_consumer(...) without auth=, @A2AConsumer without authBearerEnv/authBearerToken, a2aConfig without auth).
| Runtime | Validation site |
|---|---|
| Python | Constructor of mesh.A2ABearer |
| TypeScript | addTool registration validation |
| Java | Spring starter at boot (A2AConsumerBeanPostProcessor) |
Misuse surfaces at startup, not at first call.
Card-driven auth detection (scaffolding)¶
When you scaffold a consumer from an upstream card with meshctl scaffold a2a-consumer --url ..., the scaffolder reads card.authentication.schemes and:
- Wires the bearer auth block into the generated code if bearer is advertised.
- Sets the env-var name to the
--auth-envflag value (defaults toA2A_BEARER_TOKEN). - Generates a TODO comment in the README directing the operator to
export A2A_BEARER_TOKEN=...before running the agent.
If the card declares no authentication scheme, no auth block is generated. See Scaffolding.
Producer-side bearer¶
On the producer side, mesh.a2a.mount(...) accepts auth="bearer" (a literal string, not an A2ABearer instance). This advertises the bearer scheme on the published agent card and gates the JSON-RPC entry route — incoming requests must carry an Authorization: Bearer ... header to pass the gate. Phase 1 does NOT validate the token value itself: the producer-side auth= parameter takes only the string "bearer" (or None for no auth), and downstream verification of the token is left to the user (e.g., a FastAPI dependency, a sidecar, or the broader auth stack). The full token-resolution model — token literal vs token_env — lives on the consumer side via mesh.A2ABearer. Card auth schemes are auto-published in /.well-known/agent.json so external scaffolders (mesh's own or third-party A2A tooling) can detect bearer requirements automatically. Token-value validation on the producer side is deferred to a future phase. See Producer (Python).
Phase 1 scope and deferred work¶
| Scheme | Phase 1 status |
|---|---|
| Bearer | Shipped |
| OAuth 2 | Future work |
| mTLS | Future work |
| API key | Future work |
For non-bearer schemes today the workaround is to handle the auth headers in your own outbound request and bypass A2AClient — but that loses the framework injection / lifecycle / caching benefits. Track the A2A consumer arc on GitHub for the auth scheme expansion.
See also¶
- Producer (Python) — producer-side bearer enforcement
- Scaffolding — card-driven auth wiring