# cnats — chat over the bus A realtime web chat built with [Leptos](https://leptos.dev) (SSR + hydration on Axum) where every room is a **NATS subject** and sign-in is **Kanidm SSO** (OIDC authorization-code + PKCE). ``` browser ──(server fn POST)──▶ axum ──publish──▶ NATS chat.room. browser ◀──(SSE /sse/)── axum ◀─subscribe── NATS chat.room. browser ◀──(302 /auth/*)────── axum ◀──OIDC──▶ Kanidm ``` The browser never talks to NATS directly: the server publishes on behalf of the signed-in session (sender identity comes from the session, never the client) and fans messages out to browsers over Server-Sent Events. Any other NATS client on the bus can publish/subscribe to `chat.room.*` and participate. ## Prerequisites - Rust (stable) with the `wasm32-unknown-unknown` target - [`cargo-leptos`](https://github.com/leptos-rs/cargo-leptos): `cargo install cargo-leptos --locked` - A NATS server: `docker compose up -d nats` (or `nats-server` locally) - A running Kanidm instance you administer ## Kanidm setup Register the app as an OAuth2 client (confidential, with PKCE — Kanidm's default). Replace the URLs with yours: ```bash kanidm login --name idm_admin # create the client kanidm system oauth2 create cnats "cnats chat" http://localhost:3000 # register the redirect URL used by this app kanidm system oauth2 add-redirect-url cnats http://localhost:3000/auth/callback # map which Kanidm groups may sign in, and grant the scopes the app requests kanidm group create cnats_users kanidm group add-members cnats_users your_username kanidm system oauth2 update-scope-map cnats cnats_users openid profile email # if you serve the app over plain http in dev, allow insecure redirect urls kanidm system oauth2 enable-localhost-redirects cnats # read back the client secret for .env kanidm system oauth2 show-basic-secret cnats ``` > Kanidm's OIDC discovery endpoint is per-client: > `https:///oauth2/openid//.well-known/openid-configuration`. > The app derives this from `KANIDM_URL` + `OAUTH2_CLIENT_ID` automatically. ## Configuration ```bash cp .env.example .env # then edit ``` | Variable | Meaning | | --- | --- | | `NATS_URL` | NATS server, default `nats://127.0.0.1:4222` | | `KANIDM_URL` | Base URL of Kanidm, e.g. `https://idm.example.com` | | `OAUTH2_CLIENT_ID` | The Kanidm oauth2 client name (`cnats` above) | | `OAUTH2_CLIENT_SECRET` | Output of `show-basic-secret` | | `PUBLIC_URL` | Where browsers reach this app; `${PUBLIC_URL}/auth/callback` must be a registered redirect URL | | `COOKIE_SECURE` | `true` behind HTTPS (production) | ## Run ```bash docker compose up -d nats cargo leptos watch # dev, auto-reload on http://127.0.0.1:3000 cargo leptos build --release # production build (binary + target/site) ``` Open two browser windows, sign in, and chat — or join from the CLI: ```bash nats sub 'chat.room.*' nats pub chat.room.lobby '{"id":"cli-1","room":"lobby","username":"cli","display_name":"CLI","text":"hello from the bus","time":"12:00:00"}' ``` ## Layout ``` src/ main.rs axum entrypoint: routes, sessions, NATS + OIDC bootstrap lib.rs hydrate entrypoint (wasm) app.rs Leptos UI (login gate + chat console) auth.rs shared User type + current_user server fn chat.rs shared ChatMessage/rooms + send_message server fn (publishes to NATS) server/ oidc.rs Kanidm OIDC login/callback/logout handlers sse.rs NATS → browser SSE bridge (one subscription per client) style/main.css the console theme ``` ## Notes & production hardening - Sessions are in-memory (`tower-sessions` `MemoryStore`): restart logs everyone out, and it is single-instance. Swap in a Redis/SQL store for multiple replicas. - Messages are not persisted; you only see traffic while connected. NATS JetStream (the compose file starts NATS with `-js`) would give history via a durable stream over `chat.room.*`. - The OIDC client verifies ID-token signature, nonce, and CSRF state, and requires PKCE — but there is no token refresh; the app session lives independently of the Kanidm token lifetime.