Conversation
…sandbox|live Sandbox and Live are separate RFC 8414 issuers whose documents used to advertise the identical authorization endpoint. The consent web app boots on Live by default, so a sandbox-only connector opened in a fresh browser was refused as "Connector not authorized". The API now advertises `…/oauth/authorize?network=<tier>` (and the device flow's verification_uri likewise); document it where the authorize URL and discovery are described, and tell hand-configured providers to copy the endpoint verbatim. Pairs with nevermined-io/nvm-monorepo#3430. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cpykv9mTQL88E3oXwA11QF
aaitor
left a comment
There was a problem hiding this comment.
Reviewed the tier-qualified authorize URL docs (5 MDX files). Content checks out:
overview.mdx's new anchor (#each-tier-is-its-own-issuer-and-its-authorize-url-says-so) matches the header text and is referenced consistently fromfor-agents.mdx,device-flow.mdx,oauth-authorization-code.mdx, andmcp.mdx.- Device-flow example URLs (
?network=sandbox, then&user_code=...) use the correct single-?query shape. - RFC 6749 §3.1 citation (retain the existing query component when adding params) is accurate.
- Matches the already-merged/approved nvm-monorepo#3433 it documents — no contradictions found.
- No
docs.json/openapi.jsontouched, consistent with the PR's own claim that neither needs a change here.
No blockers found.
findings.json under ~/.claude/pr-reviews/runs/.../ in this session, so I can't complete the deterministic verdict step (act.py). Leaving this for a human approve or a later tick.
|
👀 Reviewing this now. |
eruizgar91
left a comment
There was a problem hiding this comment.
Reviewed the five MDX changes against nevermined-io/nvm-monorepo#3433 as merged on origin/main (2fc26dc5c), not against its PR description. The documented behaviour matches the shipped code; two findings are about surfaces this PR left behind, both posted inline.
Verified against the merged implementation
| Claim | Where it is enforced on origin/main |
Verdict |
|---|---|---|
authorization_endpoint = https://nevermined.app/oauth/authorize?network=<tier> |
well-known.service.ts → tierQualifiedFrontendUrl(frontendUrl, '/oauth/authorize', ENVIRONMENT) |
✅ exact |
token_endpoint = <apiHost>/oauth/token (example in overview.mdx) |
well-known.service.ts, token_endpoint: ${apiHost}/oauth/token |
✅ exact |
Device verification_uri / verification_uri_complete carry ?network=…, single ?, network first |
oauth-device.service.ts verificationUri() + searchParams.set('user_code', …) — pinned by oauth-device.service.spec.ts |
✅ exact, including the ?network=sandbox&user_code=… ordering in the example |
network is sandbox|live, "the same parameter the embeddable widgets take" |
OAUTH_TIER_PARAM / OAUTH_TIERS in @nevermined-io/commons — the comment names the embed widget's #1787 param explicitly |
✅ |
A link without network runs "on whichever tier the user's browser last used (Live by default)" |
parseOAuthTier returns undefined for an absent param; getInitialEnvironment() returns 'live' (#1603) |
✅ |
| The consent screen "reads it to open on the right tier" | lib/oauth-tier.ts → ensureOAuthTier → switchEnvironmentAndReload, wired into routes/oauth/{authorize,device,connect,checkout.$agentId}.tsx |
✅ |
| "refused as Connector not authorized" | the literal string is rendered by routes/oauth/connect.tsx:271 |
✅ |
RFC citation checked, not assumed. RFC 6749 §3.1 is the Authorization Endpoint section and does say the endpoint URI MAY carry a query component "which MUST be retained when adding additional query parameters" — so the §-number and the use are both correct. #3433's own apps/api/CLAUDE.md and lib/oauth-tier.ts cite the same section for the same reason.
Anchors and cross-links. All four new deep links point at #each-tier-is-its-own-issuer-and-its-authorize-url-says-so, which is the correct slug for ### Each tier is its own issuer, and its authorize URL says so (the comma drops). #discovery resolves to ## Discovery in the same file.
Considered and discarded
for-agents.mdxextends "keep the query string (RFC 6749 §3.1)" to the deviceverification_uritoo. Strictly, RFC 8628 §3.3.1 is the governing section there and it carries no such MUST — but a client is not supposed to append parameters toverification_uriat all (the server hands itverification_uri_complete), so the advice is harmless. Not worth a change.- The example RFC 8414 document in
overview.mdxshows only three fields. Deliberate excerpt, clearly framed as one; the full field list is the API reference's job. - Publishing ahead of the deploy. Checked the premise rather than taking it from the description:
parseOAuthTiertreats an absent param as "stay on the current tier", so the pre-#3433 webapp behaviour for a bare URL is unchanged, and nothing here breaks for a reader who adopts the documented URL before the release ships. docs.json/api-reference/openapi.jsonuntouched. Correct for both — but see the inline comment: the RFC 8414 example actually lives in a different file.
🤖 Reviewed with Claude Code
| "verification_uri": "https://nevermined.app/oauth/device", | ||
| "verification_uri_complete": "https://nevermined.app/oauth/device?user_code=BCDF-GHJK", | ||
| "verification_uri": "https://nevermined.app/oauth/device?network=sandbox", | ||
| "verification_uri_complete": "https://nevermined.app/oauth/device?network=sandbox&user_code=BCDF-GHJK", |
There was a problem hiding this comment.
MEDIUM — this new example is now contradicted by a published page: api-reference/oauth-openapi.json, which docs.json mounts as the Authentication API-reference group (docs.json → "source": "api-reference/oauth-openapi.json"), still documents the pre-#3433 shapes.
Three stale examples in that file:
docs/api-reference/oauth-openapi.json
Lines 272 to 275 in 7f41ed2
"verification_uri": "https://nevermined.app/oauth/device"
"verification_uri_complete": "https://nevermined.app/oauth/device?user_code=BCDF-GHJK"
and the RFC 8414 200 example:
docs/api-reference/oauth-openapi.json
Lines 166 to 170 in 7f41ed2
"authorization_endpoint": "https://api.sandbox.nevermined.app/oauth/authorize"
The device ones are exactly what this line now says the API returns (?network=sandbox first, then &user_code=), one page apart in the same nav. The authorization_endpoint one is worse and pre-dates this PR: it is wrong on two axes — WellKnownService builds it from nvm.neverminedAppUri (https://nevermined.app), not from the API host, and it has no ?network=. That is the exact string a hand-wiring integrator copies into a connector's "authorization URL" field, which is the failure mode nvm-monorepo#3430 exists to prevent — so leaving it is the one place these docs still actively teach the bug.
The PR description says "Not touched: api-reference/openapi.json (it does not document the RFC 8414 metadata schema, so there is nothing to add)". That is true of openapi.json — but the RFC 8414 metadata schema is documented, in oauth-openapi.json, a different file. Worth re-checking that conclusion against the file that actually carries it.
The file is hand-authored (added by docs#271; it is not in CLAUDE.md's generated-files list and no workflow syncs it), so this is three string edits in one adjacent file — it belongs in this PR rather than a follow-up.
| **You don't need to handle any of this manually** - `payments.mcp.start()` configures everything automatically! | ||
|
|
||
| <Note> | ||
| **The consent screen is per API tier.** The human approves at `https://nevermined.app/oauth/authorize`, and the Nevermined API advertises that endpoint **with the tier on it** — `?network=sandbox` from `api.sandbox.nevermined.app`, `?network=live` from `api.live.nevermined.app` — because the web app otherwise opens on whichever tier the user's browser last used (Live by default). If you wire an MCP client or an OAuth provider to Nevermined **by hand** instead of through discovery, use the tier-qualified endpoint from the tier's [`/.well-known/oauth-authorization-server`](/integrate/authentication/overview#each-tier-is-its-own-issuer-and-its-authorize-url-says-so), query string included. |
There was a problem hiding this comment.
MEDIUM — on this page the note is stronger than what ships, because of where it lands. Everything it says about the Nevermined API's document is accurate; but it sits directly under a numbered list whose step 1 is "Discovery: Client reads /.well-known/oauth-authorization-server" for your own MCP server, followed by "You don't need to handle any of this manually — payments.mcp.start() configures everything automatically!".
That discovery document is served by the SDK, not by the API, and it is still tier-blind. On nevermined-io/payments@main, src/mcp/http/oauth-metadata.ts:
authorizationUri: `${frontend}/oauth/authorize`, // no ?network=which buildAuthorizationServerMetadata emits as authorization_endpoint. payments-py does the same in payments_py/mcp/http/oauth_metadata.py. Both fixes are still open — nevermined-io/payments#447 and nevermined-io/payments-py#277 — and #3433's own apps/api/CLAUDE.md records exactly this caveat: "Until those land, an MCP server built with either SDK sends its users to a tier-blind consent URL; fixing this document fixed the API's issuer only."
So the note's only remediation — "If you wire an MCP client or an OAuth provider to Nevermined by hand instead of through discovery…" — points away from the one broken path on this page. A reader who follows the page as written uses payments.mcp.start(), relies on discovery, and still sends users to a tier-blind consent screen, i.e. straight into the "Connector not authorized" refusal the rest of this PR documents.
One sentence closes it, e.g. "MCP servers started with payments.mcp.start() still advertise a tier-blind authorization_endpoint in their own discovery document (payments#447, payments-py#277); until those ship, point hand-configured clients at the API tier's endpoint." Bounded, same file, no design decision — it should land here rather than as a follow-up, since the caveat is live today and this is the page it applies to.
eruizgar91
left a comment
There was a problem hiding this comment.
🟡 Approved with comments — no blockers; 2 should-fix, 0 nit(s) left inline.
Inline review: #394 (review)
Summary
Pairs with nevermined-io/nvm-monorepo#3433 (the API + webapp change for nevermined-io/nvm-monorepo#3430) — merged to main 2026-09-11 as
2fc26dc5c, ships in the next minor. Safe to publish ahead of that deploy: a discovery-driven client reads whatever the document says, and the current webapp ignores an unknownnetworkparam, so a hand-configured provider that adopts the documented URL early keeps working today and becomes correct on deploy (docs ADR 0003: document the requirement, not a named environment's rollout state).Sandbox and Live are separate RFC 8414 issuers, but both advertised the identical
https://nevermined.app/oauth/authorize, and the consent web app boots on Live by default — so a sandbox-only connector opened in a fresh browser profile was refused as "Connector not authorized" (measured with LangSmith Fleet on staging, 2026-09-11). The API now advertises…/oauth/authorize?network=sandbox|live(and the device flow'sverification_urilikewise), and the web app honours it.What changed
integrate/authentication/overview.mdx— new subsection under Discovery: Each tier is its own issuer, and its authorize URL says so — the example document, why the tier is on the URL, keep-the-query-string (RFC 6749 §3.1), and what to do when configuring a provider by hand.integrate/authentication/oauth-authorization-code.mdx— a prerequisite naming the tier-qualified authorize URL; the closing discovery note now saysclient_idregistration is per tier.integrate/authentication/for-agents.mdx— the discovery paragraph says the human-facing URLs carry?network=and must be passed on unchanged.integrate/authentication/device-flow.mdx— the example response now showsverification_uri/verification_uri_completewithnetwork(and the single-?shape); the hand-off step says to pass them on unchanged.integrations/mcp.mdx— a note that the consent screen is per tier and hand-wired clients must use the tier-qualified endpoint from discovery.Not touched:
api-reference/openapi.json(it does not document the RFC 8414 metadata schema, so there is nothing to add) anddocs.json(no nav change).Verification
mintlify broken-links→success no broken links found(run locally on this branch).<placeholder>aborts the whole link check).🤖 Generated with Claude Code
https://claude.ai/code/session_01Cpykv9mTQL88E3oXwA11QF