Skip to content

feat(serveredition): per-user brokered-credential REST surfaces (spec 074 T8)#692

Open
Dumbris wants to merge 3 commits into
mainfrom
074-t8-credentials-rest
Open

feat(serveredition): per-user brokered-credential REST surfaces (spec 074 T8)#692
Dumbris wants to merge 3 commits into
mainfrom
074-t8-credentials-rest

Conversation

@Dumbris

@Dumbris Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member

Summary

Spec 074 T8 — the per-user brokered-credential REST API for the server edition, mounted behind the session/JWT auth middleware. Builds on T5 (OAuthConnector) and T6 (CredentialResolver), whose blockers just resolved.

Endpoint Description
GET /api/v1/user/credentials List connection status + non-secret metadata for every brokered upstream
DELETE /api/v1/user/credentials/{server} Disconnect/revoke the caller's credential
GET /api/v1/user/credentials/{server}/connect Initiate Path B; 302-redirect to the upstream authorize URL
GET /api/v1/user/credentials/{server}/callback Validate state, exchange code, persist the per-user credential, redirect to /ui/

Security (FR-026 / FR-027)

  • No secrets in responses — the list view (CredentialStatus) has no access_token/refresh_token fields; only status + non-secret metadata (mode, scopes, token_type, audience, obtained_via, expiry). Tests assert seeded token strings never appear in the body.
  • Per-user only — list/delete key the store by the authenticated userID; the connect callback persists under the initiating user (the connector binds state -> userID), so a callback cannot write into another user's record. A cross-user test proves user A can neither see nor delete user B's credential.

Design

  • connectorProvider caches one broker.OAuthConnector per oauth_connect upstream (keyed by serverKey) so the in-memory PKCE/state survives between the connect redirect and the callback. It satisfies broker.ConnectorProvider, so T7's resolver can mint connect URLs through the same connectors (exposed via CredentialHandlers.ConnectorProvider()).
  • Statuses: connected / expired / not_connected / unavailable (store disabled). oauth_connect upstreams that aren't usable expose an actionable connect_path.

Tests (TDD)

internal/serveredition/api/credential_handlers_test.go: secret redaction, status mapping, store-disabled, delete-removes, unknown-server 404, cross-user isolation, connect redirect, connect→callback end-to-end persistence (via httptest upstream token endpoint), denied callback, unauthenticated 401.

Verification

  • go build -tags server ./cmd/mcpproxy and go build ./cmd/mcpproxy — both green.
  • go test -tags server -race ./internal/serveredition/... — all packages pass.
  • golangci-lint v2.5.0 --config .github/.golangci.yml --build-tags server ./internal/serveredition/... — new files clean (2 remaining findings are pre-existing in unrelated test files and are not linted by CI, which doesn't pass --build-tags server).

Docs (ENG-9)

CLAUDE.md server API table + docs/features/idp-token-storage.md credentials-API section. oas/swagger.yaml is unchanged: it's generated from personal-edition swag annotations (make swagger), and server-edition routes are build-tagged and not scanned — consistent with every other /api/v1/user/* and /api/v1/auth/* server-edition endpoint, none of which appear in swagger.yaml. verify-oas-coverage.sh only scans internal/httpapi/server.go; this PR adds zero routes there, so personal-edition coverage is unaffected.

Related spec 074, MCP-1041.

… 074, MCP-1041)

Add the T8 REST API for per-user upstream credentials (server edition),
mounted behind the session/JWT auth middleware:

  - GET    /api/v1/user/credentials            list connection status +
           non-secret metadata (connected/expired/not_connected/unavailable);
           access/refresh tokens are NEVER serialized (FR-026).
  - DELETE /api/v1/user/credentials/{server}   disconnect/revoke the caller's
           own credential.
  - GET    /api/v1/user/credentials/{server}/connect   initiate Path B; builds
           a PKCE authorize URL bound to the user and 302-redirects upstream.
  - GET    /api/v1/user/credentials/{server}/callback  validates state, exchanges
           the code, persists the per-user credential, redirects to the Web UI.

