KubeMQ
ConnectorsCloudEventsHow-to guides

Authentication

Secure CloudEvents requests with JWT Bearer tokens — publish, subscribe, and ClientID resolution from claims.

The CloudEvents connector has no authentication of its own. It shares the auth middleware of the shared HTTP server with the REST, MCP, and A2A connectors, so a single JWT Bearer token secures every /ce/* endpoint.

Overview

When authentication is enabled on the KubeMQ server, every CloudEvents request — both publishing (POST /ce/send/*, POST /ce/queue/*) and subscribing (GET /ce/subscribe/*) — must carry a valid JWT in the Authorization header:

Authorization: Bearer <jwt-token>

Because /ce/* endpoints are standard HTTP (not JSON-RPC), an authentication failure returns HTTP 401 Unauthorized, unlike the MCP and A2A gateways, which return the JSON-RPC -32010 error code. When authentication is disabled, requests are accepted with synthetic anonymous claims and no header is required.

Token issuance and validation (shared secret vs OIDC provider, claim schema) are configured server-wide. See Auth & security for the full model — public routes, CORS, origin validation, and TLS/mTLS.

How it works

A CloudEvents request passes through the shared auth middleware before reaching the CE handler; the verified ClientID claim flows downstream and becomes the KubeMQ message identity.

The shared auth middleware verifies the Bearer token, then hands the resolved ClientID to the CloudEvents connector.

ClientID resolution

The KubeMQ ClientID attached to a published message is resolved with authentication taking priority over the event payload:

AuthenticationClientID source
DisabledCloudEvent source attribute
Enabled (valid token)JWT claims ClientIDoverrides source

When auth is enabled, the CloudEvent source is still preserved as the ce_source tag (so the event round-trips intact), but the KubeMQ ClientID is set from the verified token. This ensures identity is controlled by the authentication system rather than by client-supplied data. The same override applies to the client_id query parameter on queue and subscribe endpoints.

Authenticated publish

Add the Authorization header to any send request. The token format and header are identical across all CloudEvents endpoints — events, events-store, queues, commands, and queries.

curl -X POST http://localhost:9090/ce/send/event \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Content-Type: application/cloudevents+json" \
  -d '{
    "specversion": "1.0",
    "type": "com.example.order.created",
    "source": "order-service",
    "subject": "orders",
    "data": {"order_id": "12345", "amount": 99.99}
  }'
req, _ := http.NewRequest("POST", base+"/ce/send/event", strings.NewReader(string(body)))
req.Header.Set("Content-Type", "application/cloudevents+json")
req.Header.Set("Authorization", "Bearer "+os.Getenv("JWT_TOKEN"))

resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal("send event:", err)
}
defer resp.Body.Close()
headers, body = to_structured(event)
headers["Authorization"] = f"Bearer {os.environ['JWT_TOKEN']}"

resp = requests.post(
    f"{base}/ce/send/event",
    data=body,
    headers=dict(headers),
    timeout=10,
)
const message = HTTP.structured(event);
const resp = await fetch(`${base}/ce/send/event`, {
  method: 'POST',
  headers: {
    ...(message.headers as Record<string, string>),
    Authorization: `Bearer ${process.env.JWT_TOKEN}`,
  },
  body: message.body as string,
});
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create(base + "/ce/send/event"))
        .POST(HttpRequest.BodyPublishers.ofByteArray(body))
        .header("Content-Type", "application/cloudevents+json")
        .header("Authorization", "Bearer " + System.getenv("JWT_TOKEN"))
        .build();

HttpResponse<String> response = httpClient.send(request,
        HttpResponse.BodyHandlers.ofString());
using var content = new ByteArrayContent(eventBytes.ToArray());
content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType.ToString());

using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("JWT_TOKEN"));

var resp = await httpClient.PostAsync($"{base_}/ce/send/event", content);
headers, body = sdk.encode_event(event, structured_format: "json")
uri = URI("#{base}/ce/send/event")
Net::HTTP.start(uri.host, uri.port) do |http|
  req = Net::HTTP::Post.new(uri)
  req["Content-Type"] = headers["Content-Type"]
  req["Authorization"] = "Bearer #{ENV.fetch('JWT_TOKEN')}"
  req.body = body
  res = http.request(req)
end
let body = serde_json::to_string(&event)?;
let resp = client
    .post(format!("{}/ce/send/event", base))
    .header("Content-Type", "application/cloudevents+json")
    .header("Authorization", format!("Bearer {}", env::var("JWT_TOKEN")?))
    .body(body)
    .send()
    .await?;

Authenticated subscribe

SSE subscriptions are long-lived GET requests; the Bearer token is sent once when the stream is opened.

curl -N "http://localhost:9090/ce/subscribe/events?client_id=my-client&channel=orders" \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -H "Accept: text/event-stream"
req, _ := http.NewRequest("GET", sseURL, nil)
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Authorization", "Bearer "+os.Getenv("JWT_TOKEN"))

resp, err := http.DefaultClient.Do(req)
with requests.get(sse_url, stream=True, timeout=None,
                  headers={"Accept": "text/event-stream",
                           "Cache-Control": "no-cache",
                           "Authorization": f"Bearer {os.environ['JWT_TOKEN']}"}) as resp:
    for raw_line in resp.iter_lines(decode_unicode=True):
        ...
const es = new EventSource(sseUrl, {
  headers: { Authorization: `Bearer ${process.env.JWT_TOKEN}` },
});
es.addEventListener('cloudevent', (evt: MessageEvent) => {
  console.log(JSON.parse(evt.data));
});
HttpURLConnection conn = (HttpURLConnection) new URL(sseUrl).openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "text/event-stream");
conn.setRequestProperty("Cache-Control", "no-cache");
conn.setRequestProperty("Authorization", "Bearer " + System.getenv("JWT_TOKEN"));
conn.setDoInput(true);
using var request = new HttpRequestMessage(HttpMethod.Get, sseUrl);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
request.Headers.Authorization =
    new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("JWT_TOKEN"));

using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
Net::HTTP.start(uri.host, uri.port) do |http|
  req = Net::HTTP::Get.new(uri)
  req["Accept"] = "text/event-stream"
  req["Cache-Control"] = "no-cache"
  req["Authorization"] = "Bearer #{ENV.fetch('JWT_TOKEN')}"
  http.request(req) do |resp|
    ...
  end
end
let stream = client
    .get(&sub_url)
    .header("Accept", "text/event-stream")
    .header("Cache-Control", "no-cache")
    .header("Authorization", format!("Bearer {}", env::var("JWT_TOKEN")?))
    .send()
    .await?
    .bytes_stream();

Running without authentication

When authentication is disabled in the server configuration, the CloudEvents connector accepts every request with synthetic anonymous claims:

  • No Authorization header is required.
  • The CloudEvent source attribute is used directly as the KubeMQ ClientID.
  • The examples throughout the CloudEvents docs assume this mode unless stated otherwise.

Running without authentication is for development and trusted networks only. Enable JWT authentication before exposing the CloudEvents connector in production.

Was this page helpful?

On this page