# Authentication (/aiway/a2a/guides/authentication)



The A2A connector is guarded by the same JWT Bearer authentication as every other
KubeMQ connector. You authenticate to the **gateway** (`/a2a/*`) and the **registry**
(`/agents/*`) with an `Authorization: Bearer` header; KubeMQ verifies the token,
records who you are, and propagates your identity to the agent as
`X-KubeMQ-Caller-ID` — without ever forwarding your token downstream.

## Overview [#overview]

Authentication for A2A is the connector-wide model described in
[Auth & Security](/connectors/reference/auth-and-security), applied to two route
families:

* **Gateway** — `POST /a2a/<agent_id>` and the SSE stream endpoint route requests to
  agents. A verified token identifies the caller; that identity becomes the agent's
  `X-KubeMQ-Caller-ID`.
* **Registry** — `POST /agents/register`, `/agents/heartbeat`, `/agents/deregister`,
  and `DELETE /agents/<agent_id>` are owned operations. The principal in your token
  becomes the agent's `registered_by`, and only that principal may later modify or
  delete it.

When server authentication is **disabled**, every caller is treated as the synthetic
`anonymous` principal and no token is required. When it is **enabled**, an unverified
or missing token is rejected — as a JSON-RPC `-32010` error on `/a2a/*`, or HTTP 401
on the REST registry endpoints.

<Callout type="info">
  A2A discovery is intentionally public: `GET /.well-known/agent-card.json` and any
  per-agent `/.well-known/agent-card.json` path bypass authentication so a caller can
  read an [agent card](/aiway/a2a/agent-cards) before it has a token.
</Callout>

## How it works [#how-it-works]

The token is verified once, at the shared auth middleware, before the request reaches
the gateway. The middleware attaches your claims (including `ClientID`) to the request;
the gateway uses that identity for registry ownership and stamps it onto the agent call
as `X-KubeMQ-Caller-ID`. The agent receives the caller's identity — never the raw
token.

<Mermaid
  chart="`