Every operation is scoped to the authenticated caller (FR-027): list/delete key
the store by the caller's userID, and the connect callback persists under the
*initiating* user (the connector binds state->userID), so a callback cannot
write into another user's record.

A connectorProvider caches one OAuthConnector (T5) per oauth_connect upstream so
the in-memory PKCE/state survives between the connect redirect and the callback,
and satisfies broker.ConnectorProvider so the T6 CredentialResolver can mint
connect URLs through the same connectors.

TDD: secret redaction, status mapping, store-disabled, delete-removes,
unknown-server 404, cross-user isolation, connect redirect, connect+callback
end-to-end credential persistence, denied callback, unauthenticated 401.

Docs: CLAUDE.md server API table + docs/features/idp-token-storage.md credentials
API section. swagger.yaml is generated from personal-edition swag annotations
(server-edition routes are build-tagged and not scanned), consistent with every
other /api/v1/user/* and /api/v1/auth/* server-edition endpoint.

Related spec 074, MCP-1041 (blocked-by T5/T6, now resolved).
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 15, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 61f0c32
Status: ✅  Deploy successful!
Preview URL: https://f1f9c72f.mcpproxy-docs.pages.dev
Branch Preview URL: https://074-t8-credentials-rest.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: 074-t8-credentials-rest

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (14 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (19 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 27570311373 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris

Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

Review verdict: REQUEST CHANGES (not ACCEPT)

Reviewed at head b1be0481a9b9. The implementation is solid, but one PR-caused CI failure blocks the gate.

🔴 Blocker — check-size failure (caused by this PR)

This PR adds 4 rows to the server-edition API table in CLAUDE.md, taking it from 39,949 → 40,411 chars, crossing the repo's hard 40,000-char limit enforced by the check-size workflow.

Fix: move the new credentials table out of CLAUDE.md into docs/features/idp-token-storage.md (already edited by this PR), or trim equivalent content, to get back under 40k. main was already at 39,949, so even a small addition trips it.

🟡 Pre-existing, NOT a defect in this PR — Windows unit test

Unit Tests (windows-latest) fails on TestForwardContentResult_MultipleTextBlocksDistinctKeys (internal/server/content_forward_test.go). This file is not touched by this PR (changes are confined to internal/serveredition, docs, and CLAUDE.md), and the test passes on ubuntu/macos. This is a pre-existing Windows-only failure that should be tracked separately — it is not a regression from this change, though it does mechanically block the merge gate.

🟢 Code quality — strong

  • FR-026 (no secrets): CredentialStatus omits token fields; populateFromCredential copies only non-secret metadata. Verified by TestCredentialsList_RedactsSecrets.
  • FR-027 (per-user isolation): list/delete key the store by authenticated userID; callback persists under the initiating user via state→userID binding. Verified by TestCredentials_CrossUserIsolation.
  • Connector caching rationale (PKCE/state must survive connect→callback) is clear and correct; setup.go wiring reuses the existing cred store cleanly.

🔵 Minor (non-blocking) — latent redirect_uri edge case

connectorProvider.connector() builds the connector with RedirectURI derived from p.baseURL, which is only populated by observeBaseURL during a connect HTTP request. If T6's CredentialResolver calls ConnectorFor() (which builds + caches the connector) before any connect request sets baseURL, the connector is cached with an empty-host redirect_uri — and OAuth requires redirect_uri to be byte-identical at token exchange. Edge case dependent on T6 wiring (not in this PR), but worth a guard or a note.

To unblock: trim CLAUDE.md back under 40k. Once check-size is green, this is an ACCEPT on the merits.

The T8 server-API table addition pushed CLAUDE.md from 39,949 to 40,411
chars, tripping the claude-md-check (check-size) 40,000-char gate. The
credential endpoints are already fully documented in
docs/features/idp-token-storage.md (the canonical server-edition home), so
drop the redundant CLAUDE.md table rows rather than trim unrelated content.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@Dumbris

Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

CodexReviewer verdict: ACCEPT

Scope: Spec 074 T8 — per-user brokered-credential REST surfaces (server edition), head 38041034.

Verification (run locally on PR head)

  • go build -tags server ./cmd/mcpproxy — ✅ green
  • go test -tags server -race -count=1 ./internal/serveredition/... — ✅ all packages pass (api 7.5s, broker, multiuser, auth, users, workspace)

Why ACCEPT

  • Secret redaction (FR-026): CredentialStatus has no access_token/refresh_token fields; populateFromCredential copies only non-secret metadata. TestCredentialsList_RedactsSecrets asserts seeded token strings never appear in the body.
  • Per-user isolation (FR-027): list/delete key the store by the authenticated userID; the connect callback persists under the initiating user via the connector's state→userID binding, not the callback's caller — so a callback cannot write into another user's record. TestCredentials_CrossUserIsolation proves A can neither see nor delete B's credential.
  • Connect flow: connectorProvider caches one connector per oauth_connect upstream so the in-memory PKCE/state survives connect→callback; redirect_uri is fixed once via observeBaseURL (OAuth byte-identical requirement). Denied/failed authorizations store nothing and redirect with an error flag.
  • TDD coverage: status mapping, store-disabled, delete-removes, unknown-server 404, connect redirect, connect→callback end-to-end persistence (httptest upstream), denied callback, unauthenticated 401. Codecov reports all modified lines covered.
  • Routes mounted behind the session/JWT auth middleware in setup.go; clean separation, no routes added to personal-edition internal/httpapi/server.go (swagger/OAS coverage unaffected, consistent with other /api/v1/user/* server-edition endpoints).

Non-blocking notes (future hardening, not for this PR)

  • connectorProvider holds PKCE/state in-memory → connect and callback must land on the same replica. Fine for current single-node server edition; document/revisit before horizontal scale-out.
  • observeBaseURL latches the gateway origin from the first request seen; benign given a stable public origin, but worth pinning to config if deployments vary Host.
  • CredentialHandlers.ConnectorProvider() is exposed for the T7 resolver but not yet wired into a resolver in setup.go — expected to be consumed by a follow-up task.

No correctness, security, or test-quality blockers. Ready for the Gatekeeper sweep to approve.

@Dumbris

Dumbris commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

CodexReviewer (recovery by CEO) — Verdict: ACCEPT ✅

Reviewed at head 38041034 (advanced from the dispatched b1be0481). +909/−0, 5 files.

Verification

  • go build -tags server ./internal/serveredition/...
  • go vet -tags server ./internal/serveredition/api/...
  • go test -tags server -count=1 ./internal/serveredition/api/... ✅ — all 11 new credential tests pass

Correctness & security

  • FR-026 (no secrets in responses): CredentialStatus omits access/refresh tokens; verified by TestCredentialsList_RedactsSecrets.
  • FR-027 (per-user isolation): all handlers scope by authenticated userID; connect/callback persist under the initiating user (state-bound), so a callback cannot write another user's record; verified by TestCredentials_CrossUserIsolation.
  • Connector caching keyed by serverKey preserves PKCE/state across connect→callback under a mutex; redirect_uri fixed once for byte-identical token exchange.
  • Edge cases covered: store-disabled → unavailable, unknown server → 404, non-connect mode → 400, unauthenticated → 401, upstream-denied → redirect with nothing stored.
  • Build tags //go:build server consistent; setup.go reuses the existing credStore; docs added.

Minor (non-blocking)

  • In-memory connector state is lost on restart → pending connect flows must be retried (graceful).
  • baseURL is first-request-wins for multi-host gateways (documented as intentional).

No blocking issues. ACCEPT.

Maintainer fix-before-merge: PR #692 tripped the PR-size review gate at
~913 added lines. Remove the docs/features/idp-token-storage.md credentials
API section from this PR so it reduces to handlers + tests only (~852
lines). The docs land in a separate follow-up PR so they're not lost.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants