Captures the per-request token design landed in byteflavour's PR #138, plus the three open review blockers (silent fallback, bare-token branch, weak test) as unchecked tasks tied to the spec.
4.8 KiB
4.8 KiB
1. Context-aware client factory
- 1.1 Add
forgejo.WithToken(ctx, token) context.Contextand an unexportedtokenKeytype topkg/forgejo/forgejo.go. Read back viatokenFromContext(ctx) string. - 1.2 Refactor
Client()toClient(ctx context.Context) *forgejo.Client. WhentokenFromContext(ctx)returns a non-empty string, build a fresh client viaforgejo.NewClient(flag.URL, forgejo.SetToken(token)). Otherwise return the singleton initialized viasync.Oncefromflag.Token(existing behavior). - 1.3 Blocker fix — when ctx-token is supplied and
forgejo.NewClient(...)returns an error, MUST NOT fall through to the singleton. Either changeClient(ctx)signature to(*forgejo.Client, error)and propagate, or returnniland log loudly. Silent demotion to the global token is a privilege-escalation vector. See design D3.
2. Transport-layer extraction
- 2.1 In
operation/operation.go, registerWithHTTPContextFunconserver.NewStreamableHTTPServerandWithSSEContextFunconserver.NewSSEServer. Each func readsr.Header.Get("Authorization")and callsforgejo.WithToken(ctx, token)when a recognized scheme is present. - 2.2 Accept
token <X>prefix (Forgejo native scheme). - 2.3 Accept
Bearer <X>prefix (OAuth2 transport). - 2.4 Blocker fix — match scheme case-insensitively (RFC 7235).
bearer xyzMUST resolve to the same token asBearer xyz. Today the case-sensitivestrings.HasPrefix("Bearer ")rejects lowercase, which then falls through to the bare-token branch (1.5), which silently downgrades to the global identity. - 2.5 Blocker fix — drop the no-scheme bare-token branch (
operation/operation.go:152-154and:181-183). Not in the spec. Accepting a bare token without a scheme expands the contract silently. If kept, document in proposal and README. - 2.6 Stdio path is left without a context func; stdio handlers see no ctx-token,
Client(ctx)returns the singleton.
3. Handler refactor
- 3.1 Change every handler under
operation/**/*.goto pass itsctxtoforgejo.Client(ctx). Mechanical; 84 callsites. - 3.2 Update
pkg/forgejo/rawhttp.go(setCommonHeaders) to read the token fromctxfirst, falling back toflag.Token. Attachment upload/download paths must honor per-request identity. - 3.3 Adjust all attachment handlers in
operation/attachment/to passctxinto the raw-http path.
4. Tests
- 4.1
pkg/forgejo/forgejo_test.goexists, exercisesWithToken→Client(ctx)→ distinct client returned. - 4.2 Blocker fix — replace the pointer-inequality assertion (
c1 != c2) with anhttptest.Serverbased test. Two parallel goroutines, each callingClient(ctx)with a distinct token, each making an outbound SDK call. The test server records inboundAuthorizationheaders and asserts each goroutine's request arrives carrying its own token (not the other's, not the global). This is the only test that proves the no-leakage requirement. - 4.3 Add a test for the D3 failure path: stub
forgejo.NewClientto return an error, supply a ctx-token, assertClient(ctx)returns error / nil (after 1.3 lands) rather than silently returning the singleton. - 4.4 Add a test for case-insensitive scheme matching (after 2.4 lands):
bearer xyz,BEARER xyz,TOKEN xyzall resolve to the same context token. - 4.5 Existing tests under
test/race/continue to pass with the singleton path exercised in stdio mode.
5. Documentation
- 5.1 Add a "Multi-tenant HTTP mode" section to README with a runnable showboat demo: start the server with no
--token, twocurlcalls with distinctAuthorizationheaders hitting the sameMcp-Session-Idshow distinct identities viaget_my_user_info, a third call with a bogus token returns the 401. Showboat ask already posted on PR #138. - 5.2 Cross-link this openspec change from the README section so future readers can find the rationale.
6. Verification
- 6.1
make buildpasses against PR #138's branch. - 6.2
go test ./...passes against PR #138's branch. - 6.3
openspec validate stateless-http-auth --strictpasses. - 6.4 Manual smoke: start server with
--transport http --url https://codeberg.org(no--token); send two requests with distinct tokens; eachget_my_user_inforeturns the correct distinct identity. - 6.5 Manual smoke: send a request with
Authorization: bearer xyz(lowercase scheme) and confirm it resolves to the per-request identity, not the global fallback. (Validates 2.4.) - 6.6 Manual smoke: send a request with a deliberately broken Forgejo URL so the SDK's version probe fails; confirm the request returns an error rather than silently authenticating as the global token. (Validates 1.3.)
- 6.7 After PR #138 merges with blocker fixes, archive this change under
openspec/changes/archive/<date>-stateless-http-auth/.