> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# OIDC and Bearer Authentication

> Let MQTT clients connect with an OIDC access token as the password, and authorize them with the same LoT rules as local users

MQTT clients can sign in with an access token from an OIDC identity provider instead of a password stored on the broker. The broker checks the token, then applies the same LoT (Language of Things) rules it uses for a local user.

> [!TIP]
>
> **Like a badge that expires.** The client shows a badge (the access token) at the door. The broker checks the badge with the issuer, lets the person in as the identity printed on it, and asks them to leave when the badge expires.

## When to use token authentication

Use it when people and services already have identities outside the broker:

* **Coreflux accounts** — Operators sign in to [Coreflux HUB](/hub/account-and-fleet) with their organization account and connect to a broker in the fleet. The HUB presents that account's access token. The broker accepts it only after [enrollment](/latest/mqtt-broker/licensing) has bound the broker to the same organization.
* **A provider you run** — Point `Issuers` at an identity provider that publishes a discovery document over HTTPS and signs access tokens with RS256.

Leave it off when every client uses a username and password in the broker's user store. It is **off by default**.

Local users keep working. A CONNECT whose password is not a token is still checked against the user store.

***

Token checks run at MQTT CONNECT. Enabling them is a broker configuration change, and the broker reads that configuration at startup.

## Quick Start

**Coreflux organization**

A broker enrolled in a Coreflux organization accepts account tokens for that organization. Enrollment fills `BoundOrg`; until that succeeds, token logins are refused and local users still connect.

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "AnonymousLogin": false,
  "Oidc": {
    "Enabled": true,
    "Issuers": ["https://accounts.example.com"],
    "ClientIdAllowlist": ["hub-desktop"],
    "RequiredScope": "coreflux-broker/connect"
  }
}
```

Replace `Issuers` with the issuer URL of the account tokens you accept. List every `client_id` exactly — `hub-desktop` is the id the broker's account-token contract uses for the desktop HUB. There is no wildcard. With enrollment turned on and `BoundOrg` still empty, the broker starts and refuses token logins until enrollment writes the organization id. With enrollment off, set `BoundOrg` yourself or the broker will not start. Restart after editing the file.

**Your own identity provider**

When the issuer serves only this broker's users, declare it in `DedicatedIssuers`. `BoundOrg` can stay empty, and group names on the token are used as written.

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "AnonymousLogin": false,
  "Oidc": {
    "Enabled": true,
    "Issuers": ["https://idp.example.com"],
    "DedicatedIssuers": ["https://idp.example.com"],
    "ClientIdAllowlist": ["plant-mqtt"],
    "RequiredScope": "",
    "RequiredClaimsVersion": ""
  }
}
```

The token must still be an **access** token (`token_use` = `access`), signed with RS256, and its `client_id` claim must be in the allowlist. Empty `RequiredScope` and `RequiredClaimsVersion` skip those checks. Group names are read from `coreflux:groups` as a space-separated string. On a dedicated issuer, a name with no organization prefix is kept as-is. Restart the broker after editing the file.

