feat(mcp): Cognito OAuth gate for multi-user MCP behind mPass#33
Merged
Conversation
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>
There was a problem hiding this comment.
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.
- 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>
Comment on lines
+93
to
+97
| function redirectUriAllowed(uri: string, allowList: string[] | null): boolean { | ||
| if (allowList === null) { | ||
| return true; | ||
| } | ||
| return allowList.some((entry) => { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Puts the integrated Penpot MCP server (
mcp/) behind AWS Cognito / mPass SSO, mirroring thesurfsense-mcp-server/plane-mcp-serverflow — MCP clients (Claude, Cursor, MCP Inspector) connect tohttps://design-mcp.<domain>/mcpand 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+/tokenproxied with a two-hop redirect through/auth/callback(Cognito has no DCR and only accepts pre-registered redirect URIs). Inbound/mcp/sse/messagesBearers are Cognito access tokens, JWKS-verified per request. The resolved identity reaches the untouched upstream handlers asreq.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 injectedX-Auth-Request-Email/X-Auth-Request-Userheaders — usable email first, then username; bare askii ids are valid keys (they join the pool's federated/native twin identities) whilecognito:*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-idto its creating identity, and fails closed (401) when no identity can be resolved. OAuth state persists in Valkey (AES-256-GCM at rest) viaMCP_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 unlessCOGNITO_USER_POOL_IDis set.Also
user:readgranted to the built-in MCP plugin manifest (penpot.currentUser/activeUserswere permission-denied) — both the cljsdefault-manifestand the staticmanifest.json, kept in sync.mcp/scripts/buildstrips the"penpot-mcp": "file:.."workspace self-reference from the shipped bundlepackage.json(pnpm packed/opt/penpotinto itself during the image build →ERR_PNPM_ENOENT).ioredis/joseare esbuild externals (bundling the CJSrequireof node builtins throws in ESM output); they install in the image via the bundle's existingpnpm install -P.Docs:
mcp/docs/moneta-cognito-auth.md. Companion devstack PR infoss-server-bundlepromotes thepenpot-mcpservice to the Compose base.Verified
tsc --noEmit, prettier, esbuild bundle clean🤖 Generated with Claude Code