graph LR
CALLER[&#x22;A2A caller<br/>+ Bearer token&#x22;]
AUTH[&#x22;JWT auth middleware&#x22;]
GW[&#x22;A2A gateway<br/>:9090&#x22;]
VSUB[&#x22;Virtual subscriber&#x22;]
AGENT[&#x22;Agent (HTTP)<br/>+ X-KubeMQ-Caller-ID&#x22;]

CALLER --> AUTH
AUTH --> GW
GW --> VSUB
VSUB -. &#x22;token stripped&#x22; .-> AGENT

class CALLER client
class AUTH broker
class GW,VSUB aiway
class AGENT external
`"
/>

*KubeMQ verifies the token at the edge and forwards the caller's identity — not the token — to the agent.*

<Callout type="warn">
  The `Authorization` header is **stripped** before the gateway calls the agent — along
  with `Cookie`, `Proxy-Authorization`, and other sensitive or hop-by-hop headers. Agents
  must trust `X-KubeMQ-Caller-ID` for the caller's identity, not a forwarded token. If an
  agent needs its own credential, register it with the agent's URL and let the agent
  authenticate downstream itself.
</Callout>

## Authenticating gateway calls [#authenticating-gateway-calls]

Add an `Authorization: Bearer <jwt>` header to any `POST /a2a/<agent_id>` request. The
header carrying the token is consumed by KubeMQ; the rest of the request — including
any `X-*` headers — is forwarded to the agent. These snippets send a `message/send`
with a Bearer token; on success the agent echoes its `received_headers` and you can see
that `Authorization` is absent and `X-KubeMQ-Caller-ID` is present.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    curl -X POST http://localhost:9090/a2a/echo-agent-01 \
      -H "Authorization: Bearer $KUBEMQ_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "jsonrpc": "2.0",
        "id": 1,
        "method": "message/send",
        "params": {
          "message": {"parts": [{"text": "Authenticated call"}]}
        }
      }'
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    using System.Text;
    using System.Text.Json;
    using System.Text.Json.Nodes;

    const string KubeMqUrl = "http://localhost:9090";
    const string AgentId = "echo-agent-01";
    var token = Environment.GetEnvironmentVariable("KUBEMQ_TOKEN");

    var payload = new JsonObject
    {
        ["jsonrpc"] = "2.0",
        ["id"] = 1,
        ["method"] = "message/send",
        ["params"] = new JsonObject
        {
            ["message"] = new JsonObject
            {
                ["parts"] = new JsonArray(new JsonObject { ["text"] = "Authenticated call" })
            }
        }
    };

    using var client = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Post, $"{KubeMqUrl}/a2a/{AgentId}")
    {
        Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, "application/json")
    };
    request.Headers.Add("Authorization", $"Bearer {token}");

    var resp = await client.SendAsync(request);
    var body = await resp.Content.ReadAsStringAsync();
    var data = JsonNode.Parse(body)!;

    var received = data["result"]?["received_headers"];
    Console.WriteLine($"Status: {(int)resp.StatusCode}");
    Console.WriteLine($"Caller ID seen by agent: {received?["X-KubeMQ-Caller-ID"]?.GetValue<string>()}");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    package main

    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"io"
    	"net/http"
    	"os"
    )

    const (
    	kubemqURL = "http://localhost:9090"
    	agentID   = "echo-agent-01"
    )

    func main() {
    	token := os.Getenv("KUBEMQ_TOKEN")
    	payload := map[string]interface{}{
    		"jsonrpc": "2.0",
    		"id":      1,
    		"method":  "message/send",
    		"params": map[string]interface{}{
    			"message": map[string]interface{}{
    				"parts": []map[string]interface{}{{"text": "Authenticated call"}},
    			},
    		},
    	}

    	data, _ := json.Marshal(payload)
    	req, _ := http.NewRequest("POST", kubemqURL+"/a2a/"+agentID, bytes.NewReader(data))
    	req.Header.Set("Content-Type", "application/json")
    	req.Header.Set("Authorization", "Bearer "+token)

    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "Request failed: %v\n", err)
    		os.Exit(1)
    	}
    	defer resp.Body.Close()

    	body, _ := io.ReadAll(resp.Body)
    	var result map[string]interface{}
    	json.Unmarshal(body, &result)
    	received, _ := result["result"].(map[string]interface{})["received_headers"].(map[string]interface{})
    	fmt.Printf("Status: %d\n", resp.StatusCode)
    	fmt.Printf("Caller ID seen by agent: %v\n", received["X-KubeMQ-Caller-ID"])
    }
    ```
  </Tab>

  <Tab value="Java">
    ```java
    import com.fasterxml.jackson.databind.ObjectMapper;

    import java.net.URI;
    import java.net.http.HttpClient;
    import java.net.http.HttpRequest;
    import java.net.http.HttpResponse;
    import java.util.List;
    import java.util.Map;

    public class Client {

        static final String KUBEMQ_URL = "http://localhost:9090";
        static final String AGENT_ID = "echo-agent-01";
        static final ObjectMapper MAPPER = new ObjectMapper();

        public static void main(String[] args) throws Exception {
            var token = System.getenv("KUBEMQ_TOKEN");
            var payload = Map.of(
                "jsonrpc", "2.0",
                "id", 1,
                "method", "message/send",
                "params", Map.of(
                    "message", Map.of("parts", List.of(Map.of("text", "Authenticated call")))
                )
            );

            var client = HttpClient.newHttpClient();
            var req = HttpRequest.newBuilder()
                .uri(URI.create(KUBEMQ_URL + "/a2a/" + AGENT_ID))
                .header("Content-Type", "application/json")
                .header("Authorization", "Bearer " + token)
                .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
                .build();
            var resp = client.send(req, HttpResponse.BodyHandlers.ofString());

            var data = MAPPER.readTree(resp.body());
            var received = data.path("result").path("received_headers");
            System.out.println("Status: " + resp.statusCode());
            System.out.println("Caller ID seen by agent: " + received.path("X-KubeMQ-Caller-ID").asText());
        }
    }
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import json
    import os

    import httpx

    KUBEMQ_URL = "http://localhost:9090"
    AGENT_ID = "echo-agent-01"

    payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "message/send",
        "params": {
            "message": {"parts": [{"text": "Authenticated call"}]},
        },
    }

    token = os.environ["KUBEMQ_TOKEN"]
    async with httpx.AsyncClient() as client:
        resp = await client.post(
            f"{KUBEMQ_URL}/a2a/{AGENT_ID}",
            json=payload,
            headers={"Authorization": f"Bearer {token}"},
        )
        data = resp.json()
        received = data.get("result", {}).get("received_headers", {})
        print(f"Status: {resp.status_code}")
        print(f"Caller ID seen by agent: {received.get('X-KubeMQ-Caller-ID')}")
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const KUBEMQ_URL = "http://localhost:9090";
    const AGENT_ID = "echo-agent-01";
    const token = process.env.KUBEMQ_TOKEN;

    const request = {
      jsonrpc: "2.0",
      id: 1,
      method: "message/send",
      params: {
        message: { parts: [{ text: "Authenticated call" }] },
      },
    };

    const resp = await fetch(`${KUBEMQ_URL}/a2a/${AGENT_ID}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(request),
    });

    const data = await resp.json();
    const received = data.result?.received_headers || {};
    console.log(`Status: ${resp.status}`);
    console.log(`Caller ID seen by agent: ${received["x-kubemq-caller-id"]}`);
    ```
  </Tab>
</Tabs>

## Authenticating registry calls [#authenticating-registry-calls]

The registry uses the **same** `Authorization: Bearer` header. The difference is what
KubeMQ does with your identity: `POST /agents/register` records the token's principal
as the agent's `registered_by`, and ownership-protected operations
(`heartbeat`, `deregister`, `DELETE`) verify that the caller's principal matches.

<Tabs groupId="language" items="['curl','C#','Go','Java','Python','TypeScript']">
  <Tab value="curl">
    ```bash
    curl -X POST http://localhost:9090/agents/register \
      -H "Authorization: Bearer $KUBEMQ_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "agent_id": "echo-agent-01",
        "name": "Echo Agent",
        "url": "http://echo-agent.internal:8000/",
        "skill_tags": ["echo"]
      }'
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    const string KubeMqUrl = "http://localhost:9090";
    var token = Environment.GetEnvironmentVariable("KUBEMQ_TOKEN");

    var card = new JsonObject
    {
        ["agent_id"] = "echo-agent-01",
        ["name"] = "Echo Agent",
        ["url"] = "http://echo-agent.internal:8000/",
        ["skill_tags"] = new JsonArray("echo")
    };

    using var client = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Post, $"{KubeMqUrl}/agents/register")
    {
        Content = new StringContent(card.ToJsonString(), Encoding.UTF8, "application/json")
    };
    request.Headers.Add("Authorization", $"Bearer {token}");

    var resp = await client.SendAsync(request);
    var data = JsonNode.Parse(await resp.Content.ReadAsStringAsync())!;
    Console.WriteLine($"registered_by: {data["registered_by"]?.GetValue<string>()}");
    ```
  </Tab>

  <Tab value="Go">
    ```go
    token := os.Getenv("KUBEMQ_TOKEN")
    card := map[string]interface{}{
    	"agent_id":   "echo-agent-01",
    	"name":       "Echo Agent",
    	"url":        "http://echo-agent.internal:8000/",
    	"skill_tags": []string{"echo"},
    }

    data, _ := json.Marshal(card)
    req, _ := http.NewRequest("POST", "http://localhost:9090/agents/register", bytes.NewReader(data))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer "+token)

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    var result map[string]interface{}
    json.Unmarshal(body, &result)
    fmt.Printf("registered_by: %v\n", result["registered_by"])
    ```
  </Tab>

  <Tab value="Java">
    ```java
    var token = System.getenv("KUBEMQ_TOKEN");
    var card = Map.of(
        "agent_id", "echo-agent-01",
        "name", "Echo Agent",
        "url", "http://echo-agent.internal:8000/",
        "skill_tags", List.of("echo")
    );

    var client = HttpClient.newHttpClient();
    var req = HttpRequest.newBuilder()
        .uri(URI.create(KUBEMQ_URL + "/agents/register"))
        .header("Content-Type", "application/json")
        .header("Authorization", "Bearer " + token)
        .POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(card)))
        .build();
    var resp = client.send(req, HttpResponse.BodyHandlers.ofString());

    var data = MAPPER.readTree(resp.body());
    System.out.println("registered_by: " + data.path("registered_by").asText());
    ```
  </Tab>

  <Tab value="Python">
    ```python
    token = os.environ["KUBEMQ_TOKEN"]
    card = {
        "agent_id": "echo-agent-01",
        "name": "Echo Agent",
        "url": "http://echo-agent.internal:8000/",
        "skill_tags": ["echo"],
    }

    async with httpx.AsyncClient() as client:
        resp = await client.post(
            "http://localhost:9090/agents/register",
            json=card,
            headers={"Authorization": f"Bearer {token}"},
        )
        data = resp.json()
        print(f"registered_by: {data.get('registered_by')}")
    ```
  </Tab>

  <Tab value="TypeScript">
    ```typescript
    const token = process.env.KUBEMQ_TOKEN;
    const card = {
      agent_id: "echo-agent-01",
      name: "Echo Agent",
      url: "http://echo-agent.internal:8000/",
      skill_tags: ["echo"],
    };

    const resp = await fetch(`${KUBEMQ_URL}/agents/register`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(card),
    });

    const data = await resp.json();
    console.log(`registered_by: ${data.registered_by}`);
    ```
  </Tab>
</Tabs>

## Agent ownership [#agent-ownership]

When authentication is enabled, the registry enforces ownership so one principal cannot
hijack or delete another principal's agents.

* On `POST /agents/register`, the agent's `registered_by` is set automatically from the
  token's principal — clients cannot spoof it in the body.
* `heartbeat`, `deregister`, and `DELETE /agents/<id>` succeed only when the caller's
  principal matches the agent's `registered_by`. A mismatch returns **403 Forbidden**
  (ownership conflict).
* If an agent's `registered_by` is **blank** while auth is enabled, the ownership check
  **fails closed** — no principal can modify or delete it. Re-register the agent with a
  token to take ownership.

| Operation                                        | Auth                       | Ownership check                                           |
| ------------------------------------------------ | -------------------------- | --------------------------------------------------------- |
| `GET /.well-known/agent-card.json`               | Public                     | —                                                         |
| `GET /agents`, `GET /agents/<id>`                | Bearer (when auth enabled) | None (read-only)                                          |
| `POST /agents/register`                          | Bearer                     | Sets `registered_by`; rejects cross-principal re-register |
| `POST /agents/heartbeat`                         | Bearer                     | Caller must own the agent                                 |
| `POST /agents/deregister`, `DELETE /agents/<id>` | Bearer                     | Caller must own the agent                                 |
| `POST /a2a/<id>` (gateway)                       | Bearer                     | None (any authenticated caller may message any agent)     |

## Caller identity reaches the agent [#caller-identity-reaches-the-agent]

Because the gateway strips `Authorization&#x60; before calling the agent, the agent learns
who is calling from the &#x2A;*`X-KubeMQ-Caller-ID`** header, which the virtual subscriber
always injects with the caller's `ClientID`. This holds regardless of which transport
originated the call (A2A HTTP, gRPC, REST, or the MCP bridge), so an agent can apply its
own per-caller logic without parsing tokens.

| Header                  | Set by             | Visible to agent  | Contains                           |
| ----------------------- | ------------------ | ----------------- | ---------------------------------- |
| `Authorization: Bearer` | Caller             | **No** (stripped) | The caller's JWT                   |
| `X-KubeMQ-Caller-ID`    | Virtual subscriber | **Yes**           | The caller's `ClientID` (identity) |

<Callout type="info">
  When server authentication is disabled, the injected `X-KubeMQ-Caller-ID` reflects the
  synthetic `anonymous` identity — agents that gate on caller identity should account for
  this in non-secured deployments.
</Callout>

## Related [#related]

<Cards>
  <Card title="Auth & Security" href="/connectors/reference/auth-and-security" description="The shared JWT, CORS, origin-validation, and TLS model behind every connector." />

  <Card title="Agent registry" href="/aiway/a2a/registry" description="Register, list, heartbeat, and deregister agents — where ownership is enforced." />

  <Card title="Synchronous messaging" href="/aiway/a2a/sync-messaging" description="message/send and header forwarding through the gateway." />

  <Card title="Building agents" href="/aiway/a2a/guides/building-agents" description="Build a plain HTTP agent and trust X-KubeMQ-Caller-ID for caller identity." />
</Cards>
