# Authentication (/connectors/amqp/how-to/authentication)



This guide explains how a native AMQP 1.0 client proves *who it is* to the KubeMQ AMQP 1.0
connector (authentication / SASL) and *what it may do* once attached (authorization). It covers
the three SASL mechanisms, how the client identity (`ClientID`) is derived, why `container-id`
matters, and the audit events the connector emits.

<Callout type="info">
  On a stock dev broker, authentication is **off** and clients connect **ANONYMOUS** — so the
  examples clone-and-run with no credentials. SASL **PLAIN** with a KubeMQ JWT is the one
  credentialed mechanism that also runs on a stock broker; SASL **EXTERNAL** requires mTLS (see
  [TLS and mTLS](/connectors/amqp/how-to/tls-and-mtls)).
</Callout>

## The three SASL mechanisms [#the-three-sasl-mechanisms]

The connector computes the SASL mechanism list **per connection** from the auth and TLS context
and offers it in a fixed order: **EXTERNAL → PLAIN → ANONYMOUS**.

| Mechanism       | Offered when                                                        | Credential                                                                  | Identity (`ClientID`) becomes                                                                  |
| --------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| **`PLAIN`**     | **always**                                                          | RFC 4616 `authzid\x00authcid\x00passwd`; the **password is the KubeMQ JWT** | auth on → `ClientID` from the JWT; auth off → sanitized SASL username, else the `container-id` |
| **`ANONYMOUS`** | only when the auth service is **disabled**                          | none                                                                        | sanitized `container-id`                                                                       |
| **`EXTERNAL`**  | only on an **mTLS** connection with a *verified* client certificate | the certificate itself (no JWT)                                             | the cert **Subject CN**, sanitized                                                             |

### PLAIN — the documented contract [#plain--the-documented-contract]

PLAIN is the mechanism to use when authentication is enabled. The credential layout is the
standard RFC 4616 triple `authzid \x00 authcid \x00 passwd`:

* **`passwd` (password) = the KubeMQ JWT.** It is validated server-side; on success the
  connector takes the `ClientID` from the JWT's claims.
* **`authcid` (username) is audit-only / informational when auth is on.** It does **not** become
  the identity — the JWT does. (When auth is *off*, the username is used as a convenience
  identity; see precedence below.)
* The SASL initial-response binary is capped at **16 KiB**. A larger response is rejected as a
  malformed/hostile peer. A KubeMQ JWT fits comfortably.
* **No SCRAM-SHA-256, GSSAPI, or Azure CBS.** PLAIN is the only credentialed mechanism.

Most clients accept a `(username, password)` pair — put the JWT in the **password** slot:

<Tabs groupId="language" items="['Go','Python','Java','C#','JavaScript','Rust']">
  <Tab value="Go">
    ```go
    // Azure/go-amqp — SASL PLAIN: username is audit-only, password is the KubeMQ JWT.
    conn, err := amqp.Dial(ctx, "amqp://broker:5672", &amqp.ConnOptions{
        SASLType: amqp.SASLTypePlain("audit-username", os.Getenv("KUBEMQ_AMQP_JWT")),
    })
    if err != nil {
        log.Fatalf("dial (bad/expired JWT? auth-disabled broker?): %v", err)
    }
    defer conn.Close()
    ```
  </Tab>

  <Tab value="Python">
    ```python
    # python-qpid-proton — SASL PLAIN: username audit-only, password is the KubeMQ JWT.
    # allow_insecure_mechs permits PLAIN over plaintext amqp://; use amqps:// in production.
    conn = BlockingConnection(
        "amqp://broker:5672",
        user="audit-username",
        password=os.environ["KUBEMQ_AMQP_JWT"],
        allowed_mechs="PLAIN",
        allow_insecure_mechs=True,
    )
    ```
  </Tab>

  <Tab value="Java">
    ```java
    // qpid-jms — the username/password passed to createConnection become the SASL
    // PLAIN authcid/password (password = the KubeMQ JWT). Pin PLAIN on the URI.
    String url = "amqp://broker:5672?amqp.saslMechanisms=PLAIN";
    JmsConnectionFactory factory = new JmsConnectionFactory(url);
    Connection connection = factory.createConnection(
            "audit-username", System.getenv("KUBEMQ_AMQP_JWT"));
    connection.start();
    ```
  </Tab>

  <Tab value="C#">
    ```csharp
    // AMQPNetLite — an Address carrying User + Password makes the client negotiate PLAIN
    // (password = the KubeMQ JWT). The host/port/user/password ctor avoids URL-encoding it.
    var baseAddress = new Address("amqp://broker:5672");
    var connectAddress = new Address(
        baseAddress.Host, baseAddress.Port,
        "audit-username", Environment.GetEnvironmentVariable("KUBEMQ_AMQP_JWT"),
        "/", baseAddress.Scheme);
    var connection = await Connection.Factory.CreateAsync(connectAddress);
    ```
  </Tab>

  <Tab value="JavaScript">
    ```typescript
    // rhea / rhea-promise — setting both username + password makes rhea select PLAIN
    // (password = the KubeMQ JWT).
    const connection = await container.connect({
      host: "broker",
      port: 5672,
      container_id: `kubemq-amqp10-js-${process.pid}`,
      username: "audit-username",
      password: process.env.KUBEMQ_AMQP_JWT,
    });
    ```
  </Tab>

  <Tab value="Rust">
    ```rust
    // fe2o3-amqp — SASL PLAIN: username audit-only, password is the KubeMQ JWT.
    let mut connection = Connection::builder()
        .container_id("amqp10-client")
        .sasl_profile(SaslProfile::Plain {
            username: "audit-username".to_string(),
            password: std::env::var("KUBEMQ_AMQP_JWT")?,
        })
        .open("amqp://broker:5672")
        .await?;
    ```
  </Tab>
