Files
gbrain/test
d01a921e01 v0.26.1 fix(oauth): client_credentials tokens rejected by MCP bearer auth (#577)
* fix(oauth): client_credentials tokens rejected by MCP bearer auth

Three bugs found in production when connecting Claude Code via Tailscale:

1. Token validation fails with 'Token has no expiration time'
   - Root cause: postgres driver with prepare:false returns expires_at as
     string, but MCP SDK's bearerAuth middleware checks typeof === 'number'
   - Fix: Number(row.expires_at) in verifyAccessToken

2. OAuth metadata missing client_credentials grant type
   - Root cause: MCP SDK hardcodes ['authorization_code', 'refresh_token']
     in mcpAuthRouter's .well-known endpoint
   - Fix: middleware intercepts metadata response and appends
     'client_credentials' before it reaches the client
   - Claude Code's native OAuth auto-discovery now finds the CC flow

3. Express 5 compatibility fixes
   - trust proxy: 'loopback' for reverse proxy deployments (Caddy/Tailscale)
     without this, express-rate-limit throws ERR_ERL_UNEXPECTED_X_FORWARDED_FOR
   - /admin/* wildcard → /admin/{*path} (Express 5 named param syntax)

* test(oauth): add regression tests for v0.26.1 fixes

Unit test (oauth.test.ts):
- expiresAt is always a number, not string — SDK bearerAuth compat

Integration tests (serve-http-oauth.test.ts, 7 cases):
- client_credentials token accepted at /mcp (the actual regression)
- token expires_in matches server TTL
- OAuth metadata includes client_credentials grant type
- token endpoint discoverable from metadata
- admin dashboard serves SPA (Express 5 wildcard fix)
- X-Forwarded-For doesn't crash rate limiter (trust proxy fix)
- read-only token cannot call write operations (scope enforcement)

42 tests, 0 failures, 172 assertions.

* test(e2e): full E2E suite for serve-http OAuth 2.1 (15 cases)

Spins up a real gbrain serve --http against real Postgres, registers an
OAuth client, mints tokens via client_credentials, and exercises the full
MCP JSON-RPC pipeline end-to-end.

E2E cases (test/e2e/serve-http-oauth.test.ts):
- mint token via client_credentials grant
- minted token accepted at /mcp — tools/list returns tools
- minted token works for tools/call — search executes
- expired/invalid token rejected at /mcp
- missing Authorization header returns 401
- OAuth metadata includes all three grant types
- OAuth metadata issuer matches public URL
- admin dashboard serves SPA (Express 5 wildcard fix)
- admin sub-routes serve SPA fallback
- X-Forwarded-For doesn't crash rate limiter
- read-only token rejected for write operations
- write-scoped token can call read operations
- health endpoint works without auth
- multiple tokens work independently
- wrong client_secret rejected at token endpoint

Unit test addition (test/oauth.test.ts):
- expiresAt is always typeof number (SDK bearerAuth compat)

Total: 50 tests, 0 failures, 201 assertions.

---------

Co-authored-by: Wintermute <wintermute@garrytan.com>
2026-05-03 06:27:33 -07:00
..