Skip to content

feat(mcp): Cognito OAuth gate for multi-user MCP behind mPass#33

Merged
jawad-khan merged 2 commits into
foss-sandboxfrom
feat/mcp-cognito-gate
Jun 12, 2026
Merged

feat(mcp): Cognito OAuth gate for multi-user MCP behind mPass#33
jawad-khan merged 2 commits into
foss-sandboxfrom
feat/mcp-cognito-gate

Conversation

@hunzlahmalik

Copy link
Copy Markdown

What

Puts the integrated Penpot MCP server (mcp/) behind AWS Cognito / mPass SSO, mirroring the surfsense-mcp-server / plane-mcp-server flow — MCP clients (Claude, Cursor, MCP Inspector) connect to https://design-mcp.<domain>/mcp and auto-OAuth; no token paste.

How it works

MCP leg. The server is its own OAuth 2.0 authorization server over Cognito: RFC 8414/9728 discovery, RFC 7591 DCR shim over the pre-registered app client, and /authorize + /token proxied with a two-hop redirect through /auth/callback (Cognito has no DCR and only accepts pre-registered redirect URIs). Inbound /mcp /sse /messages Bearers are Cognito access tokens, JWKS-verified per request. The resolved identity reaches the untouched upstream handlers as req.query.userToken.

Plugin leg / pairing. Tools execute through the Penpot MCP plugin in the user's browser (wss://design.<domain>/mcp/ws, through Traefik + mPass). The bridge pairs each plugin connection to its MCP sessions by the injected X-Auth-Request-Email / X-Auth-Request-User headers — usable email first, then username; bare askii ids are valid keys (they join the pool's federated/native twin identities) while cognito:* placeholder values are rejected so users can never collide on one key.

Multi-user. Cognito mode forces multi-user mode (no "any connected plugin" fallback), pins each mcp-session-id to its creating identity, and fails closed (401) when no identity can be resolved. OAuth state persists in Valkey (AES-256-GCM at rest) via MCP_OAUTH_STORAGE_URL.

Fork isolation

All fork logic lives in mcp/packages/server/src/moneta/ (config / storage / cognito / provider / install / bridge). Upstream files carry three one-line hooks: index.ts (force multi-user), PenpotMcpServer.start() (install the gate), PluginBridge (header identity). The whole gate is a no-op unless COGNITO_USER_POOL_ID is set.

Also

  • user:read granted to the built-in MCP plugin manifest (penpot.currentUser / activeUsers were permission-denied) — both the cljs default-manifest and the static manifest.json, kept in sync.
  • mcp/scripts/build strips the "penpot-mcp": "file:.." workspace self-reference from the shipped bundle package.json (pnpm packed /opt/penpot into itself during the image build → ERR_PNPM_ENOENT).
  • ioredis / jose are esbuild externals (bundling the CJS require of node builtins throws in ESM output); they install in the image via the bundle's existing pnpm install -P.

Docs: mcp/docs/moneta-cognito-auth.md. Companion devstack PR in foss-server-bundle promotes the penpot-mcp service to the Compose base.

Verified

  • tsc --noEmit, prettier, esbuild bundle clean
  • Live end-to-end in the devstack: Inspector OAuth → Cognito → tool calls executing in the paired user's design file; cross-identity session reuse rejected; identity-less plugin connections rejected in multi-user mode

🤖 Generated with Claude Code

Make the MCP server its own OAuth 2.0 authorization server over AWS
Cognito (RFC 8414/9728 discovery, RFC 7591 DCR shim, /authorize +
/token proxied with a two-hop redirect through /auth/callback),
mirroring the surfsense-mcp / plane-mcp flow so MCP clients (Claude,
Cursor, Inspector) auto-OAuth. Inbound /mcp /sse /messages requests are
JWKS-verified Cognito Bearers; the resolved identity is surfaced to the
untouched upstream handlers as req.query.userToken.

Plugin connections pair by the mPass identity headers injected on
wss://design.<domain>/mcp/ws: usable email first, then username. Bare
askii ids are accepted as pairing keys (they join the pool's
federated/native twin identities), while cognito:* placeholder values
are rejected so users can never collide on a shared key. Cognito mode
forces multi-user mode and pins each MCP session id to the identity
that created it.

All fork logic lives in packages/server/src/moneta/ with three one-line
upstream hooks (index.ts, PenpotMcpServer.start, PluginBridge). OAuth
state persists in Valkey (AES-256-GCM at rest, key HKDF-derived from
OIDC_CLIENT_SECRET or MCP_JWT_SIGNING_KEY) via MCP_OAUTH_STORAGE_URL,
in-memory otherwise.