</Tabs>

### ANONYMOUS — the auth-off default [#anonymous--the-auth-off-default]

When the broker's authentication service is disabled, the connector offers `ANONYMOUS`. This is
the mechanism the runnable examples rely on: no credential is sent and the identity falls back to
the `container-id`. **ANONYMOUS is only offered when auth is off** — requesting it against an
auth-enabled broker is rejected as an auth failure (it was never advertised).

### EXTERNAL — mTLS, cert CN → ClientID [#external--mtls-cert-cn--clientid]

`EXTERNAL` is offered **only** when the TLS handshake presented a client certificate that the
listener *verified* (the mTLS listener). The identity is the client certificate's **Subject
CN**, sanitized to a valid `ClientID`; no JWT is needed. An empty CN is rejected. Because EXTERNAL
depends on mTLS, it is covered alongside TLS — see
[TLS and mTLS](/connectors/amqp/how-to/tls-and-mtls).

## Identity precedence [#identity-precedence]

The connector resolves the client identity in this order, depending on the negotiated mechanism
and whether auth is enabled:

1. **PLAIN + auth on** → the JWT's `ClientID` claim.
2. **PLAIN + auth off** → the sanitized SASL username (`authcid`); if empty, the `container-id`.
3. **EXTERNAL** → the client certificate's Subject CN (sanitized).
4. **ANONYMOUS** → the sanitized `container-id`.
5. **bare (no SASL)** → the sanitized `container-id`; if empty, a generated `amqp10-<uuid8>`.

Sanitization caps the value at **256** characters and maps every character outside
`[a-zA-Z0-9_-]` to `_`.

## `container-id` is required — and must be stable for durable subscribers [#container-id-is-required--and-must-be-stable-for-durable-subscribers]

Every AMQP 1.0 `OPEN` **must** carry a non-empty `container-id`. An empty value is rejected with
`CLOSE(amqp:invalid-field, "open.container-id is required")`. The value is sanitized
(`[a-zA-Z0-9_-]`, ≤ 256).

`container-id` matters for two reasons beyond identity:

* It becomes the `ClientID` whenever there is **no SASL identity** (ANONYMOUS / bare / auth-off
  PLAIN-with-empty-username).
* It is **half of the durable-subscription identity**. A durable events-store subscriber that
  reconnects with a *different* `container-id` will not resume its old position — it becomes a
  different durable identity.

<Callout type="warn">
  **Set a stable `container-id` for any durable subscriber.** Because the container-id is half the
  durable identity, a subscriber that lets its container-id drift across reconnects (e.g. a
  randomly generated one) will never resume — it creates a new subscription each time. See
  [Reliability](/connectors/amqp/how-to/reliability).
