Skip to content

docs(auth): the OAuth authorize URL is per tier and carries ?network=sandbox|live - #394

Open
r-marques wants to merge 1 commit into
mainfrom
docs/oauth-authorize-tier-3430
Open

r-marques wants to merge 1 commit into
mainfrom
docs/oauth-authorize-tier-3430

Conversation

@r-marques

@r-marques r-marques commented Sep 11, 2026

Copy link
Copy Markdown
Member

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 unknown network param, 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's verification_uri likewise), 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 says client_id registration 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 shows verification_uri / verification_uri_complete with network (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) and docs.json (no nav change).

Verification

  • mintlify broken-linkssuccess no broken links found (run locally on this branch).
  • All placeholders are backticked (a bare <placeholder> aborts the whole link check).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cpykv9mTQL88E3oXwA11QF

…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 aaitor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 from for-agents.mdx, device-flow.mdx, oauth-authorization-code.mdx, and mcp.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.json touched, consistent with the PR's own claim that neither needs a change here.

No blockers found.

⚠️ Bot tooling blocker: same run-dir "sensitive file" permission gate as earlier today (#3412, #622, #624, #437, #276, #3427, #631, #634) — can't write 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.

@eruizgar91

Copy link
Copy Markdown
Member

👀 Reviewing this now.

@eruizgar91 eruizgar91 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.tstierQualifiedFrontendUrl(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.tsensureOAuthTierswitchEnvironmentAndReload, 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.mdx extends "keep the query string (RFC 6749 §3.1)" to the device verification_uri too. 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 to verification_uri at all (the server hands it verification_uri_complete), so the advice is harmless. Not worth a change.
  • The example RFC 8414 document in overview.mdx shows 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: parseOAuthTier treats 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.json untouched. 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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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:

"user_code": { "type": "string", "example": "BCDF-GHJK" },
"verification_uri": { "type": "string", "example": "https://nevermined.app/oauth/device" },
"verification_uri_complete": { "type": "string", "example": "https://nevermined.app/oauth/device?user_code=BCDF-GHJK" },
"expires_in": { "type": "integer", "example": 600 },

"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:

"description": "Advertises the authorization, token, and device_authorization endpoints, the supported grants (including urn:ietf:params:oauth:grant-type:device_code), and the JWKS URI. scopes_supported is deliberately absent (#2397).",
"responses": {
"200": { "description": "RFC 8414 metadata", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true, "example": { "issuer": "https://api.sandbox.nevermined.app", "authorization_endpoint": "https://api.sandbox.nevermined.app/oauth/authorize", "token_endpoint": "https://api.sandbox.nevermined.app/oauth/token", "device_authorization_endpoint": "https://api.sandbox.nevermined.app/oauth/device_authorization", "jwks_uri": "https://api.sandbox.nevermined.app/.well-known/jwks.json", "grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"], "code_challenge_methods_supported": ["S256"] } } } } }
}
}

"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.

Comment thread integrations/mcp.mdx
**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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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 eruizgar91 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟡 Approved with comments — no blockers; 2 should-fix, 0 nit(s) left inline.

Inline review: #394 (review)

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