Also:
- grant user:read to the built-in MCP plugin manifest so
  penpot.currentUser / penpot.activeUsers work (runtime permission)
- strip the workspace self-reference from the shipped bundle
  package.json (pnpm packed /opt/penpot into itself during the image
  build - ERR_PNPM_ENOENT)
- externalize ioredis/jose in the esbuild bundle (bundled CJS require
  of node builtins throws in ESM output)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional “Moneta” Cognito/OIDC OAuth gate in front of the Penpot MCP server so MCP clients can auto-complete OAuth (discovery + DCR + authorize/token), while preserving existing upstream MCP handlers by only injecting identity as req.query.userToken and pairing via mPass-injected headers on the plugin WebSocket leg.

Changes:

  • Introduces mcp/packages/server/src/moneta/* implementing Cognito proxy auth, OAuth state storage (memory or Valkey/Redis + AES-GCM), and Express middleware installation.
  • Forces multi-user mode when Cognito auth is enabled and updates plugin pairing to prefer mPass identity headers over ?userToken=.
  • Updates build packaging and dependencies (externalizing ioredis/jose), plus aligns plugin permissions (user:read) and adds documentation.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
mcp/scripts/build Strips workspace self-reference from shipped package.json to avoid pnpm packing issues in images.
mcp/pnpm-lock.yaml Adds lock entries for new runtime deps (ioredis, jose) and transitive packages.
mcp/packages/server/src/PluginBridge.ts Prefers Moneta identity headers for plugin WS pairing in Cognito mode.
mcp/packages/server/src/PenpotMcpServer.ts Installs Moneta auth middleware before registering upstream MCP routes.
mcp/packages/server/src/moneta/storage.ts Adds OAuth state storage with optional Valkey/Redis backend and at-rest encryption.
mcp/packages/server/src/moneta/provider.ts Implements SDK OAuthServerProvider facade over Cognito (DCR shim + authorize/token + token verification).
mcp/packages/server/src/moneta/install.ts Wires CORS, discovery/auth routes, callback, bearer guard, session binding, and identity injection middleware.
mcp/packages/server/src/moneta/index.ts Exposes Moneta entrypoints (monetaAuthEnabled, installMonetaAuth, identity helper).
mcp/packages/server/src/moneta/config.ts Adds env-driven configuration and the COGNITO_USER_POOL_ID feature switch.
mcp/packages/server/src/moneta/cognito.ts Adds Cognito OIDC discovery, JWKS validation, exchanges, userInfo fallback, identity extraction.
mcp/packages/server/src/moneta/bridge.ts Reads mPass-injected identity headers on WS upgrade for plugin pairing.
mcp/packages/server/src/index.ts Forces multi-user mode when Moneta auth is enabled.
mcp/packages/server/package.json Adds deps + esbuild externals for ioredis/jose.
mcp/packages/plugin/public/manifest.json Adds user:read permission for MCP plugin.
mcp/docs/moneta-cognito-auth.md Documents the two-leg pairing model, env vars, and devstack setup.
frontend/src/app/main/data/workspace/mcp.cljs Adds user:read permission to the CLJS default manifest (kept in sync with static manifest).
Files not reviewed (1)
  • mcp/pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mcp/packages/server/src/moneta/bridge.ts
Comment thread mcp/packages/server/src/moneta/install.ts
Comment thread mcp/packages/server/src/moneta/provider.ts Outdated
Comment thread mcp/packages/server/src/moneta/bridge.ts
Comment thread mcp/packages/server/src/moneta/cognito.ts
Comment thread mcp/packages/server/src/moneta/cognito.ts
- log plugin-connection identity as 8-char fingerprints at info (full
  values only at debug) so production logs carry no raw identifiers
- warn at startup when MCP_ALLOWED_CLIENT_REDIRECT_URIS is unset in
  production (open DCR redirect allow-list)
- make wildcard redirect-URI matching URL-aware: require exact origin
  equality before the prefix test so a broad entry can never match a
  host extension like https://example.com.evil

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.

Files not reviewed (1)
  • mcp/pnpm-lock.yaml: Generated file

Comment on lines +93 to +97
function redirectUriAllowed(uri: string, allowList: string[] | null): boolean {
if (allowList === null) {
return true;
}
return allowList.some((entry) => {
@jawad-khan jawad-khan merged commit f91fc36 into foss-sandbox Jun 12, 2026
1 of 11 checks passed
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.

3 participants