148 lines
8.1 KiB
Markdown
148 lines
8.1 KiB
Markdown
# NATS Auth Callout — Integration Test
|
|
|
|
## Prerequisites
|
|
|
|
- Podman (running nats-server container)
|
|
- Rust toolchain
|
|
|
|
## Running the Test
|
|
|
|
```bash
|
|
# From the workspace root
|
|
cargo test -p integration-test-callout -- --nocapture --test-threads=1
|
|
```
|
|
|
|
The `--test-threads=1` flag is important because both tests use the same podman
|
|
container engine and ports must not collide.
|
|
|
|
If a previous test run left a stale container:
|
|
|
|
```bash
|
|
podman rm -f nats-callout-test-14222 nats-callout-test-14223
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌──────────────┐ ┌─────────────────┐ ┌───────────────────┐
|
|
│ IoT Device │ │ Callout Service │ │ Mock OIDC │
|
|
│ (async-nats) │ │ (auth handler) │ │ Server │
|
|
│ │ │ │ │ │
|
|
│ 1. Connect │ │ 4. Subscribe to │ │ JWKS + │
|
|
│ with │ │ $SYS.REQ. │ │ openid- │
|
|
│ Zitadel │ │ USER.AUTH │ │ configuration │
|
|
│ JWT token │ │ │ │ │
|
|
│ │ │ 6. Decode auth │ │ │
|
|
│ │ │ request JWT │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ 7. Validate │ │ │
|
|
│ │ │ Zitadel JWT │ │ │
|
|
│ │ │ (extract │ │ │
|
|
│ │ │ device_id) │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ 8. Build user │ │ │
|
|
│ │ │ JWT with │ │ │
|
|
│ │ │ scoped perms │ │ │
|
|
│ │ │ │ │ │
|
|
│ │ │ 9. Send auth │ │ │
|
|
│ │ │ response JWT │ │ │
|
|
└──────┬───────┘ └────────┬─────────┘ └───────────────────┘
|
|
│ │
|
|
│ │
|
|
▼ ▼
|
|
┌──────────────────────────────────────────────────────────────────┐
|
|
│ nats-server (podman) │
|
|
│ │
|
|
│ accounts { │
|
|
│ DEVICES: { jetstream: enabled, │
|
|
│ users: [{user: "auth", password: "auth"}, │
|
|
│ {user: "platform", password: "platform"}]│
|
|
│ } │
|
|
│ } │
|
|
│ │
|
|
│ authorization { │
|
|
│ auth_callout { │
|
|
│ issuer: <CALLOUT_NKEY_PUB> # signs user JWTs │
|
|
│ auth_users: [auth, platform] # bypass callout │
|
|
│ account: DEVICES # target account │
|
|
│ } │
|
|
│ } │
|
|
│ │
|
|
│ 2. Device connects → NATS sends auth request to callout │
|
|
│ 3. Callout responds with user JWT → NATS validates & admits │
|
|
│ 5. Device can only pub/sub on its scoped subjects │
|
|
└──────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Auth Flow (Step by Step)
|
|
|
|
```
|
|
Device NATS Server Callout Service
|
|
│ │ │
|
|
│ CONNECT │ │
|
|
│ (auth_token= │ │
|
|
│ Zitadel JWT) │ │
|
|
│────────────────>│ │
|
|
│ │ │
|
|
│ │ $SYS.REQ.USER.AUTH │
|
|
│ │ (auth request JWT)│
|
|
│ │───────────────────>│
|
|
│ │ │
|
|
│ │ 1. Decode auth request JWT
|
|
│ │ 2. Extract Zitadel JWT from connect_opts
|
|
│ │ 3. Validate JWT, extract device_id
|
|
│ │ 4. Build user JWT:
|
|
│ │ - subject = caller's nkey
|
|
│ │ - audience = "DEVICES"
|
|
│ │ - pub_allow: device-state.{id}, _INBOX.>
|
|
│ │ - sub_allow: device-commands.{id}, _INBOX.>
|
|
│ │ 5. Wrap in auth response JWT
|
|
│ │ - audience = server_id
|
|
│ │ - signed by issuer NKey
|
|
│ │ │
|
|
│ │ auth response JWT │
|
|
│ │<───────────────────│
|
|
│ │ │
|
|
│ +OK │ │
|
|
│<────────────────│ │
|
|
│ │ │
|
|
│ SUB device-commands.sensor-01 │
|
|
│────────────────>│ (permission check: allowed)
|
|
│ │ │
|
|
│ PUB device-state.sensor-01 │
|
|
│────────────────>│ (permission check: allowed)
|
|
│ │ │
|
|
│ SUB device-commands.sensor-99 │
|
|
│────────────────>│ (permission check: DENIED)
|
|
│ -ERR Permissions Violation │
|
|
│<────────────────│ │
|
|
```
|
|
|
|
## What the Tests Verify
|
|
|
|
### `device_authenticates_and_pubsub`
|
|
- Device connects with a Zitadel JWT
|
|
- Callout service validates the JWT and returns a per-device user JWT
|
|
- Device subscribes to `device-commands.sensor-test-01`
|
|
- Device publishes to `device-state.sensor-test-01`
|
|
- Platform client (username/password) receives the device state
|
|
- Platform sends a command, device receives it
|
|
|
|
### `device_cannot_access_other_device_subjects`
|
|
- Device A connects with JWT for `sensor-a`
|
|
- Device B connects with JWT for `sensor-b`
|
|
- Device A attempts to subscribe to `device-commands.sensor-b`
|
|
- NATS enforces permissions: device A gets `Permissions Violation`
|
|
|
|
## Key Design Decisions
|
|
|
|
- **Centralized auth callout** (not operator mode): no per-device NATS
|
|
accounts, no account JWTs, no `$SYS.REQ.CLAIMS.UPDATE`. All devices land
|
|
in a single `DEVICES` account with per-device permissions in user JWTs.
|
|
- **`auth_users` bypass**: `auth` and `platform` users skip the callout
|
|
and authenticate directly with their password. Only devices with a
|
|
Zitadel JWT go through the callout.
|
|
- **`issuer` NKey**: the callout config's `issuer` is a plain NKey public
|
|
key (generated with `KeyPair::new_account()`). The callout service signs
|
|
user JWTs with the corresponding seed. NATS verifies the response JWT
|
|
against this key. |