feat(oauth): accept token_ttl_seconds at Dynamic Client Registration, clamped to admin policy (#2179)

POST /register now accepts an optional token_ttl_seconds field (RFC 7591
extension). The server clamps it into an admin-configured window
(oauth.dcr_ttl_min_seconds / oauth.dcr_ttl_max_seconds config keys;
defaults 300s..7d), persists it as the client's per-client TTL override
(oauth_clients.token_ttl), and echoes the EFFECTIVE value back as
token_ttl_seconds in the registration response. Fail-safe posture:
absent/malformed -> server default; out-of-range -> clamped, never
rejected; pre-migration schemas without the token_ttl column keep
registering (no echo).

The MCP SDK's /register handler strips unknown body members before they
reach the clients store, so serve-http parses the raw body in a
middleware and carries the value through an AsyncLocalStorage context.

Root-cause follow-through: the per-client token_ttl lookup moved from
exchangeClientCredentials into issueTokens, so authorization_code (the
DCR default grant) and refresh issuance honor the override too —
previously only client_credentials did.

Tests: clamp boundaries (below/at/in/at/above, floor, inverted window),
store-level persistence + echo + no-context back-compat, cross-grant TTL
enforcement (test/oauth-dcr-ttl.test.ts), and a DB-gated wire-level e2e
in test/e2e/serve-http-oauth.test.ts. Docs: docs/mcp/DEPLOY.md.

Reported-by: @asabirov (#2179)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-27 16:35:21 -07:00
co-authored by Claude Fable 5
parent 032af6e5f7
commit d2a432dfd7
6 changed files with 434 additions and 18 deletions
+14
View File
@@ -156,6 +156,20 @@ await oauthProvider.registerClientManual(
For self-service client registration (Dynamic Client Registration, RFC 7591),
start the server with `--enable-dcr`. DCR is off by default.
DCR requests may include an optional `token_ttl_seconds` field (integer,
seconds) to request a per-client access-token lifetime. The server clamps the
request into an admin-configured window — never rejects over it — persists the
effective value as the client's TTL override, and echoes it back as
`token_ttl_seconds` in the registration response. Subsequent `/token` responses
for that client carry the matching `expires_in`. Clients that omit the field
keep the server default (`--token-ttl`). Configure the window (defaults: 300
seconds to 7 days):
```bash
gbrain config set oauth.dcr_ttl_min_seconds 600
gbrain config set oauth.dcr_ttl_max_seconds 86400
```
### 3. Expose the server
**v0.34 — bind explicitly.** `gbrain serve --http` defaults to `127.0.0.1`.