Skip to content

feat(teams): CredentialStore interface + AES-256-GCM BBolt backend (074-T1) - #587

Merged
Dumbris merged 1 commit into
mainfrom
mcp-1034-credential-store
Jun 4, 2026
Merged

feat(teams): CredentialStore interface + AES-256-GCM BBolt backend (074-T1)#587
Dumbris merged 1 commit into
mainfrom
mcp-1034-credential-store

Conversation

@Dumbris

@Dumbris Dumbris commented Jun 4, 2026

Copy link
Copy Markdown
Member

Summary

Foundation task MCP-1034 / 074-T1 for upstream token brokering (spec 074). No blockers. New server-edition package internal/teams/broker/ (//go:build server).

  • CredentialStore interface (Get/Put/Delete/List) — the abstraction seam so external secret managers (Vault/AWS) can be added later without changing callers (FR-023).
  • BBoltAESStore backend — bucket user_upstream_credentials:
    • upstream creds keyed <userID>:<serverKey> (serverKey = existing SHA256(name+url) scheme, see oauth.GenerateServerKey)
    • idp subject tokens keyed by bare <userID> (empty serverKey selects them)
  • UpstreamCredential value model (Type, AccessToken, RefreshToken, ExpiresAt, Scopes, TokenType, Audience, ObtainedVia, UpdatedAt), JSON-serialized then AES-256-GCM encrypted with a per-record random nonce (FR-020), isolated per user (FR-021).
  • Master key from env MCPPROXY_CRED_KEY or teams.credential_encryption_key (32-byte base64). Missing key → store disabled gracefully (rest of gateway unaffected, logged for startup surfacing); present-but-invalid → loud error (FR-022).
  • Decryption failure (e.g. rotated key) → record treated as absent, never fatal; List skips undecryptable records.
  • Adds teams.credential_encryption_key config field as the config-side key source.

Mirrors the obot-platform/mcp-oauth-proxy AES approach.

Tests (TDD — test-first, all green with -race)

  • encrypt/decrypt roundtrip (+ asserts plaintext token absent on disk)
  • per-user isolation (incl. userID-prefix bleed guard)
  • subject-token keying + List exclusion
  • nonce uniqueness (same plaintext → different ciphertext)
  • expiry helpers (IsExpired/IsValid/ExpiresWithin, zero = never)
  • missing-key disabled path (all ops → ErrStoreDisabled)
  • invalid/short key → constructor error
  • key-changed → ErrNotFound (not-connected)
  • ResolveMasterKey env-over-config precedence

Verification

go build ./cmd/mcpproxy                         # personal: OK
go build -tags server ./cmd/mcpproxy            # server:   OK
go test -tags server -race ./internal/teams/broker/ ./internal/config/   # ok
golangci-lint run --build-tags=server ./internal/teams/broker/...        # 0 issues

Notes

  • Docs: no docs/ file documents teams config keys (they live in CLAUDE.md, which is at the 40k CI size limit); the field is an inert foundation seam not yet wired/activated, so user-facing docs are deferred to the downstream startup-wiring task. Spec FRs already documented in specs/074-upstream-token-brokering/spec.md.
  • Out of scope (downstream tasks): startup wiring + "surface disabled at startup", token-exchange/refresh, callers.

Related spec: specs/074-upstream-token-brokering · FR-020..FR-023

Foundation for upstream token brokering (spec 074, MCP-1034). New
server-edition package internal/teams/broker provides:

- CredentialStore interface (Get/Put/Delete/List) as the abstraction seam
  for future external secret-manager backends (FR-023).
- BBoltAESStore: bucket "user_upstream_credentials", keyed
  "<userID>:<serverKey>" for upstream creds and bare "<userID>" for idp
  subject tokens. serverKey follows the existing SHA256(name+url) scheme.
- UpstreamCredential value model, JSON-serialized then AES-256-GCM
  encrypted with a per-record random nonce (FR-020) and isolated per user
  (FR-021).
- Master key resolved from MCPPROXY_CRED_KEY env or
  teams.credential_encryption_key (32-byte base64). Missing key ->
  store disabled gracefully; present-but-invalid -> loud error (FR-022).
- Decryption failure (e.g. rotated key) treated as record-absent, never
  fatal; List skips undecryptable records.

Adds teams.credential_encryption_key config field as the config-side key
source. TDD: roundtrip, per-user isolation, nonce uniqueness, expiry
helpers, missing-key disabled path, key-changed -> not-found.

Related spec: specs/074-upstream-token-brokering
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2526aa9
Status: ✅  Deploy successful!
Preview URL: https://69aeac4b.mcpproxy-docs.pages.dev
Branch Preview URL: https://mcp-1034-credential-store.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 4, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: mcp-1034-credential-store

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 (24 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 26935941638 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris
Dumbris merged commit a7412ff into main Jun 4, 2026
37 checks passed
Dumbris added a commit that referenced this pull request Jun 4, 2026
…(spec 074, MCP-1035) (#588)

* feat(config): per-upstream auth_broker block + teams credential keys (spec 074)

Add server-edition configuration surface for per-user upstream token
brokering (spec 074, T2 / MCP-1035):

- ServerConfig.AuthBroker *AuthBrokerConfig with mode
  (token_exchange|entra_obo|oauth_connect), token_endpoint, resource
  (RFC 8707 audience), scopes, client_id/secret, and configurable header
  + header_format (defaults "Authorization" / "Bearer {token}", FR-016).
- TeamsConfig.CredentialEncryptionKey (env fallback MCPPROXY_CRED_KEY,
  explicit config wins) and StoreIDPTokens bool, default false (FR-006).
- Validation rejects auth_broker on stdio/non-HTTP-family upstreams with
  an "unsupported in this phase" message (FR-002); HTTP-family upstreams
  pass and have header defaults applied. Opt-in per server; upstreams
  without a broker behave exactly as today (FR-003).

The AuthBrokerConfig type and validation are behind //go:build server with
a personal-edition stub (empty struct + no-op validator), so the personal
edition is unaffected. AuthBroker carries swaggerignore (mirrors Teams) —
swagger-verify confirms no OpenAPI drift.

Related #587

* test(storage): cover ServerConfig.AuthBroker in saveServerSync field canary

AuthBroker (spec 074) is server-edition per-upstream broker config carried
in the JSON config (like Shared), not persisted to the BBolt UpstreamRecord.
Mark it intentionally excluded in TestSaveServerSyncFieldCoverage so the
field-coverage canary passes.

Related #588
Dumbris added a commit that referenced this pull request Jun 10, 2026
…MCP-1036) (#601)

* feat(teams): persist + refresh IdP subject token at login (spec 074, MCP-1036)

Today HandleCallback fetched userinfo then discarded the provider token. When
teams.store_idp_tokens is enabled, capture the IdP access + refresh token at
login and persist it encrypted (AES-256-GCM) via the credential store as an
idp_subject_token record keyed by userID (empty serverKey).

Adds GetValidIDPSubjectToken: returns a non-expired token, refreshing via the
provider refresh_token grant when expired/near-expiry; when no valid token can
be produced (absent, store disabled, expired-and-not-refreshable, or refresh
failed) it returns ErrReauthRequired — never a stale token. This is the
prerequisite seam consumed by the credential resolver (Path A token exchange,
MCP-1039).

Default-off: with store_idp_tokens false (default) or no encryption key, login
behaves exactly as before — no storage. Persist is best-effort: a write failure
never breaks login.

- OAuthProvider.RefreshAccessToken: refresh_token grant (RFC 6749 §6)
- OAuthHandler.credStore + SetCredentialStore; wired in teams/setup.go
- TDD: capture-when-enabled, no-storage-when-disabled, refresh-near-expiry,
  expired-not-refreshable -> re-auth, absent -> re-auth, store-disabled -> re-auth

FR-004, FR-005, FR-006. Related #588 (T2 config), #587 (T1 store).

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

* fix(teams/auth): request offline access when store_idp_tokens enabled

When teams.store_idp_tokens is on, the login authorization request must ask
providers for a durable refresh token so GetValidIDPSubjectToken can actually
refresh instead of returning ErrReauthRequired after expiry (Codex finding on
PR #601).

Provider-specific contracts (FR-006 default-off — login unchanged otherwise):
- Google:    access_type=offline + prompt=consent (OfflineAuthParams)
- Microsoft: offline_access scope (OfflineAccessScopes)
- GitHub:    unchanged (no refresh-token mechanism in scope)

BuildAuthURL gains an offlineAccess bool; caller (HandleLogin) sets it from
config.StoreIDPTokens. New tests:
- TestBuildAuthURL_OfflineAccess_Google / _Microsoft
- TestHandleLogin_RequestsOfflineAccess (default-off assertion added to
  TestHandleLogin_Redirects)

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

* docs: add idp-token-storage operator guide (store_idp_tokens + credential_encryption_key)

Satisfies ENG-9 requirement from Codex R2 review on PR #601: document the
operator/security model introduced by idp_subject_token.go and setup.go:
- store_idp_tokens config field (default off)
- credential_encryption_key / MCPPROXY_CRED_KEY env override
- AES-256-GCM at-rest encryption model, key generation, key rotation caveat
- Token lifecycle (login → use → refresh → re-auth)

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

* docs: fix IdP token-storage bucket name (user_upstream_credentials)

CodexReviewer caught that idp-token-storage.md named the key-rotation cleanup
bucket as 'upstream_credentials' but the actual store is 'user_upstream_credentials'
(internal/teams/broker/bbolt_aes.go:28) — following the doc would clear the wrong
bucket and leave IdP tokens behind.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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