> [!NOTE]
>
> There is no `COREFLUX_OIDC_*` environment variable on the broker. These fields live in the broker configuration file (the same JSON as [ports and TLS](/latest/mqtt-broker/configuration)). `BoundOrg` is the exception: a successful [enrollment](/latest/mqtt-broker/licensing#quick-start) writes the organization id into it.

## What the client sends

On CONNECT, put the access token in the **password**. An optional `Bearer ` prefix is accepted. The broker does not read the CONNECT username as the identity — the identity is the token's subject claim (`sub` unless you change `UsernameClaim`).

| CONNECT field | What to send |
| - | - |
| Password | The access token, or `Bearer ` followed by the token |
| Username | Any value your client requires. It is not the broker username |

A password that is not shaped like a token (three non-empty segments separated by `.`) stays on the local user-store path.

The Coreflux HUB does this when you **Connect** on a fleet row. See [Account, fleet, and Cloud mode](/hub/account-and-fleet).

Use MQTT over TLS (`mqtts://` or `wss://`) whenever the token leaves the broker host. OIDC does not encrypt the MQTT connection. A plaintext listener carries the access token in the CONNECT password. Mutual TLS is a separate check — it does not replace the token, and the token does not replace it. See [TLS Server Certificate](/latest/mqtt-broker/configuration#tls-server-certificate).

## How the broker decides

When `Oidc.Enabled` is true and the password looks like a token:

1. The broker loads the issuer's discovery document over HTTPS and checks the RS256 signature, the issuer, and the expiry.
2. It requires `token_use` = `access`. An ID token is refused.
3. It requires the `client_id` claim to be in `ClientIdAllowlist`.
4. If `RequiredScope` is set, that exact scope must appear in the token's space-separated `scope` list. Coreflux account tokens from the HUB request `coreflux-broker/connect`.
5. If `RequiredClaimsVersion` is set (default `1`), the `coreflux:claims_version` claim must match it.
6. If `BoundOrg` is set, that organization id must appear in the space-separated `coreflux:orgs` claim. An empty `BoundOrg` refuses the token unless the issuer is listed in `DedicatedIssuers`.
7. Groups and roles are read from `coreflux:groups` and `coreflux:role`. On a bound broker they must be prefixed with the organization id (`org_…:engineers`). The broker strips that prefix and authorizes the plain name.

A discovery or key outage **refuses** the connection. It does not fall through to anonymous access.

### Identity and rules

The session is not written to the local user store. HUB User Management and `-listUsers` still show local accounts only.

| What a rule sees | Source |
| - | - |
| `USER IS "…"` | `UsernameClaim`, default `sub` |
| `USER IN GROUP "engineers"` | De-qualified name from `coreflux:groups` for `BoundOrg` |
| `USER HAS AllowedSystemConfiguration` | True only when `AdminRoles` or `AdminGroups` matches |

The display name (`preferred_username` by default) is written to login logs. Rules never match it. A rule written against an email address does not match a subject id.

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE RULE EngineersPublish WITH PRIORITY 200 FOR Publish TO TOPIC "plant/status/#"
    IF USER IN GROUP "engineers" THEN
        ALLOW
    ELSE
        DENY
```

Custom rules like this one require a Growth or Enterprise license. On Community, set `AdminRoles` or `AdminGroups` instead: a matching token receives `AllowedSystemConfiguration`, `AllowedUserManagement`, and `AllowedLogManagement`, which the shipped rules already allow. Both lists are empty by default, so a Coreflux account is **not** an administrator until you name the role or group. Use the plain name (`owner`), not `org_…:owner`.

Group and role changes apply on the next token. The live session keeps the groups it connected with until it is disconnected.

See [RBAC with LoT Rules](/latest/lot-language/rules/rbac#federated-accounts).

### Token lifetime

`DisconnectOnExpiry` is on by default. When the access token expires, the broker waits `ExpiryGraceSeconds` (default 30) and then disconnects the client. On MQTT 5 the disconnect reason is `NotAuthorized`. The client connects again with a fresh token. That window is how a revoked account loses MQTT access: there is no separate revocation list.

Signature checks allow `ClockSkewSeconds` of clock difference (default 60).

## Configuration reference

All fields sit under `Oidc` in the broker configuration file. Restart the broker after a change. Turning `Enabled` on forces `AnonymousLogin` off, even if the file still says `true`.

| Field | Default | Required when enabled | Purpose |
| - | - | - | - |
| `Enabled` | `false` | — | Turn token authentication on |
| `Issuers` | empty | Exactly one HTTPS issuer URL | Discovery and signature keys. More than one issuer refuses to start |
| `DedicatedIssuers` | empty | No | Issuers that serve a single organization. Only these may run with an empty `BoundOrg` |
| `ClientIdAllowlist` | empty | Yes, at least one id | Exact `client_id` values. Empty refuses to start |
| `RequiredScope` | empty | No | Exact scope string. Empty skips the check |
| `RequiredClaimsVersion` | `1` | No | Expected `coreflux:claims_version`. Empty skips the check |
| `BoundOrg` | empty | Yes for a shared issuer | Organization id that must appear in `coreflux:orgs`. Enrollment sets this |
| `OrgsClaim` | `coreflux:orgs` | No | Claim holding organization ids |
| `GroupsClaim` | `coreflux:groups` | No | Claim holding `orgId:group` names |
| `RoleClaim` | `coreflux:role` | No | Claim holding `orgId:role` names. Used only for `AdminRoles` |
| `ClaimsVersionClaim` | `coreflux:claims_version` | No | Claim holding the contract version |
| `UsernameClaim` | `sub` | No | Session identity for `USER IS` |
| `DisplayNameClaim` | `preferred_username` | No | Label in logs. Empty disables it. Never used for authorization |
| `AdminRoles` | empty | No | Plain role names that receive the three admin permission flags |
| `AdminGroups` | empty | No | Plain group names that receive the same flags. Compared case-insensitively |
| `GroupsSeparator` | space | No | Separator for the list claims |
| `ClockSkewSeconds` | `60` | No | Allowed clock difference for expiry |
| `DisconnectOnExpiry` | `true` | No | Disconnect when the access token expires |
| `ExpiryGraceSeconds` | `30` | No | Extra seconds after expiry before disconnect |

> [!WARNING]
>
> An enabled configuration with no issuer, more than one issuer, an empty `ClientIdAllowlist`, or a shared issuer and an empty `BoundOrg` **refuses to start**. The exception is enrollment: if enrollment is on and `BoundOrg` is not known yet, the broker starts, serves local users, and refuses token logins until enrollment writes the organization id. See [Licensing and Enrollment](/latest/mqtt-broker/licensing).

## Failure modes

| What you see | Cause |
| - | - |
| Broker will not start, log says the allowlist is empty or `BoundOrg` is not set | Fix `Oidc` or finish enrollment. A shared issuer with no organization binding is rejected on purpose |
| MQTT 5 `BadUserNameOrPassword` | The password looked like a token and validation failed: bad signature, expired, wrong issuer, ID token, `client_id` not allowed, scope missing, claims version mismatch, organization mismatch, or a group entry naming an organization the token does not list |
| MQTT 5 `ServerUnavailable` | The broker could not fetch the issuer's discovery document or signing keys. The connection is denied |
| MQTT 5 `NotAuthorized` after a successful token check | A `FOR Connect` rule denied the session, or the broker required a clean session the client did not request |
| Disconnect with `NotAuthorized` after the token lifetime | `DisconnectOnExpiry` dropped the client. Reconnect with a new access token |
| `USER IS` never matches the person's email | The identity is `sub`. Match a group, or set `UsernameClaim` only if that claim is stable and unique |
| A local password is rejected as a bad token | The password contained two dots and three non-empty segments, so it took the token path |

Login attempts are published on [`$SYS/Coreflux/Comms/Logins`](/latest/mqtt-broker/sys-topics#syscorefluxcommslogins). For a token login, `Username` is the subject and `DisplayName` is the display-name claim. The password is not included.

## How this fits with the portal

[portal.coreflux.org](https://portal.coreflux.org) is where an organization gets a license and registers brokers. Enrollment tells **this** broker which organization it belongs to (`BoundOrg`). The HUB fleet view lists those brokers for people signed in with an account in that organization. **Connect** on a fleet row sends that account's access token to the broker. LoT rules then see the groups on the token.

A license plan and an organization binding are different steps. The plan is [activation](/latest/mqtt-broker/licensing). The binding is enrollment. Token login needs the binding when the issuer is shared.

***

## Best practices

> **Prefer groups over subject ids**
>
> Subject ids are opaque. Write `USER IN GROUP` rules for the names your identity provider puts on the token, and keep `UsernameClaim` on `sub`.

> **Put tokens on TLS listeners**
>
> The access token is the CONNECT password. Expose `8883` and `443` on the broker host, and keep `1883` off untrusted networks.

> **Keep the client allowlist short**
>
> Every id in `ClientIdAllowlist` can present a token this broker will try to accept. Add a service client only for the job that needs MQTT.

> **Do not confuse this with MCP tokens**
>
> [MCP Server](/latest/mqtt-broker/mcp-server) bearer tokens are minted by the broker for HTTP `/mcp`. They are not OIDC access tokens, and an OIDC token is not an MCP token.

> [!WARNING]
>
> The broker accepts RS256 access tokens only, and it requires the `token_use` claim to equal `access`. There is no setting to allow another algorithm or to skip that claim. An identity provider that omits `token_use` or `client_id` cannot authenticate MQTT clients until its tokens include them.

## Next Steps

> **[Account, fleet, and Cloud mode](/hub/account-and-fleet)**
>
> Sign in to the HUB with a Coreflux account and connect to brokers in the organization.

> **[RBAC with LoT Rules](/latest/lot-language/rules/rbac#federated-accounts)**
>
> Authorize token sessions with groups and permission flags.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.