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



The KubeMQ STOMP connector authenticates a client **once, at CONNECT time**, using the STOMP
`passcode` header as a KubeMQ JWT. It then authorizes every SEND and SUBSCRIBE through the
server-wide Casbin authorizer. This guide covers the `login` / `passcode` part of the CONNECT frame
and what the connector does with it — for the full handshake (ports, heartbeats, TLS) see
[Connectivity and security](/connectors/stomp/how-to/connectivity-and-security).

<Callout type="info">
  On a stock dev server, **both authentication and authorization are off** (the server default) — a
  `nil` authenticator means allow-all. So the examples connect **unauthenticated**: leave `passcode`
  empty (or set any value) and use a freeform `login`.
</Callout>

## The no-auth default [#the-no-auth-default]

When the server-wide `Authentication` block is disabled, the connector is wired with a `nil`
authenticator and **skips the auth check entirely** — any (or empty) `passcode` succeeds, and the
derived session id comes from `login`. This is the server default, so every example connects without
a token.

```text
CONNECT
accept-version:1.2
heart-beat:10000,10000
login:my-app          # freeform; only used to derive the session id when there are no claims
passcode:             # empty in the default no-auth mode (any value also works)

^@
```

## `passcode` is a KubeMQ JWT — validated at CONNECT only [#passcode-is-a-kubemq-jwt--validated-at-connect-only]

When authentication **is** enabled, the connector validates the `passcode` header as a KubeMQ JWT
during the handshake, before any data frame is processed:

| Aspect         | Behavior                                                                                                             |
| -------------- | -------------------------------------------------------------------------------------------------------------------- |
| Credential     | The `passcode` header is a **KubeMQ JWT** (not a password).                                                          |
| When validated | **Connect-time only** — the connector never re-validates a live connection.                                          |
| Token expiry   | A JWT that expires **after** CONNECT does **not** drop the live connection; there is no per-operation re-validation. |
| Failure        | Audited as `auth.failure`, then `ERROR "authentication failed"` (no detail leak) + socket close.                     |
| Success        | Audited as `auth.success`; the JWT's `ClientID` claim becomes the session id.                                        |

<Callout type="warn">
  **Auth is connect-time only.** Because the JWT is checked only during the handshake, a token that
  expires mid-session keeps working until the client disconnects and reconnects. Do **not** rely on a
  STOMP connection drop as a token-expiry signal — it will not happen. To rotate credentials, the
  client must reconnect with a fresh `passcode`.
</Callout>

## `login` derives the session id [#login-derives-the-session-id]

The `login` header is **freeform** and is used purely to derive the client/session id **when JWT
claims are absent**. The derivation order is:

1. **`claims.ClientID`** — when authentication is on and the JWT carries a client id.
2. **`stomp-<sanitized-login>`** — the `login` value with characters outside `[a-zA-Z0-9_\-.]`
   stripped, truncated to 64 characters.
3. **`stomp-<uuid8>`** — a random fallback when there is no usable login.

The derived id is returned to the client as the &#x2A;*`session`** header on the `CONNECTED` frame, and
is what surfaces as `client_id` in the management API and audit log.

<Callout type="info">
  **STOMP has no client-id uniqueness rule.** Two connections may share the same `login` (and
  therefore the same derived id). The connector keeps all cross-connection state under a private,
  always-unique key, so same-login fleets are the normal, supported case — they do not collide.
</Callout>

## The `host` header is accepted and ignored [#the-host-header-is-accepted-and-ignored]

STOMP 1.1+ defines a `host` header for vhost selection. The KubeMQ connector **accepts any value (or
none) and ignores it** — there are **no vhost semantics**. Sending a `host` header will not scope,
isolate, or route your traffic.

## Authorization — per-channel Casbin [#authorization--per-channel-casbin]

When the server-wide authorizer is enabled, the connector runs a Casbin enforce check on every data
frame:

| Frame                                 | Check             | Resource                                                                  | Channel                     |
| ------------------------------------- | ----------------- | ------------------------------------------------------------------------- | --------------------------- |
| `SEND`                                | write             | the pattern name (`events` / `store` / `queues` / `commands` / `queries`) | the resolved KubeMQ channel |
| `SUBSCRIBE` (queues / events / store) | read              | the pattern name                                                          | the resolved KubeMQ channel |
| `SUBSCRIBE` to `/reply/...`           | **none — exempt** | —                                                                         | —                           |

The `Resource` is the **pattern name** (not the prefix), and the `Channel` is the **resolved KubeMQ
channel** after slash→dot mapping — `/queue/orders/new` enforces resource `queues`, channel
`orders.new`. A `nil` authorizer means allow-all. A denied frame produces `ERROR "access denied"` +
socket close.

<Callout type="info">
  **RPC SEND is authorized as `commands` / `queries`.** A SEND to `/command/exec` is a write enforce
  against resource `commands`, channel `exec`. There is no separate authorization step for the reply:
  it arrives on a `/reply/` subscription, which is connection-local and exempt.
</Callout>

### Why `/reply/` is exempt [#why-reply-is-exempt]

A `/reply/<name>` subscription is **connection-local** — it never touches a KubeMQ channel, has no
downstream channel to authorize, and exists only so the RPC bridge can deliver the matching reply on
the same connection. The connector therefore &#x2A;*never consults Casbin for `reply`**. This is what
lets an authorized requester receive its command or query reply without a second, separate grant.

## Error semantics [#error-semantics]

Both authentication and authorization failures are **terminal**: the connector emits an `ERROR`
frame and closes the socket. The message is **sanitized** — internal error text never crosses the
wire.

| Condition                    | Wire `message`          | Closes connection |
| ---------------------------- | ----------------------- | ----------------- |
| JWT validation fails         | `authentication failed` | Yes               |
| Casbin denies SEND/SUBSCRIBE | `access denied`         | Yes               |

Treat every `ERROR` frame as terminal in client code.

## Related [#related]

<Cards>
  <Card title="Connectivity and security" href="/connectors/stomp/how-to/connectivity-and-security" description="The full CONNECT handshake, ports 61613/61614, heartbeats, TLS, and the connection limit." />

  <Card title="Destination mapping" href="/connectors/stomp/how-to/destination-mapping" description="How a destination resolves to the pattern and channel that authorization checks against." />

  <Card title="Auth & security" href="/connectors/reference/auth-and-security" description="The shared KubeMQ JWT model and TLS/mTLS concepts across connectors." />
</Cards>
