Repository navigation
Add mcp-oauth-console: OAuth sign-in for MCP servers - #77
Microbiosis wants to merge 4 commits into
Conversation
0fb4272 to
f543139
Compare
MyPrototypeWhat
left a comment
There was a problem hiding this comment.
Reviewed at f543139.
Thanks for this. The dual status model, device-code preference when the Host owns the loopback port, keeping tokens off the page, and the surgical Authorization patch are all solid. npm run check reports the package OK on this head, and GitHub Actions is green. A few things need to change before merge.
Must fix
-
miniapp/node/errors.mjsis imported but not in the package. Both the entry and the domain service import it (server.mjs:16→describeServiceError/isKnownServiceError;oauth-service.mjs:21→serviceError), and the client comments point at it too (app.js:36). The file is not among the 15 paths in this PR (confirmed withgit ls-treeonf543139and a directimport()ofoauth-service.mjs, which fails withCannot find module '.../errors.mjs').scripts/validate.mjsonly checks manifests, so it still prints OK. As shipped,start()cannot load. Please add the missing module (catalog of codes → HTTP status + user-facing message, plusserviceError/isKnownServiceError), or fold that catalog into an existing Node file and drop the dead imports. -
Credential paths must not follow
..in the server name.loadCredential/saveCredentialbuild the path asjoin(credentialDir, \${serverName}.json`)(oauth-service.mjs:570,:593).path.joinresolves.., so a name like../..//xlands outside this plugin'scontext.dataDir(and can touch another plugin's directory under the same parent). The authorize / device / apply routes takeserverNamefrom the request body (server.mjs:212,:227,:262), including names that are not yet in the Host MCP config, so this is reachable from the page's "add server" flow. Please restrict names the same way other packages do (for example^[A-Za-z0-9._-]+$, reject./.., and/or resolve the path and refuse anything that is not still undercredentialDir`). -
Host MCP config write: accepted, with conditions. We accept writing the Host MCP config. The README discloses which file is edited and how, and the backup / atomic / serialized write approach matches repo
docs/security.md("Writing outside dataDir"). Before merge, please meet these conditions:a. Only write the file the user confirmed, and
realpathit. Discovery currently triesMINIMAX_MCP_CONFIG, parents ofcontext.dataDir,cwd, the Node executable's directory, then the default Host location (resolveMcpConfigPath,oauth-service.mjs:154-176). The env var and default-location fallback are not a published contract, and paths are notrealpath'd, so an unrelatedmcp.jsonundercwdor next to the executable can win. Write only the resolved default file or one the user confirms in the UI,realpath'd before read and write.b. Hold the lock across the whole read-modify-write. The read / modify / verify in
patchMcpAuthorizationaround:987is not fully under the write lock, so another process (or a second apply) editing this file at the same time can lose entries. Take the lock before reading, re-read inside it, and release only after the write is verified.c. Background refresh must be opt-in or confirmed each time. Every 60s,
refreshExpiring(server.mjs:300-309) can write credentials and patch the Host MCP config with no second confirm; the firstapplyasks the user, the timer does not. Make background refresh an explicit opt-in, or ask for confirmation on each write.d. Write credential files with mode
0o600. Tokens are stored in cleartext under the plugin's credential dir (save path aroundoauth-service.mjs:593). Set the mode explicitly on create and keep it on rewrite, so a looser umask cannot leave the grant world-readable. This applies on macOS/Linux; on Windows the mode is effectively ignored and nothing extra is needed.e. Back up
mcp.jsonbefore every write, including background refresh writes. The plugin already backs up before writing (backupConfigintocontext.dataDir,oauth-service.mjs:509-511, README:73-74); please keep that.refreshServer(:919) goes through the samepatchMcpAuthorizationpath, so refresh writes are backed up today too (:1004,:1045). Two gaps remain:backupConfig(:190-199) swallows errors and returnsfalse, and the write still goes ahead, so a failed backup is silent; and every write overwrites the singlemcp.json.bak, so a quiet refresh replaces the backup taken before the user's last confirmed apply. Please abort the write if the backup fails, and keep the pre-apply backup from being clobbered by refresh writes (for example a separate or timestamped backup). -
Outbound fetches must share one URL guard, including private networks.
assertTargetUrl(oauth-service.mjs:316-333) runs on the MCP endpoint at authorize / device start, but theresource_metadataURL fromWWW-Authenticate(:377) and the issuer /.well-known/ token / device / registration endpoints taken from that metadata are fetched without the same check. The allowlist also lets RFC1918 and IPv4-mapped forms through (10/8,192.168/16,172.16/12,[::ffff:127.0.0.1]). Warm-up and/api/discoverprobeserver.urlfrom the Host config withoutassertTargetUrleither (server.mjs:186,:319). A hostile MCP can point metadata at an internal address (SSRF). Please run every fetch target through one checker that rejects private / link-local / metadata addresses (and, if you ever follow redirects, re-check after each hop). -
Callback page renders the
errorparam as raw HTML.handleCallbacktakeserrorstraight from the callback URL and concatenates it into the result message (`授权被拒绝:${error}`,oauth-service.mjs:847);renderCallbackPagethen inserts that message unescaped into<p>${detail}</p>(server.mjs:416). Anyone who can get the user to open a crafted callback URL (or a hostile authorization server redirecting with a chosenerror) can inject markup and script into a page served from the plugin's origin. Please HTML-escapedetail(andtitle) before interpolating it (&,<,>,",').
Optional
-
PKCE complete path treats a missing
stateas success. IncompleteAuthorization(oauth-service.mjs:798),if (state && state !== session.state)only fails when a state is present and wrong; a pasted callback URL with nostatestill exchanges the code. Prefer requiring an exact match whenever the session stored one. -
JSON key lookup is first-match substring of the key spelling.
findMemberObjectSpan/findMemberStringSpan(oauth-service.mjs:111-142) useindexOf(JSON.stringify(key)), so a server name that appears as a key elsewhere in the file (or a nested"Authorization"outside the intended entry) can patch the wrong span — confirmed by running it. Scoping the search tomcpServersfirst, or parsing and re-serializing only that entry while accepting format churn, would be safer. -
Creating a brand-new server entry rewrites the whole file (
:990-1007), which conflicts with the README claim that other bytes stay unchanged. -
Apply confirm is page-only. The confirm step lives in the page (
app.js:838-881);POST /api/applyhas no confirm token. Anything that can call that route can write the Host MCP config without the dialog. -
Browser open on non-Windows / non-macOS.
openInBrowser(oauth-service.mjs:706-719) usesexplorer.exeoropen. On Linux neither is reliable; the page already shows the URL to copy, so a short README / UI note for "open failed — use the link" is enough. -
Manifest / catalog copy still describes only authorization-code + PKCE.
.minimax-plugin/plugin.json:6and the root README table row omit the device-code path that this head recommends. Aligning those one-liners with the package README would avoid underselling the preferred flow. -
GRANT_TTL_MSis defined but unused. Either wire it or drop it so the lifetime story matches the code. -
No plugin tests. There is no
tests/tree. Even a few unit cases aroundassertTargetUrl, safe server-name → credential path, and the Authorization patch (replace / insert headers / create entry) would have caught item 2 above and would help the next review.
|
Update: please disregard condition d ( |
f543139 to
2674da1
Compare
…tighten the guards Must fix - package miniapp/node/errors.mjs, which the entry imports but the package did not ship, so the Mini App could not start at all - keep every error code the service can raise in one catalog; an unregistered code now fails fast instead of reaching the user - restrict a server name to a safe set, so it can no longer walk out of the credential directory - locate mcp.json only in the Host data-dir chain and the documented default location; the working directory and the Node executable's directory are no longer searched, and the resolved path is realpath'ed and shown in the page - bind a write to the exact path the user confirmed - run the whole read-modify-write under one lock and re-read inside it - refuse a write when the backup failed, and keep the confirmed snapshot apart from the one a background refresh writes - make the automatic refresh an explicit switch, off by default - validate every outbound URL, including the IPv4-mapped, IPv4-compatible and NAT64 IPv6 forms; URLs a remote server handed back are always strict - escape the callback page, and require the PKCE state to match exactly - limit the JSON lookup to mcpServers, and insert a new entry textually Optional - bind a confirmed write to a one-time token - report a browser that could not be opened, instead of claiming it opened - drop the unused grant TTL - add unit tests for the URL guard, the credential path, the surgical patch and the error catalog
|
All five Must fix items and the eight Optional ones are addressed on Must fix
Package tests: Still |
Run the authorization-code + PKCE flow for MCP servers that require OAuth, keep the token inside the Node process, write the Authorization header into the MCP configuration, and refresh it before it expires. The page separates two facts that are easy to conflate: what the server demands, probed with a handshake that matches the declared transport and only read as OAuth when the server sends a Bearer resource_metadata challenge, and what this machine holds, the local credential. Credentials live under context.dataDir. The MCP configuration is patched surgically, written atomically, and backed up before every write.
From MiniMax Code 3.1.2 the Host authenticates every request to the assigned loopback port, so the browser redirect of an authorization-code flow answers 401 and the callback handler never runs. The device authorization grant (RFC 8628) has no redirect at all, so it is not affected; the page now offers it as the recommended method and keeps the PKCE method with the callback URL copied back by hand. Also rebase onto upstream main, document the Agent turns and the two sign-in methods, and tag the row action buttons with data-action so they can be targeted unambiguously.
…tighten the guards Must fix - package miniapp/node/errors.mjs, which the entry imports but the package did not ship, so the Mini App could not start at all - keep every error code the service can raise in one catalog; an unregistered code now fails fast instead of reaching the user - restrict a server name to a safe set, so it can no longer walk out of the credential directory - locate mcp.json only in the Host data-dir chain and the documented default location; the working directory and the Node executable's directory are no longer searched, and the resolved path is realpath'ed and shown in the page - bind a write to the exact path the user confirmed - run the whole read-modify-write under one lock and re-read inside it - refuse a write when the backup failed, and keep the confirmed snapshot apart from the one a background refresh writes - make the automatic refresh an explicit switch, off by default - validate every outbound URL, including the IPv4-mapped, IPv4-compatible and NAT64 IPv6 forms; URLs a remote server handed back are always strict - escape the callback page, and require the PKCE state to match exactly - limit the JSON lookup to mcpServers, and insert a new entry textually Optional - bind a confirmed write to a one-time token - report a browser that could not be opened, instead of claiming it opened - drop the unused grant TTL - add unit tests for the URL guard, the credential path, the surgical patch and the error catalog
- follow redirects by hand and check every hop again with the same rule, so a 302 cannot walk past the URL guard - resolve IPv4-mapped, IPv4-compatible and NAT64 IPv6 addresses back to their IPv4 address before the private-range check - let the configured MCP endpoint use a private address when the user opted into it, in the authorize and device paths as well as the probe - check the authorization endpoint too, so the browser is never sent to an address a hostile server chose - map an unregistered error code to the generic one before it reaches the page, and route the remaining raw throws through the catalog - add tests for the redirect guard and for the error contract
86acdcb to
55be487
Compare
Adds
mcp-oauth-console: sign in to MCP servers that require OAuth.This branch also answers the review on #77 — the reply there lists the Must fix items.
What it does
Runs an OAuth sign-in for a server, keeps the token inside the Node process, writes the
Authorizationheader into the MiniMax Code MCP configuration, and can refresh the token before it expires.The page shows two independent facts per server: what the server demands (probed from the endpoint) and what this machine holds (the local credential). A server can require OAuth and still be satisfied by a local token, and one can need no OAuth and still have no token — a single status field hides one of the two.
Notes for the reviewer
401and the authorization code has to be copied back from the address bar.401alone is not OAuth. Only aWWW-Authenticate: Bearer resource_metadata="…"challenge (RFC 9728) counts, so the page distinguishes requires OAuth / requires a Bearer credential / requires a credential, scheme undeclared / not required / undetermined.initializecall forstreamable-http, aGETforsse.mcp.jsonis surgical, atomic, backed up, and bound to the path the user confirmed.Checks
npm run check:plugins/microbiosis/mcp-oauth-console: OK,repository: OK. CI green on55be487.Files read and written, network hosts, spawned processes, state, credentials, Agent turns and the tested environment are in the package README: README.md.
Package tests:
node --test "tests/**/*.test.mjs"from the package directory.