</Callout>

## Authorization — enforced at attach, per resource [#authorization--enforced-at-attach-per-resource]

Once authenticated, the connector enforces a Casbin policy **at `ATTACH` time**, against the
`ClientID`, the resolved pattern (mapped to a resource name), the channel, and the link role. If
no authorizer is wired (auth off), nothing is enforced.

| Link the client attaches                                   | Server role     | Permission enforced                                                                         |
| ---------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------- |
| **Receiver** from `<pattern>/<ch>` (client *consumes*)     | server-sender   | **Read** on `(ClientID, resource, channel)`                                                 |
| **Sender** to a fixed `<pattern>/<ch>` (client *produces*) | server-receiver | **Write** on `(ClientID, resource, channel)`                                                |
| **Sender** with a **null (anonymous) target**              | server-receiver | **deferred** — each message is authorized **per-message with Write** on its `properties.to` |
| **`/responses/<RequestID>`** (RPC reply token)             | server-receiver | **not enforced** — connection-scoped reply token                                            |

* The resource name maps the pattern: `events-store` → `events_store`; `queues` / `events` /
  `commands` / `queries` map to themselves.
* **Anonymous-terminus links** cannot be checked at attach because there is no fixed channel yet.
  Instead, the transfer layer authorizes **each message** with a **Write** check on the message's
  `to`, backed by a short-lived LRU cache. See
  [Addressing](/connectors/amqp/concepts/addressing).

A denied authorization closes the link with `DETACH(amqp:unauthorized-access)` and a generic
description. &#x2A;*No policy internals leak.** Your client should surface the
`amqp:unauthorized-access` condition and treat it as a permission error, not retry it as
transient.

## Audit events — exactly two [#audit-events--exactly-two]

The connector's audit surface for AMQP 1.0 lives entirely in the SASL layer and emits **only two
event types**:

| Audit event        | Emitted on                          | Fields                                                                                    |
| ------------------ | ----------------------------------- | ----------------------------------------------------------------------------------------- |
| **`auth.success`** | successful SASL authentication      | `ClientID`, `Transport: "amqp10"`, `SourceIP`, `Metadata{mechanism}`                      |
| **`auth.failure`** | failed/rejected SASL authentication | `ClientID`, `Transport: "amqp10"`, `SourceIP`, `Error` (sanitized), `Metadata{mechanism}` |

<Callout type="warn">
  The AMQP 1.0 connector does **not** emit `client.connected` or `client.disconnected` audit
  events. Do not build alerting, dashboards, or compliance reporting that depends on
  connection-lifecycle audit events from this connector — they are not produced. The only audit
  signal is authentication success/failure (with the SASL mechanism and source IP). For
  connection/link visibility, use the dashboard API and Prometheus metrics — see
  [Connections endpoint](/connectors/amqp/reference/connections-endpoint).
</Callout>

## Quick decision guide [#quick-decision-guide]

| You want…                                  | Do this                                                                                                  |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| Clone-and-run on a stock dev broker        | Connect ANONYMOUS (no credentials)                                                                       |
| Authenticate with KubeMQ identity          | SASL **PLAIN**, JWT in the **password** slot; username is audit-only                                     |
| Authenticate with a client certificate     | mTLS + SASL **EXTERNAL** (cert CN → ClientID) — see [TLS and mTLS](/connectors/amqp/how-to/tls-and-mtls) |
| Resume a durable events-store subscription | Set a &#x2A;*stable `container-id`** (it is half the durable identity)                                   |
| Diagnose a permission failure              | Look for `DETACH(amqp:unauthorized-access)`; check the Read/Write policy for that channel                |

## Related [#related]

<Cards>
  <Card title="TLS and mTLS" href="/connectors/amqp/how-to/tls-and-mtls" description="amqps:// on 5671, mutual TLS, and SASL EXTERNAL where the certificate CN becomes the ClientID." />

  <Card title="Auth & security" href="/connectors/reference/auth-and-security" description="Shared JWT model, public routes, CORS, origin validation, and TLS/mTLS across KubeMQ connectors." />

  <Card title="Addressing" href="/connectors/amqp/concepts/addressing" description="The address grammar and the per-message Write check on an anonymous sender's properties.to." />
</Cards>
