Skip to main content
← Back to list
01Issue
FeatureShippedSwamp Club
Assigneesstack72

Relationships

↑ child of #662

#1015 swamp-club: enable OAuth provider for swamp serve collective-based admission

Opened by stack72 · 7/7/2026· Shipped 7/7/2026

Parent

Part of swamp #662 (serve authentication & authorization), Phase 2 Track A.

Goal

Make swamp-club an OAuth 2.0 authorization server so that swamp serve can authenticate users and verify their collective membership. After this ships, a swamp serve instance configured with `--auth-mode oauth --allowed-collectives acme-corp` will only admit users who are members of the `acme-corp` collective in swamp-club.

No SSO. No IdP groups. No `idp-group:` grants. Just: "are you in the allowed collective?"

The flow

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

What to build in swamp-club

1. Enable `oidcProvider` plugin

Already in better-auth 1.4.18, no new dependency. Add to `lib/auth.ts`:

```typescript import { oidcProvider } from "better-auth/plugins/oidc-provider";

oidcProvider({ loginPage: "/login", getAdditionalUserInfoClaim: async (user, scopes, client) => { // Return the user's collective memberships // The Organization plugin already manages this data const orgs = await getOrganizationsForUser(user.id); return { collectives: orgs.map(o => o.slug), }; }, }), ```

This exposes:

  • `GET /api/auth/.well-known/openid-configuration` — OIDC discovery
  • `GET /api/auth/oauth2/authorize` — authorization endpoint
  • `POST /api/auth/oauth2/token` — token endpoint
  • `GET /api/auth/oauth2/userinfo` — userinfo endpoint
  • `POST /api/auth/oauth2/register` — client registration

2. Enable `deviceAuthorization` plugin

Also already in better-auth 1.4.18:

```typescript import { deviceAuthorization } from "better-auth/plugins/device-authorization";

deviceAuthorization({ verificationUri: "/device", expiresIn: "30m", interval: "5s", }), ```

This exposes:

  • `POST /api/auth/device/code` — request device code + user code
  • `POST /api/auth/device/token` — poll for access token
  • `GET /api/auth/device?user_code=XXXX` — verify a user code
  • `POST /api/auth/device/approve` — user approves
  • `POST /api/auth/device/deny` — user denies

3. Wire collectives into userinfo

The `getAdditionalUserInfoClaim` function needs to read the user's organization memberships from the Organization plugin and return them as `collectives`. The Organization plugin is already enabled — the data is there.

The userinfo response should look like: ```json { "sub": "user-uuid-123", "email": "sarah@acme.com", "name": "Sarah Chen", "collectives": ["acme-corp", "dev-team"] } ```

4. Device verification page

Create or adapt a page at `/device` where users:

  1. Enter the user code shown in their terminal
  2. Sign in to swamp-club if not already authenticated
  3. Approve or deny the device

The existing `routes/login.tsx` has custom device code interstitial logic. Either adapt it or create a new `routes/device.tsx`.

The page flow:

  • User visits `/device`
  • If not authenticated → sign in first (email/password, social — whatever)
  • Enter the code → page calls `GET /api/auth/device?user_code=XXXX` to verify
  • Click "Approve" → page calls `POST /api/auth/device/approve`
  • Done — CLI polling picks up the approval

5. Client auto-registration

swamp serve needs to register itself as an OAuth client on first start. The `registerOAuthApplication` endpoint handles this. The registration needs to be callable with an API key (the admin's `swamp auth login` credential).

For first-party apps (swamp-serve), skip the consent screen — the user shouldn't see "Do you authorize swamp-serve?" Use `skipConsent: true` on the trusted client or auto-approve consent for clients registered by the same organization.

6. Validate the device authorization + OIDC token flow works together

The device authorization plugin returns its own access token format. The OIDC provider has its own token format. These need to work together — the access token from the device grant must be usable at the userinfo endpoint. Verify this integration works, since both plugins are independent.

Testing

```bash

1. Start swamp-club locally

2. Verify OIDC discovery

curl http://localhost:8000/api/auth/.well-known/openid-configuration

3. Register a test client (or use trustedClients config)

curl -X POST http://localhost:8000/api/auth/oauth2/register \ -H "Content-Type: application/json" \ -d '{"client_name":"swamp-serve-test","redirect_uris":[],"grant_types":["urn:ietf:params:oauth:grant-type:device_code"]}'

4. Request a device code

curl -X POST http://localhost:8000/api/auth/device/code \ -H "Content-Type: application/json" \ -d '{"client_id":""}'

5. Open browser, visit /device, enter the code, sign in, approve

6. Poll for token

curl -X POST http://localhost:8000/api/auth/device/token \ -H "Content-Type: application/json" \ -d '{"grant_type":"urn:ietf:params:oauth:grant-type:device_code","device_code":"","client_id":""}'

7. Call userinfo with the access token

curl -H "Authorization: Bearer " \ http://localhost:8000/api/auth/oauth2/userinfo

8. Verify response includes collectives

Expected: {"sub":"...","email":"...","name":"...","collectives":["acme-corp"]}

```

Scope

  • Enable two plugins in `lib/auth.ts` (no new dependencies)
  • Wire organization memberships into userinfo as `collectives`
  • Device verification page
  • Client registration works with API key auth
  • First-party consent skip

Out of scope

  • SSO / IdP federation — later phase
  • IdP groups / `idp-group:` grants — later phase
  • swamp CLI / serve changes — separate issue in swamp repo
  • Auto-registration logic on the swamp serve side — separate issue

Key files

  • Auth config: `lib/auth.ts`
  • Login page: `routes/login.tsx`
  • Auth routes: `routes/api/auth/[...path].ts`
  • Organization domain: `lib/domain/collective/collective.ts`
  • Organization repo: `lib/infrastructure/better-auth-collective-repository.ts`
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 7 MOREREVIEW+ 3 MOREPR_MERGED+ 1 MORENOTIFICATION_SKIPPED

Shipped

7/7/2026, 9:33:03 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack727/7/2026, 2:21:32 PM
stack72 linked parent of #6627/7/2026, 2:51:57 PM

Sign in to post a ripple.