Skip to main content
← Back to list
01Issue
FeatureIn ProgressSwamp CLI
Assigneesstack72

Relationships

↑ child of #662

#1016 serve-auth: mode: oauth — device grant client + collective-based admission

Opened by stack72 · 7/7/2026

Parent

Sub-issue of #662 (serve authentication & authorization). Phase 2 Track A. Depends on swamp-club #1015 (OAuth provider + device authorization plugins).

Goal

Implement `--auth-mode oauth` on swamp serve. Users authenticate via swamp-club using the OAuth device grant flow. Admission is controlled by collective membership — only users in the allowed collectives can connect.

This replaces manual token minting (`mode: token`) with a self-service login experience: users authenticate themselves through swamp-club, and their collective membership determines access.

The flow

  1. Admin starts: `swamp serve --auth-mode oauth --admins user:paul --allowed-collectives acme-corp`
  2. First start: server auto-registers as an OAuth client with swamp-club using the admin's API key
  3. User runs: `swamp auth login --server wss://swamp-serve.example.com`
  4. CLI calls swamp serve's `/auth/device` endpoint
  5. swamp serve starts a device authorization grant against swamp-club
  6. CLI shows: `Visit https://swamp-club.com/device and enter code: ABCD-1234`
  7. User opens browser, signs in to swamp-club (email/password, social — whatever), enters code, approves
  8. swamp serve polls swamp-club's token endpoint, gets access token
  9. swamp serve calls swamp-club's userinfo endpoint — gets sub, email, name, collectives
  10. swamp serve checks: is the user a member of `acme-corp`? (from `--allowed-collectives`)
  11. Yes → mints server token, returns to CLI. No → rejected.
  12. CLI stores credential in `~/.config/swamp/servers.json` — future commands auto-authenticate

What to build

1. OAuth device grant client in swamp serve

A new module (e.g., `src/serve/oauth_client.ts`) that implements the client side of RFC 8628:

Start the device grant: ```typescript async function startDeviceGrant(providerUrl: string, clientId: string): Promise<{ deviceCode: string; userCode: string; verificationUri: string; expiresIn: number; interval: number; }> ``` Calls `POST /api/auth/device/code` with the client ID.

Poll for authorization: ```typescript async function pollForToken(providerUrl: string, clientId: string, deviceCode: string, interval: number): Promise<{ accessToken: string; }> ``` Polls `POST /api/auth/device/token` every `interval` seconds. Handles `authorization_pending` (keep polling), `slow_down` (increase interval), `expired_token` (give up), `access_denied` (user denied).

Get userinfo: ```typescript async function getUserInfo(providerUrl: string, accessToken: string): Promise<{ sub: string; email: string; name: string; collectives: string[]; }> ``` Calls `GET /api/auth/oauth2/userinfo` with Bearer token.

2. Auto-registration on first start

When `--auth-mode oauth` and no client credentials are stored locally:

  1. Read the admin's swamp-club API key from `~/.config/swamp/auth.json` (from `swamp auth login`)
  2. Call swamp-club's `POST /api/auth/oauth2/register` to register this serve instance as an OAuth client
  3. Store the returned `client_id` and `client_secret` locally (e.g., in `.swamp/oauth-client.json` or the vault)
  4. Subsequent starts read the stored credentials

If the admin hasn't run `swamp auth login`, refuse to start with: "oauth mode requires a swamp-club account — run 'swamp auth login' first"

3. `/auth/device` endpoint on swamp serve

A new HTTP endpoint on the serve handler that the CLI calls to start the device grant flow:

``` POST /auth/device → swamp serve calls swamp-club's device/code endpoint → returns { userCode, verificationUri, verificationUriComplete } to the CLI ```

The CLI then displays the code and URL. swamp serve handles the polling internally — the CLI just waits for the result.

Alternatively, swamp serve could expose a WebSocket frame type (`auth.login`) that handles the entire flow. Either approach works.

4. Admission policy

After getting the userinfo response, swamp serve applies the admission policy:

```typescript function checkAdmission( collectives: string[], allowedCollectives: string[], allowedUsers: string[], userSub: string, ): boolean { if (allowedUsers.includes(userSub)) return true; return collectives.some(c => allowedCollectives.includes(c)); } ```

