-
Notifications
You must be signed in to change notification settings - Fork 1
added auth to mcp server, uses OAuth bridge to privy.io #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
WalkthroughThis pull request introduces OAuth 2.0 authorization server capabilities with PKCE support, Privy authentication integration, and a locally-bundled OAuth consent UI. Changes span configuration, backend authentication middleware, OAuth token/flow management, frontend consent UI components, and build configuration updates. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as OAuth Consent UI
participant Server as Express Server
participant Privy as Privy Auth
participant OAuth as OAuth Server
User->>UI: Opens /oauth/authorize
Server->>UI: Renders consent page + context injection
UI->>UI: Parses AuthorizationPageContext from DOM
UI->>Privy: Initialize Privy client
User->>UI: Enters email address
UI->>Privy: Send OTP via email
Privy-->>User: Email with verification code
User->>UI: Enters verification code
UI->>Privy: Verify code & authenticate
Privy-->>UI: Access token (privyToken)
UI->>Server: POST /oauth/authorize/complete<br/>(state, privyToken)
Server->>Privy: Validate token
Privy-->>Server: Verified claims
Server->>OAuth: Complete authorization<br/>(issue code/token)
OAuth-->>Server: Authorization code
Server-->>UI: Redirect URI + code
UI->>Server: POST /oauth/token (code + PKCE verifier)
Server->>OAuth: Exchange code for tokens
OAuth-->>Server: Access + refresh tokens
Server-->>UI: Access token response
UI->>Server: Call protected /mcp endpoint<br/>(Bearer token)
Server->>Server: Validate token via authenticatePrivy
Server-->>UI: Protected resource
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas requiring extra attention:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
🧹 Nitpick comments (6)
src/server.ts (2)
189-189: Verify trust proxy configuration.Setting
trust proxytotruemakes Express trust all proxies. This is necessary forgetRequestOriginto correctly readx-forwarded-protoandx-forwarded-hostheaders (lines 241-244). However, in production, you should configure this to trust only specific proxies to prevent header spoofing attacks.Consider using a more restrictive configuration in production:
-app.set("trust proxy", true); +app.set("trust proxy", process.env.TRUST_PROXY || "loopback");Then set
TRUST_PROXYappropriately in your deployment environment (e.g.,"loopback, linklocal, uniquelocal"or a specific IP/CIDR range).
356-372: Missing CSRF protection on authorization completion endpoint.The
/oauth/authorize/completeendpoint accepts state and tokens via POST body without additional CSRF protection. While thestateparameter provides some binding to the authorization request, an attacker with access to a validstatevalue could potentially forge completion requests.Consider adding one of the following protections:
- Verify the request origin matches the expected domain
- Add a short-lived CSRF token to the authorization page context
- Ensure the consent UI submits via form POST from the same domain (already appears to be the case based on the architecture)
Since the consent UI is served from the same origin and the state is cryptographically random, the current implementation may be acceptable, but document this security assumption.
src/oauth.ts (4)
201-227: Expired record cleanup lacks concurrency protection.The
cleanupExpiredRecordsfunction iterates over maps and deletes entries while iterating. While JavaScript Map iteration is safe for deletions during iteration, there's no protection against concurrent access if multiple requests trigger cleanup simultaneously.Consider one of the following:
- Add a simple flag to prevent concurrent cleanup runs
- Use a scheduled background task instead of inline cleanup
- Accept that concurrent cleanup is safe for the in-memory implementation
For production, document that this implementation is not thread-safe and is intended for single-process deployment only.
397-406: Default client_id logic may cause confusion.Lines 397-406 automatically default to the first static client if only one exists and no
client_idis provided. This behavior is logged as a warning but might surprise developers.Consider making this behavior explicit via an environment variable like
OAUTH_ALLOW_DEFAULT_CLIENT_ID=trueto make the implicit behavior opt-in.
418-431: PKCE validation warnings are appropriate but permissive.The code logs warnings when PKCE parameters are missing (lines 418-420, 422-424) but proceeds without PKCE. This maintains backward compatibility but weakens security.
Consider adding a strict mode via environment variable:
const requirePKCE = process.env.OAUTH_REQUIRE_PKCE === "true"; if (!hasChallenge) { if (requirePKCE) { throw createOAuthError("invalid_request", "code_challenge is required when PKCE enforcement is enabled."); } console.warn("[oauth] Missing code_challenge; proceeding without PKCE validation."); }
550-554: Consider using stronger token generation.While
randomBytes(32).toString("base64url")provides 256 bits of entropy (lines 550, 553), which is cryptographically strong, tokens are currently stateful (stored in maps). For a stateless token approach, consider JWT tokens signed with a secret key.The current approach is secure for the in-memory implementation. If scaling to multiple servers, consider:
- Using Redis or a database for shared token storage
- Switching to signed JWT tokens for stateless validation
This is documented as a single-process limitation.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockwidgets/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (15)
.data/oauth-clients.json(1 hunks)README.md(6 hunks)env.example(1 hunks)package.json(1 hunks)spec.md(1 hunks)src/auth.ts(1 hunks)src/oauth.ts(1 hunks)src/privy.ts(1 hunks)src/server.ts(2 hunks)widgets/package.json(1 hunks)widgets/src/oauth-consent/App.tsx(1 hunks)widgets/src/oauth-consent/index.html(1 hunks)widgets/src/oauth-consent/index.tsx(1 hunks)widgets/src/oauth-consent/oauth-consent.css(1 hunks)widgets/vite.config.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
widgets/src/oauth-consent/App.tsx (1)
src/oauth.ts (1)
AuthorizationPageContext(67-79)
widgets/src/oauth-consent/index.tsx (1)
widgets/src/oauth-consent/App.tsx (1)
App(41-238)
src/oauth.ts (1)
src/privy.ts (2)
VerifyAuthTokenResponse(57-57)verifyPrivyToken(27-55)
src/auth.ts (2)
src/privy.ts (1)
VerifyAuthTokenResponse(57-57)src/oauth.ts (1)
validateAccessToken(764-798)
src/server.ts (2)
src/oauth.ts (12)
AuthorizationPageContext(67-79)serializeAuthorizationContext(844-846)authorizationServerMetadata(815-830)protectedResourceMetadata(832-838)listSupportedScopes(840-842)registerClient(306-358)prepareAuthorization(360-487)completeAuthorization(489-546)TokenEndpointResponse(81-87)exchangeCodeForTokens(594-657)refreshAccessToken(659-721)revokeToken(723-762)src/auth.ts (2)
authenticatePrivy(31-75)AuthenticatedRequest(13-23)
🔇 Additional comments (29)
env.example (1)
9-40: Well-documented environment variables for OAuth/Privy integration.The environment variables are clearly documented with inline comments explaining their purpose. The naming is consistent and follows best practices. The
DANGEROUSLY_OMIT_AUTHflag is appropriately named to warn developers about the security implications.README.md (2)
40-40: Good feature documentation.The addition of the bundled consent UI feature to the features list clearly communicates a key capability of the system.
174-188: Comprehensive security documentation.The new Privy Authentication and OAuth Facade sections provide clear guidance on authentication flows, token handling, and security configuration. The warning about
DANGEROUSLY_OMIT_AUTHis appropriately emphasized.widgets/src/oauth-consent/index.tsx (1)
1-12: Clean React 18 entry point implementation.The implementation correctly uses React 18's
createRootAPI and includes appropriate error handling for missing DOM containers. The code is minimal and focused on its single responsibility.widgets/src/oauth-consent/index.html (1)
1-13: Appropriate HTML scaffold for OAuth consent UI.The HTML structure is minimal and appropriate for a Vite-based React application. The
__OAUTH_CONTEXT__placeholder indicates server-side context injection, which is a standard pattern for embedding runtime configuration.package.json (1)
22-23: Privy SDK dependencies added correctly.The Privy SDK dependencies are properly placed alphabetically and use appropriate version ranges. Note that there is a version mismatch with
widgets/package.json(0.56.1 vs 0.57.0) that should be reconciled.widgets/vite.config.ts (2)
3-3: Import added for multi-entry build support.The
resolveimport frompathis correctly added to support the multi-entry build configuration.
15-16: Multi-entry build configuration.The build now supports both widget and OAuth consent UI entries, with appropriate output directory separation. This aligns with the PR's goal of bundling the OAuth consent UI locally.
widgets/src/oauth-consent/oauth-consent.css (1)
1-143: Polished consent page styling.Layout, focus states, and dark-mode handling look solid here.
src/privy.ts (1)
1-55: Privy client bootstrap looks sound.Env validation, client singleton, and defensive claim logging all check out.
spec.md (1)
1-73: Spec is comprehensive.The plan covers build outputs, server wiring, and risk mitigations clearly.
src/server.ts (8)
37-45: LGTM! Asset fallback handling is robust.The fallback logic correctly handles multiple asset paths for both build and development scenarios. The error message clearly identifies which paths were checked.
199-204: LGTM! OAuth asset caching is properly disabled.The explicit
Cache-Control: no-store, no-cache, must-revalidateheaders prevent browsers from caching OAuth assets, which is essential for ensuring users see the latest consent UI.
324-354: Handle empty query parameters more defensively.Line 325 returns a 204 response when no query parameters are present, which is a clean way to handle direct navigation. However, ensure this doesn't conflict with OAuth clients that might send empty-string parameters.
The implementation looks correct, but verify that OAuth clients (e.g., ChatGPT connector) don't send requests with empty query parameter values that would bypass this check.
374-405: LGTM! Token endpoint properly handles multiple grant types.The implementation correctly branches on
grant_typeand calls the appropriate OAuth functions. TheCache-Controlheaders are properly set to prevent token caching.
415-434: Userinfo endpoint implementation follows OpenID Connect standards.The endpoint correctly requires authentication and returns standard OIDC claims. The
Cache-Controlheaders prevent caching of user information.
440-440: Authentication requirement on MCP endpoint is correctly enforced.The
/mcpendpoint now requires authentication viaauthenticatePrivymiddleware, which validates the OAuth access token. This properly secures the MCP protocol endpoint.
50-59: Regex pattern is safe; HTML attributes use double quotes consistently.The codebase's HTML templates (
widgets/src/echo/index.htmlandwidgets/src/oauth-consent/index.html) use exclusively double-quoted attributes (e.g.,src="./index.tsx",type="module"). No single-quoted or unquoted attributes are present. The regex pattern/(src|href)="\/([^"]+)"/gcorrectly targets double-quoted attributes, which is the format used in the actual source HTML. The bundler-generated HTML processed byrewriteAssetUrlswill maintain this well-formed structure.
1-1: dotenv/config is correctly positioned as first import.Verified that
src/server.tsis the sole entry point (compiled todist/server.jsand executed via start script), withimport "dotenv/config"at line 1 before any other imports. Module-level environment variable reads in dependent modules (src/auth.ts,src/privy.ts) occur after the dotenv side-effect loads the.envfile synchronously. No alternative entry points exist that could bypass this initialization.src/oauth.ts (10)
193-195: LGTM! Token generation uses cryptographically secure randomness.The
generateIdfunction correctly usesrandomBytesfrom the crypto module, which provides cryptographically secure random values. The prefix helps with token identification and debugging.
197-199: LGTM! PKCE code challenge implementation is correct.The SHA-256 hashing with base64url encoding matches the PKCE specification (RFC 7636).
267-304: Redirect URI validation correctly enforces security constraints.The validation properly:
- Requires absolute URIs
- Restricts to HTTP/HTTPS protocols
- Allows HTTP only for localhost (lines 285-294)
- Requires exact match against registered URIs (line 296)
306-358: Client registration validates redirect URIs during registration.The function correctly validates all redirect URIs before creating the client and persists the registered client immediately.
489-546: Authorization completion includes fallback token mechanism.The
completeAuthorizationfunction accepts both a primary token and a fallback token (lines 491-492), attempting verification with fallback if primary fails (lines 506-518). This is a good resilience pattern.Ensure that the fallback mechanism is documented and that the consent UI understands when to provide a fallback token.
548-592: Token issuance correctly handles offline_access scope.The
issueTokensfunction only issues refresh tokens whenoffline_accessis included in the scope (line 551), which follows OAuth best practices. Token expiration is correctly calculated.
594-657: Code exchange correctly implements PKCE verification.The
exchangeCodeForTokensfunction properly validates:
- All required parameters
- Client ID match
- Redirect URI match
- Code expiration
- PKCE code_verifier (lines 638-646)
The authorization code is deleted after use (line 648), preventing replay attacks.
659-721: Refresh token implementation follows OAuth security best practices.The
refreshAccessTokenfunction:
- Validates the refresh token exists and hasn't expired
- Enforces that requested scopes don't exceed original scopes (lines 700-707)
- Rotates both access and refresh tokens (lines 709-712)
Token rotation is an excellent security practice that limits the window of exposure if tokens are compromised.
723-762: Token revocation performs cascading cleanup.The
revokeTokenfunction correctly revokes both the specified token and its associated counterpart (access↔refresh). The logic handles both with and withouttoken_type_hint.
764-798: Access token validation includes automatic cleanup.The
validateAccessTokenfunction not only validates tokens but also removes expired tokens (lines 775-782) and their associated refresh tokens. This helps maintain the integrity of the token stores.
| [ | ||
| { | ||
| "clientId": "chatgpt-connector", | ||
| "clientName": "ChatGPT Connector", | ||
| "redirectUris": [ | ||
| "https://chat.openai.com/connector_platform_oauth_redirect", | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762366322 | ||
| }, | ||
| { | ||
| "clientId": "client_7ae73df1dc3b6c45c16f7474ae45f5b7", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762368173 | ||
| }, | ||
| { | ||
| "clientId": "client_f1d608f62d925684e17454aa9b855b0a", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762371898 | ||
| }, | ||
| { | ||
| "clientId": "client_d695fe5e3b2f5e14e3165fd9f1aac875", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762373359 | ||
| }, | ||
| { | ||
| "clientId": "client_b8afe8a79036fc7b058d4715579e6e26", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762374157 | ||
| }, | ||
| { | ||
| "clientId": "client_a839b67a1631e816be9f52b84798539e", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762374563 | ||
| }, | ||
| { | ||
| "clientId": "client_c25d481d92586585b2a41e029e26349c", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762375179 | ||
| }, | ||
| { | ||
| "clientId": "client_0f1c3a7191fe9f56da0820278299287c", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762375851 | ||
| }, | ||
| { | ||
| "clientId": "client_bd88568d7414d92aae22e0a7174d9216", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762376136 | ||
| }, | ||
| { | ||
| "clientId": "client_3944089896aeeb49d4e95563c1d9d9d2", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762376339 | ||
| }, | ||
| { | ||
| "clientId": "client_4ba2d740e920151e7634527d5a0be31b", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762376895 | ||
| }, | ||
| { | ||
| "clientId": "client_e93485252120d80e010a2db0a62acbba", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762377021 | ||
| }, | ||
| { | ||
| "clientId": "client_2cf5165e4b09539698fb29c19d16954f", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762378402 | ||
| }, | ||
| { | ||
| "clientId": "client_8418817ad2d0e160e014e3ef4b79b48f", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762378595 | ||
| }, | ||
| { | ||
| "clientId": "client_6618670518a1c0446a6704cb7b096056", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762378926 | ||
| }, | ||
| { | ||
| "clientId": "client_5deb8f2616e5fc58505329c83e4fd0ea", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762379334 | ||
| }, | ||
| { | ||
| "clientId": "client_13c8dd04c7f8748d764496f3cbb0fe62", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762380479 | ||
| }, | ||
| { | ||
| "clientId": "client_b3d561390cf288ffe1292d56b7c1a604", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762381579 | ||
| }, | ||
| { | ||
| "clientId": "client_3fc83893f55bf1ec48f6ef826bde9697", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762381668 | ||
| }, | ||
| { | ||
| "clientId": "client_8bf74b117c447d71f3abfb58e7bb669b", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762436210 | ||
| }, | ||
| { | ||
| "clientId": "client_b8188dae1f189d1cde38161c1e20b9a3", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762436960 | ||
| }, | ||
| { | ||
| "clientId": "client_bdd3888088a9ea6df81cf4a04f6aad03", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762438379 | ||
| }, | ||
| { | ||
| "clientId": "client_3f8908334f6d05839eb23e2ea59827c4", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762438534 | ||
| }, | ||
| { | ||
| "clientId": "client_9a32c28ce8ed227c2b02c9f6b61a044f", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762438766 | ||
| }, | ||
| { | ||
| "clientId": "client_7799e6aa40caca6ebfeb1dfc09a7543d", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762439872 | ||
| }, | ||
| { | ||
| "clientId": "client_64ad7bfbcb6f516b936129aafaee074d", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762439925 | ||
| }, | ||
| { | ||
| "clientId": "client_f23c056ba72dfe7f5c83994ff4db4470", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762445838 | ||
| }, | ||
| { | ||
| "clientId": "client_81d66fc5bc3b9515b2698597da7c4625", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762446028 | ||
| }, | ||
| { | ||
| "clientId": "client_c7a3851ca93a29f53001a8a681df19b0", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762446290 | ||
| }, | ||
| { | ||
| "clientId": "client_1d5763b429781338712e288d4ebc1e66", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762446443 | ||
| }, | ||
| { | ||
| "clientId": "client_f0b4e9925e49899fd4a20a74f3c25e88", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762446572 | ||
| }, | ||
| { | ||
| "clientId": "client_302f4b55ffc4fb11cac8c1f3b76ce2c7", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762446836 | ||
| }, | ||
| { | ||
| "clientId": "client_24cb3840bb888d2299de3b1a5df9e823", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762447102 | ||
| }, | ||
| { | ||
| "clientId": "client_91628f04a116bbd6ed14f6a034da7e56", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762447301 | ||
| }, | ||
| { | ||
| "clientId": "client_7021dd306e9b5f2ac73f0fe42c849f44", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762447468 | ||
| }, | ||
| { | ||
| "clientId": "client_f70f9ee7486a7e5c4918b9fe17766db6", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762447965 | ||
| }, | ||
| { | ||
| "clientId": "client_4143c4458171da5c0d1ea4770b46f7ae", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762448997 | ||
| }, | ||
| { | ||
| "clientId": "client_e3d4d39f8f7218c5e1779d0edba6c27b", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762449742 | ||
| }, | ||
| { | ||
| "clientId": "client_bdb4f507c19f68178c48025d9c73007a", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762449892 | ||
| }, | ||
| { | ||
| "clientId": "client_c9060bd35169c05b7382156432f49a9f", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762450061 | ||
| }, | ||
| { | ||
| "clientId": "client_2331ef54994da91734d6b6be0236ecc7", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762450222 | ||
| }, | ||
| { | ||
| "clientId": "client_9fda83a1bb93272825b0a09d3a39f256", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762451100 | ||
| }, | ||
| { | ||
| "clientId": "client_11d7945ffeea2dd5abda16ca11423596", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762451393 | ||
| }, | ||
| { | ||
| "clientId": "client_e2810eb1470b6b185f87af4129b21349", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762452060 | ||
| }, | ||
| { | ||
| "clientId": "client_a3f3071071f465c94775af0b11342b2f", | ||
| "clientName": "ChatGPT", | ||
| "redirectUris": [ | ||
| "https://chatgpt.com/connector_platform_oauth_redirect" | ||
| ], | ||
| "scopes": [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access" | ||
| ], | ||
| "clientIdIssuedAt": 1762453493 | ||
| } | ||
| ] No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excessive duplicate client entries and hardcoded OAuth configuration.
This file contains 46 OAuth client entries, where 45 of them are nearly identical (all named "ChatGPT" with the same redirect URIs and scopes). This appears to be accumulated test/development data.
Concerns:
- Production readiness: Having dozens of duplicate client configurations suggests this is test data that shouldn't be committed.
- Security: Hardcoding OAuth client configurations in version control makes it difficult to rotate clients or manage them dynamically.
- Maintainability: Static JSON files are difficult to manage for OAuth clients, which typically need dynamic registration and rotation.
Consider:
- Reducing this to only the essential production client(s) needed (likely just "chatgpt-connector").
- Moving OAuth client management to a database or external configuration service.
- Implementing dynamic client registration rather than hardcoded configurations.
- Adding a
.gitignoreentry for this file if it will contain environment-specific clients.
Example minimal configuration:
[
{
"clientId": "chatgpt-connector",
"clientName": "ChatGPT Connector",
"redirectUris": [
"https://chat.openai.com/connector_platform_oauth_redirect",
"https://chatgpt.com/connector_platform_oauth_redirect"
],
"scopes": [
"openid",
"profile",
"email",
"offline_access"
],
"clientIdIssuedAt": 1762366322
}
]🤖 Prompt for AI Agents
In .data/oauth-clients.json spanning lines 1 to 633, the file contains 46 OAuth
client entries where 45 are nearly identical duplicate "ChatGPT" clients with
identical configurations, representing accumulated test data that should not be
committed to version control. Remove all duplicate ChatGPT client entries (from
the second entry onwards) and keep only the "chatgpt-connector" entry which is
the essential production client needed. This reduces the file to a minimal
configuration containing only the legitimate production OAuth client.
| "WWW-Authenticate", | ||
| `Bearer realm="index-mcp", error="${ | ||
| validation.error === "expired" ? "invalid_token" : "invalid_grant" | ||
| }", error_description="${validation.message}"` | ||
| ); | ||
| return res.status(401).json({ error: validation.message }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the correct WWW-Authenticate error code.
For resource servers, RFC 6750 requires error="invalid_token" when the bearer token is unknown or expired. Returning invalid_grant here can confuse standards-compliant clients. Please normalize the error code to invalid_token.
- res.setHeader(
- "WWW-Authenticate",
- `Bearer realm="index-mcp", error="${
- validation.error === "expired" ? "invalid_token" : "invalid_grant"
- }", error_description="${validation.message}"`
- );
+ res.setHeader(
+ "WWW-Authenticate",
+ `Bearer realm="index-mcp", error="invalid_token", error_description="${validation.message}"`
+ );Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/auth.ts around lines 57 to 63, the WWW-Authenticate header currently sets
error to "invalid_grant" for non-expired failures; RFC 6750 requires resource
servers to use error="invalid_token" for unknown or expired bearer tokens.
Change the header construction so the error value is always "invalid_token"
(remove the conditional), keep the rest of the header the same (realm and
error_description), and return the 401 response as before.
| const dataDir = join(process.cwd(), ".data"); | ||
| const clientsFile = join(dataDir, "oauth-clients.json"); | ||
|
|
||
| const issuer = | ||
| process.env.OAUTH_ISSUER_URL || | ||
| process.env.MCP_SERVER_URL || | ||
| "http://localhost:3002"; | ||
| const resourceIndicator = | ||
| process.env.OAUTH_RESOURCE_INDICATOR || `${issuer.replace(/\/$/, "")}/mcp`; | ||
| const canonicalResourceIndicator = resourceIndicator.replace(/\/$/, ""); | ||
| const accessTokenTtlSeconds = Number( | ||
| process.env.OAUTH_ACCESS_TOKEN_TTL_SECONDS ?? "3600" | ||
| ); | ||
| const refreshTokenTtlSeconds = Number( | ||
| process.env.OAUTH_REFRESH_TOKEN_TTL_SECONDS ?? "1209600" | ||
| ); | ||
| const authorizationCodeTtlSeconds = Number( | ||
| process.env.OAUTH_CODE_TTL_SECONDS ?? "300" | ||
| ); | ||
| const supportedScopes = | ||
| process.env.OAUTH_SUPPORTED_SCOPES?.split(/\s+/).filter(Boolean) ?? [ | ||
| "openid", | ||
| "profile", | ||
| "email", | ||
| "offline_access", | ||
| ]; | ||
| const defaultScopes = | ||
| process.env.OAUTH_DEFAULT_SCOPES?.split(/\s+/).filter(Boolean) ?? | ||
| supportedScopes; | ||
|
|
||
| const staticClientIds = | ||
| process.env.OAUTH_ALLOWED_CLIENT_IDS?.split(",").map((id) => id.trim()) ?? [ | ||
| "chatgpt-connector", | ||
| ]; | ||
|
|
||
| const staticRedirectUris = | ||
| process.env.OAUTH_ALLOWED_REDIRECT_URIS?.split(",") | ||
| .map((value) => value.trim()) | ||
| .filter(Boolean) ?? [ | ||
| "https://chat.openai.com/connector_platform_oauth_redirect", | ||
| "https://chatgpt.com/connector_platform_oauth_redirect", | ||
| ]; | ||
|
|
||
| const clients = new Map<string, OAuthClient>(); | ||
| const authorizationRequests = new Map<string, AuthorizationRequestRecord>(); | ||
| const authorizationCodes = new Map<string, AuthorizationCodeRecord>(); | ||
| const accessTokens = new Map<string, AccessTokenRecord>(); | ||
| const refreshTokens = new Map<string, RefreshTokenRecord>(); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Configuration lacks validation and has hardcoded fallbacks.
The configuration reads from environment variables with fallback defaults, but several issues exist:
-
Hardcoded client ID and redirect URIs (lines 127-138): The static client "chatgpt-connector" with specific redirect URIs is hardcoded. This reduces flexibility and makes testing harder.
-
No validation of critical config: TTL values are parsed as numbers without validation (lines 107-115). Invalid values could cause runtime errors or security issues.
-
Issuer URL normalization inconsistent: Some places use
issuer.replace(/\/$/, "")while the issuer itself may or may not have a trailing slash.
Add configuration validation at startup:
// After line 125
if (accessTokenTtlSeconds <= 0 || refreshTokenTtlSeconds <= 0 || authorizationCodeTtlSeconds <= 0) {
throw new Error("OAuth TTL values must be positive integers");
}
if (!issuer || !resourceIndicator) {
throw new Error("OAUTH_ISSUER_URL and OAUTH_RESOURCE_INDICATOR must be configured");
}
// Normalize issuer consistently
const issuer = (process.env.OAUTH_ISSUER_URL || process.env.MCP_SERVER_URL || "http://localhost:3002").replace(/\/$/, "");| function bootstrapClients() { | ||
| if (!existsSync(dataDir)) { | ||
| mkdirSync(dataDir, { recursive: true }); | ||
| } | ||
|
|
||
| if (existsSync(clientsFile)) { | ||
| try { | ||
| const parsed: OAuthClient[] = JSON.parse( | ||
| readFileSync(clientsFile, "utf-8") | ||
| ); | ||
| for (const client of parsed) { | ||
| clients.set(client.clientId, client); | ||
| } | ||
| } catch (error) { | ||
| console.warn("Failed to read persisted OAuth clients:", error); | ||
| } | ||
| } | ||
|
|
||
| for (const id of staticClientIds) { | ||
| if (!clients.has(id)) { | ||
| clients.set(id, { | ||
| clientId: id, | ||
| clientName: "ChatGPT Connector", | ||
| redirectUris: [...staticRedirectUris], | ||
| scopes: [...defaultScopes], | ||
| clientIdIssuedAt: Math.floor(Date.now() / 1000), | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| persistClients(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Client bootstrap creates pre-registered clients without versioning.
The bootstrapClients function reads persisted clients and ensures static clients exist. However, if the static client configuration changes (e.g., redirect URIs are updated), existing persisted clients won't be updated.
Consider adding a version check or update mechanism:
for (const id of staticClientIds) {
if (!clients.has(id)) {
clients.set(id, {
clientId: id,
clientName: "ChatGPT Connector",
redirectUris: [...staticRedirectUris],
scopes: [...defaultScopes],
clientIdIssuedAt: Math.floor(Date.now() / 1000),
});
+ } else {
+ // Update redirect URIs for static clients to match current config
+ const existing = clients.get(id)!;
+ existing.redirectUris = [...staticRedirectUris];
+ existing.scopes = [...defaultScopes];
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function bootstrapClients() { | |
| if (!existsSync(dataDir)) { | |
| mkdirSync(dataDir, { recursive: true }); | |
| } | |
| if (existsSync(clientsFile)) { | |
| try { | |
| const parsed: OAuthClient[] = JSON.parse( | |
| readFileSync(clientsFile, "utf-8") | |
| ); | |
| for (const client of parsed) { | |
| clients.set(client.clientId, client); | |
| } | |
| } catch (error) { | |
| console.warn("Failed to read persisted OAuth clients:", error); | |
| } | |
| } | |
| for (const id of staticClientIds) { | |
| if (!clients.has(id)) { | |
| clients.set(id, { | |
| clientId: id, | |
| clientName: "ChatGPT Connector", | |
| redirectUris: [...staticRedirectUris], | |
| scopes: [...defaultScopes], | |
| clientIdIssuedAt: Math.floor(Date.now() / 1000), | |
| }); | |
| } | |
| } | |
| persistClients(); | |
| } | |
| function bootstrapClients() { | |
| if (!existsSync(dataDir)) { | |
| mkdirSync(dataDir, { recursive: true }); | |
| } | |
| if (existsSync(clientsFile)) { | |
| try { | |
| const parsed: OAuthClient[] = JSON.parse( | |
| readFileSync(clientsFile, "utf-8") | |
| ); | |
| for (const client of parsed) { | |
| clients.set(client.clientId, client); | |
| } | |
| } catch (error) { | |
| console.warn("Failed to read persisted OAuth clients:", error); | |
| } | |
| } | |
| for (const id of staticClientIds) { | |
| if (!clients.has(id)) { | |
| clients.set(id, { | |
| clientId: id, | |
| clientName: "ChatGPT Connector", | |
| redirectUris: [...staticRedirectUris], | |
| scopes: [...defaultScopes], | |
| clientIdIssuedAt: Math.floor(Date.now() / 1000), | |
| }); | |
| } else { | |
| // Update redirect URIs for static clients to match current config | |
| const existing = clients.get(id)!; | |
| existing.redirectUris = [...staticRedirectUris]; | |
| existing.scopes = [...defaultScopes]; | |
| } | |
| } | |
| persistClients(); | |
| } |
| export function serializeAuthorizationContext(context: AuthorizationPageContext) { | ||
| return JSON.stringify(context).replace(/</g, "\u003c"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
XSS prevention in context serialization.
The serializeAuthorizationContext function escapes < characters to prevent XSS when embedding JSON in HTML. However, this only handles one case.
For complete XSS prevention when embedding in HTML, escape additional characters:
export function serializeAuthorizationContext(context: AuthorizationPageContext) {
- return JSON.stringify(context).replace(/</g, "\u003c");
+ return JSON.stringify(context)
+ .replace(/</g, "\u003c")
+ .replace(/>/g, "\u003e")
+ .replace(/&/g, "\u0026");
}This prevents various HTML and script injection patterns when the JSON is embedded in script tags or HTML attributes.
🤖 Prompt for AI Agents
In src/oauth.ts around lines 844 to 846, the serializeAuthorizationContext only
escapes "<" which is insufficient for safe HTML embedding; update the function
to escape additional characters before returning the stringified context — at
minimum replace &, >, <, /, single quote (') and the Unicode line/separator
characters U+2028 and U+2029 with their corresponding \\uXXXX escapes (e.g.
\\u0026, \\u003e, \\u003c, \\u002f, \\u0027, \\u2028, \\u2029) or use a small
helper that maps and replaces these characters to their unicode-escaped forms so
the JSON can be safely embedded in script tags or attributes.
| function getRequestOrigin(req: Request) { | ||
| const forwardedProto = req.header("x-forwarded-proto"); | ||
| const proto = forwardedProto?.split(",")[0]?.trim() || req.protocol || "http"; | ||
| const forwardedHost = req.header("x-forwarded-host"); | ||
| const host = forwardedHost ?? req.header("host"); | ||
| if (!host) { | ||
| return `${normalizedBaseUrl}`; | ||
| } | ||
| return `${proto}://${host}`; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Origin detection relies on proxy headers without validation.
The getRequestOrigin function trusts x-forwarded-proto and x-forwarded-host headers when present. Since trust proxy is set to true (line 189), these headers are accepted from any source, creating a potential security risk if attackers can inject these headers.
Consider validating the proxy headers or restricting which proxies are trusted. If an attacker controls these headers, they could manipulate the origin used in OAuth redirects, authorization endpoints, and MCP manifests. As mentioned in the earlier comment on line 189, configure trust proxy to trust only specific proxies in production.
🤖 Prompt for AI Agents
In src/server.ts around lines 240 to 249, the getRequestOrigin function blindly
trusts the x-forwarded-proto and x-forwarded-host headers without validating
they come from authorized sources, creating a security risk for OAuth redirects
and authorization endpoints. Add validation to check the request's origin IP
address against a whitelist of trusted proxies before accepting these headers.
If the request does not come from a trusted proxy IP, fall back to using the
request's own protocol and host properties instead of the forwarded headers.
| "touch-server": "touch ../src/server.ts" | ||
| }, | ||
| "dependencies": { | ||
| "@privy-io/js-sdk-core": "^0.56.1", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Version mismatch with root package.json.
The widgets package specifies @privy-io/js-sdk-core version ^0.56.1, while the root package.json (line 22) specifies ^0.57.0. This version mismatch may cause type incompatibilities or runtime issues if the packages evolve differently.
Apply this diff to align versions:
- "@privy-io/js-sdk-core": "^0.56.1",
+ "@privy-io/js-sdk-core": "^0.57.0",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "@privy-io/js-sdk-core": "^0.56.1", | |
| "@privy-io/js-sdk-core": "^0.57.0", |
🤖 Prompt for AI Agents
In widgets/package.json around line 13, the @privy-io/js-sdk-core dependency is
set to "^0.56.1" which mismatches the root package.json specifying "^0.57.0";
update the widgets package dependency to the same version range as the root
(change to "^0.57.0") so both manifests align and avoid type/runtime
incompatibilities, then run a fresh install (npm/yarn) and verify lockfile
consistency.
| const trimmed = email.trim(); | ||
| if (!trimmed) { | ||
| setStatusMessage('Enter a valid email address.', 'error'); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| setPending(true); | ||
| setStatusMessage('Sending verification code…'); | ||
| await privyClient.auth.email.sendCode(trimmed); | ||
| setStage('code'); | ||
| setStatusMessage('We sent a 6-digit code to your email.', 'success'); | ||
| } catch (error: any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trim and persist the email before verifying.
handleSendCode trims the input before sending it to Privy but leaves the original (possibly whitespace-padded) value in state. Later, loginWithCode reuses the untrimmed email, so a trailing space causes Privy to reject the verification. Please sanitize the email you store and reuse the trimmed value on verification.
- const trimmed = email.trim();
- if (!trimmed) {
+ const trimmedEmail = email.trim();
+ if (!trimmedEmail) {
setStatusMessage('Enter a valid email address.', 'error');
return;
}
try {
setPending(true);
setStatusMessage('Sending verification code…');
- await privyClient.auth.email.sendCode(trimmed);
+ await privyClient.auth.email.sendCode(trimmedEmail);
+ setEmail(trimmedEmail);
@@
- const session = await privyClient.auth.email.loginWithCode(email, trimmedCode);
+ const sanitizedEmail = email.trim();
+ const session = await privyClient.auth.email.loginWithCode(sanitizedEmail, trimmedCode);Also applies to: 110-114
🤖 Prompt for AI Agents
In widgets/src/oauth-consent/App.tsx around lines 71 to 83 (and similarly at
lines 110 to 114), the handler trims the email for the API call but doesn't
persist the trimmed value in component state, so later handlers use the original
untrimmed email and cause Privy to reject it; update the flow to store the
sanitized trimmed email in state before calling privyClient (e.g., call
setEmail(trimmed) or otherwise assign the trimmed value to the state variable
used by loginWithCode) and ensure any other send/verify paths also use the
trimmed/stored value.
| console.log('Privy session', session); | ||
|
|
||
| const appAccessToken = (session as any)?.token; | ||
| const privyAccessToken = (session as any)?.privy_access_token; | ||
| console.log('Selected Privy token source', { | ||
| hasAppAccessToken: Boolean(appAccessToken), | ||
| hasPrivyAccessToken: Boolean(privyAccessToken), | ||
| hasAccessTokenField: Boolean(session?.accessToken?.token) | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove sensitive token logging.
console.log statements here dump session objects and token presence flags to the browser console. That exposes bearer tokens to anyone with console access and violates basic token-handling hygiene. Please drop these logs (or guard them behind an explicit debug flag that never runs in production).
- console.log('Privy session', session);
-
const appAccessToken = (session as any)?.token;
const privyAccessToken = (session as any)?.privy_access_token;
- console.log('Selected Privy token source', {
- hasAppAccessToken: Boolean(appAccessToken),
- hasPrivyAccessToken: Boolean(privyAccessToken),
- hasAccessTokenField: Boolean(session?.accessToken?.token)
- });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.log('Privy session', session); | |
| const appAccessToken = (session as any)?.token; | |
| const privyAccessToken = (session as any)?.privy_access_token; | |
| console.log('Selected Privy token source', { | |
| hasAppAccessToken: Boolean(appAccessToken), | |
| hasPrivyAccessToken: Boolean(privyAccessToken), | |
| hasAccessTokenField: Boolean(session?.accessToken?.token) | |
| }); | |
| const appAccessToken = (session as any)?.token; | |
| const privyAccessToken = (session as any)?.privy_access_token; |
🤖 Prompt for AI Agents
In widgets/src/oauth-consent/App.tsx around lines 114 to 122, remove the
console.log statements that print the full session object and token presence
flags (they expose bearer tokens to the browser console); either delete those
logs entirely or replace them with a guarded debug-only logger that checks a
dedicated debug flag (e.g., process.env.REACT_APP_DEBUG === 'true') before
logging, and ensure the default production build never enables this flag.
| entryFileNames: (chunk) => { | ||
| const name = chunk.name ?? ''; | ||
| const folder = name.startsWith('oauth/') ? 'oauth' : 'widgets'; | ||
| return `${folder}/[name]-[hash:8].js`; | ||
| }, | ||
| chunkFileNames: (chunk) => { | ||
| const name = chunk.name ?? ''; | ||
| const folder = name.startsWith('oauth/') ? 'oauth' : 'widgets'; | ||
| return `${folder}/[name]-[hash:8].js`; | ||
| }, | ||
| assetFileNames: (assetInfo) => { | ||
| const name = assetInfo.name ?? ''; | ||
| const folder = name.includes('oauth') ? 'oauth' : 'widgets'; | ||
| return `${folder}/[name]-[hash:8][extname]`; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent path detection logic for assets.
The file naming functions use inconsistent logic to determine which folder assets should be placed in:
entryFileNamesandchunkFileNamesusename.startsWith('oauth/')(lines 21, 26)assetFileNamesusesname.includes('oauth')(line 31)
This inconsistency could cause assets to be misplaced. For example, a file like widgets/oauth-helper.css would be placed in the oauth folder due to the includes check, even though it belongs to the widgets entry.
Apply this diff to use consistent logic:
assetFileNames: (assetInfo) => {
const name = assetInfo.name ?? '';
- const folder = name.includes('oauth') ? 'oauth' : 'widgets';
+ const folder = name.startsWith('oauth/') || name.includes('src/oauth-consent/') ? 'oauth' : 'widgets';
return `${folder}/[name]-[hash:8][extname]`;
}Alternatively, if asset placement should be based on the entry point that generated them, consider using Rollup's asset source information to determine the correct folder more reliably.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| entryFileNames: (chunk) => { | |
| const name = chunk.name ?? ''; | |
| const folder = name.startsWith('oauth/') ? 'oauth' : 'widgets'; | |
| return `${folder}/[name]-[hash:8].js`; | |
| }, | |
| chunkFileNames: (chunk) => { | |
| const name = chunk.name ?? ''; | |
| const folder = name.startsWith('oauth/') ? 'oauth' : 'widgets'; | |
| return `${folder}/[name]-[hash:8].js`; | |
| }, | |
| assetFileNames: (assetInfo) => { | |
| const name = assetInfo.name ?? ''; | |
| const folder = name.includes('oauth') ? 'oauth' : 'widgets'; | |
| return `${folder}/[name]-[hash:8][extname]`; | |
| } | |
| entryFileNames: (chunk) => { | |
| const name = chunk.name ?? ''; | |
| const folder = name.startsWith('oauth/') ? 'oauth' : 'widgets'; | |
| return `${folder}/[name]-[hash:8].js`; | |
| }, | |
| chunkFileNames: (chunk) => { | |
| const name = chunk.name ?? ''; | |
| const folder = name.startsWith('oauth/') ? 'oauth' : 'widgets'; | |
| return `${folder}/[name]-[hash:8].js`; | |
| }, | |
| assetFileNames: (assetInfo) => { | |
| const name = assetInfo.name ?? ''; | |
| const folder = name.startsWith('oauth/') || name.includes('src/oauth-consent/') ? 'oauth' : 'widgets'; | |
| return `${folder}/[name]-[hash:8][extname]`; | |
| } |
Summary by CodeRabbit
Release Notes
New Features
Documentation
Chores