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
- Admin starts swamp serve with `--auth-mode oauth --allowed-collectives acme-corp`
- 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`)
- User runs `swamp auth login --server wss://swamp-serve.example.com`
- swamp serve starts a device authorization grant against swamp-club
- CLI shows: "Visit https://swamp-club.com/device and enter code: ABCD-1234"
- User opens browser, signs in to swamp-club (email/password, GitHub, Google — whatever they use)
- User enters the code, approves the device
- swamp serve polls swamp-club's token endpoint, gets an access token
- swamp serve calls swamp-club's userinfo endpoint — gets sub, email, name, and collectives (the user's organization memberships)
- swamp serve checks: is the user a member of `acme-corp`?
- 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:
- Enter the user code shown in their terminal
- Sign in to swamp-club if not already authenticated
- 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`
Shipped
Click a lifecycle step above to view its details.