If rejected, return a clear error: "user not admitted — not a member of allowed collectives (acme-corp)"

5. Server token minting with collectives

On admission, mint a server token (same as `mode: token`) but include the user's collectives on the token record:

```typescript { principalId: "user:sub-uuid", principalEmail: "sarah@acme.com", displayName: "Sarah Chen", collectives: ["acme-corp", "dev-team"], // from userinfo // ... existing fields } ```

The collectives are snapshotted at login time. They become stale if the user's collective membership changes in swamp-club — refreshed at next login or via server-side refresh (later phase).

6. `swamp auth login --server` command

Update the CLI login command to support the OAuth flow when connecting to a serve instance:

```bash swamp auth login --server wss://swamp-serve.example.com ```

Flow:

  1. CLI calls swamp serve's `/auth/device` (or sends an `auth.login` WebSocket frame)
  2. Receives user code + verification URL
  3. Opens browser to the verification URL (or tells user to visit it)
  4. Displays: "Visit https://swamp-club.com/device and enter code: ABCD-1234"
  5. Waits for swamp serve to report success
  6. Receives the server token
  7. Stores via `ServerCredentialRepository`
  8. Displays: "✓ Logged in as Sarah Chen (acme-corp, dev-team) on wss://swamp-serve.example.com"

7. Wire collectives into enforcement

Update `authorizeOrReject` in `src/serve/connection.ts` to populate collectives from the server token record instead of passing empty arrays:

```typescript // Currently: { principal, collectives: [] }

// After: { principal, collectives: tokenRecord.collectives ?? [] } ```

This means `idp-group:` grants will match for OAuth-authenticated users whose collectives include the grant's subject. For `mode: token` users, collectives remain empty (no change).

Testing (local, both services running)

```bash

Terminal 1: swamp-club on localhost:8000

cd ~/code/swamp-club/swamp-club && deno task dev

Terminal 2: swamp serve on localhost:9090

cd /tmp/test-oauth && swamp repo init && swamp model create command/shell hello swamp serve --auth-mode oauth --admins user:paul --allowed-collectives test-collective

Terminal 3: test the flow

swamp auth login --server ws://127.0.0.1:9090

→ "Visit http://localhost:8000/device and enter code: ABCD-1234"

→ Sign in to swamp-club (email/password), enter code, approve

→ "✓ Logged in as Test User (test-collective)"

Verify

swamp access can-i --server ws://127.0.0.1:9090 swamp model method run hello execute --input 'run=echo "oauth works"' --server ws://127.0.0.1:9090

Negative test: user NOT in test-collective

→ Should be rejected at admission

```

Scope

  • OAuth device grant client module
  • Auto-registration with swamp-club on first start
  • `/auth/device` endpoint (or WebSocket frame) on swamp serve
  • Admission policy (collective check)
  • Server token minting with collectives
  • `swamp auth login --server` CLI command
  • Wire collectives into `authorizeOrReject`
  • Tests

Out of scope

  • SSO / IdP federation — later phase
  • IdP groups on userinfo — later phase (requires SSO)
  • Server-side userinfo refresh — later phase
  • `mode: token` changes — existing flow unchanged

Dependencies

  • swamp-club #1015 must ship first (OIDC provider + device authorization plugins)

References

  • Auth config: `src/domain/access/serve_auth_config.ts` (`--auth-mode oauth` already defined)
  • Server token model: `src/domain/models/access/server_token_model.ts`
  • Client credential storage: `src/infrastructure/persistence/server_credential_repository.ts`
  • Token auth: `src/serve/token_auth.ts` (pattern to follow for OAuth token minting)
  • Remote run: `src/cli/remote_run.ts` (client-side WebSocket patterns)
  • Existing auth login: `src/libswamp/auth/login.ts` (existing CLI login flow for swamp-club)
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 3 MOREPR_LINKED

In Progress

7/7/2026, 2:53:36 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack727/7/2026, 2:29:52 PM
stack72 linked parent of #6627/7/2026, 2:51:47 PM

Sign in to post a ripple.