From 7f41ed21ba5289357be49dc7854f185aa3e34c77 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Fri, 11 Sep 2026 12:14:34 +0200 Subject: [PATCH 1/5] docs(auth): the OAuth authorize URL is per tier and carries ?network=sandbox|live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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=` (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) Claude-Session: https://claude.ai/code/session_01Cpykv9mTQL88E3oXwA11QF --- integrate/authentication/device-flow.mdx | 6 +++--- integrate/authentication/for-agents.mdx | 2 +- .../authentication/oauth-authorization-code.mdx | 3 ++- integrate/authentication/overview.mdx | 14 ++++++++++++++ integrations/mcp.mdx | 4 ++++ 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/integrate/authentication/device-flow.mdx b/integrate/authentication/device-flow.mdx index fd9bf523..446f53b8 100644 --- a/integrate/authentication/device-flow.mdx +++ b/integrate/authentication/device-flow.mdx @@ -37,14 +37,14 @@ This is the "claim ceremony" an agent can use on its own. Both the `device_autho { "device_code": "a1b2c3…", "user_code": "BCDF-GHJK", - "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", "expires_in": 600, "interval": 5 } ``` - `device_code` is your machine secret (opaque — poll with it). `user_code` is what the human types. `interval` is the minimum seconds between polls. + `device_code` is your machine secret (opaque — poll with it). `user_code` is what the human types. `interval` is the minimum seconds between polls. Both verification URIs carry the tier you called (`?network=sandbox|live`) — pass them on unchanged, so the human's browser opens the verification page on the tier your ceremony is pending on ([why](/integrate/authentication/overview#each-tier-is-its-own-issuer-and-its-authorize-url-says-so)). diff --git a/integrate/authentication/for-agents.mdx b/integrate/authentication/for-agents.mdx index c85568bb..ac13b578 100644 --- a/integrate/authentication/for-agents.mdx +++ b/integrate/authentication/for-agents.mdx @@ -30,7 +30,7 @@ Everything the ceremony needs is advertised in standards-based metadata (see [Di | Protected Resource Metadata | `/.well-known/oauth-protected-resource` | RFC 9728 | | JWKS (ES256K / secp256k1) | `/.well-known/jwks.json` | RFC 7517 (ES256K curve: RFC 8812) | -Resolve these against the environment you're targeting — `https://api.sandbox.nevermined.app` (test) or `https://api.live.nevermined.app` (real money). +Resolve these against the environment you're targeting — `https://api.sandbox.nevermined.app` (test) or `https://api.live.nevermined.app` (real money). The human-facing URLs each document advertises — the `authorization_endpoint`, and the `verification_uri` the device flow returns — carry that tier as `?network=sandbox|live`. Hand them to the human as-is and keep the query string when you add parameters of your own; the consent screen reads it to open on the right tier ([why](/integrate/authentication/overview#each-tier-is-its-own-issuer-and-its-authorize-url-says-so)). ## The short version diff --git a/integrate/authentication/oauth-authorization-code.mdx b/integrate/authentication/oauth-authorization-code.mdx index 510004b0..ba2bf4fc 100644 --- a/integrate/authentication/oauth-authorization-code.mdx +++ b/integrate/authentication/oauth-authorization-code.mdx @@ -20,6 +20,7 @@ Use the **OAuth 2.1 Authorization Code flow with PKCE** when a browser-based MCP - **An authenticated user.** `POST /oauth/authorize` runs in the signed-in user's context — the browser client calls it with the user's Nevermined API key. Without an authenticated user it fails with `BCK.OAUTH.0007`. - **A pre-registered `client_id`.** Same as the device flow — connectors are onboarded out of band (no dynamic client registration). An unregistered `client_id` is rejected with `BCK.OAUTH.0016`. - **PKCE is mandatory.** Generate a `code_verifier` and its `code_challenge` (`S256`). There is no non-PKCE path. +- **The right tier's authorize URL.** The browser step starts at the `authorization_endpoint` of the tier you are integrating with — `https://nevermined.app/oauth/authorize?network=sandbox` or `…?network=live` — as advertised by that tier's [RFC 8414 document](/integrate/authentication/overview#each-tier-is-its-own-issuer-and-its-authorize-url-says-so). Keep the `network` query parameter when you add `client_id`, `redirect_uri`, `code_challenge` and the rest; it is what makes the consent screen open on the tier your `client_id` is registered on. If you configure the provider by hand rather than via discovery, paste the endpoint with its query string. ## The flow @@ -91,5 +92,5 @@ Use the **OAuth 2.1 Authorization Code flow with PKCE** when a browser-based MCP The connection the user approves is recorded as an **AgentBinding** — the consent receipt. The `client_id` is the *connector* fronting the connection (Cursor, Claude, LangSmith Fleet), which is distinct from the *agent* the consent is for. Users can review and revoke these at any time from the [Connected agents](/integrate/authentication/connections) surface. -Both the `authorization_endpoint` and the `authorization_code` grant are advertised in the [RFC 8414 metadata](/integrate/authentication/overview#discovery). As with the device flow, discovery doesn't substitute for a pre-registered `client_id`. +Both the `authorization_endpoint` and the `authorization_code` grant are advertised in the [RFC 8414 metadata](/integrate/authentication/overview#discovery) — per tier, with the tier on the endpoint (`?network=sandbox|live`). As with the device flow, discovery doesn't substitute for a pre-registered `client_id`; and a `client_id` is registered per tier, so a connector known to Sandbox is "not authorized" on Live and vice versa. diff --git a/integrate/authentication/overview.mdx b/integrate/authentication/overview.mdx index 7288a767..2e9f7dba 100644 --- a/integrate/authentication/overview.mdx +++ b/integrate/authentication/overview.mdx @@ -82,6 +82,20 @@ Everything the ceremony needs is advertised in standards-based metadata. If a ca Because a transport now mints a credential this API accepts, discovery reflects it: the Protected Resource Metadata names this issuer in `authorization_servers`, and a genuine authentication `401` carries an RFC 6750 `WWW-Authenticate: Bearer resource_metadata="…"` challenge pointing at it. +### Each tier is its own issuer, and its authorize URL says so + +Sandbox and Live are separate authorization servers with separate discovery documents (`https://api.sandbox.nevermined.app/.well-known/oauth-authorization-server` and `https://api.live.nevermined.app/…`), but both send the human to the same web app for consent. That app cannot tell from a bare `/oauth/authorize` link which issuer it is acting for, so **the human-facing endpoints carry the tier as a query parameter**: + +```json +{ + "issuer": "https://api.sandbox.nevermined.app", + "authorization_endpoint": "https://nevermined.app/oauth/authorize?network=sandbox", + "token_endpoint": "https://api.sandbox.nevermined.app/oauth/token" +} +``` + +`network` is `sandbox` or `live` — the same parameter the embeddable widgets take. The device flow's `verification_uri` carries it too. **Keep the query string** when you append your own parameters (RFC 6749 §3.1 requires it; standard clients do this already). A discovery-driven client gets the right value for free; if you configure an OAuth provider **by hand** — a connector's "authorization URL" form field, say — copy the endpoint from the tier's discovery document verbatim, query string included. An authorize link without `network` is served on whichever tier the user's browser last used (Live by default), and a connector registered only on Sandbox is then refused as *"Connector not authorized"* until the user switches manually. + ### Reading a 401 A `401` is *usually* a missing, malformed, or invalid key — not a hint to go discover something. It isn't *always* an authentication problem, though: a few endpoints return `401` when you're authenticated but lack access to a specific resource. Distinguish by the error body's `category` field: diff --git a/integrations/mcp.mdx b/integrations/mcp.mdx index 4fbbcc71..2d954241 100644 --- a/integrations/mcp.mdx +++ b/integrations/mcp.mdx @@ -624,6 +624,10 @@ When a client connects to your MCP server: **You don't need to handle any of this manually** - `payments.mcp.start()` configures everything automatically! + +**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. + + **For production servers**, replace `http://localhost:3000` with your public domain (e.g., `https://weather-mcp.yourdomain.com`). From 08768b36e021a8b37d2bec826c49b82844512e22 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 15 Sep 2026 13:27:38 +0200 Subject: [PATCH 2/5] docs(auth): fix the stale RFC 8414 / device examples in oauth-openapi.json; name the SDK discovery gap on the MCP page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds eruizgar91's two should-fix findings on #394: - `api-reference/oauth-openapi.json` (hand-authored, docs#271; mounted as the Authentication API-reference group) still showed the pre-#3430 shapes. The `authorization_endpoint` example was wrong on two axes — API host instead of the web app, and no `?network=` — i.e. the exact string a hand-wiring integrator copies, the failure mode nvm-monorepo#3430 exists to prevent. Three string edits, made as text. - `integrations/mcp.mdx`: the note sat under "your own MCP server's discovery document", which the SDK serves and which is still tier-blind until payments#447 / payments-py#277 ship; say so and point hand-configured clients at the API tier's endpoint meanwhile. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Cpykv9mTQL88E3oXwA11QF --- api-reference/oauth-openapi.json | 6 +++--- integrations/mcp.mdx | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api-reference/oauth-openapi.json b/api-reference/oauth-openapi.json index c318e6a8..9e6c4e35 100644 --- a/api-reference/oauth-openapi.json +++ b/api-reference/oauth-openapi.json @@ -165,7 +165,7 @@ "summary": "Authorization Server Metadata (RFC 8414)", "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"] } } } } } + "200": { "description": "RFC 8414 metadata", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true, "example": { "issuer": "https://api.sandbox.nevermined.app", "authorization_endpoint": "https://nevermined.app/oauth/authorize?network=sandbox", "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"] } } } } } } } }, @@ -270,8 +270,8 @@ "properties": { "device_code": { "type": "string", "description": "Machine secret the agent polls with (opaque)." }, "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" }, + "verification_uri": { "type": "string", "example": "https://nevermined.app/oauth/device?network=sandbox", "description": "Where the human approves. Carries the API tier (`network=sandbox|live`) — hand it over unchanged." }, + "verification_uri_complete": { "type": "string", "example": "https://nevermined.app/oauth/device?network=sandbox&user_code=BCDF-GHJK", "description": "RFC 8628 §3.3.1: verification_uri with the user_code pre-filled (one query string)." }, "expires_in": { "type": "integer", "example": 600 }, "interval": { "type": "integer", "example": 5, "description": "Minimum seconds between token polls." } } diff --git a/integrations/mcp.mdx b/integrations/mcp.mdx index 2d954241..10034fed 100644 --- a/integrations/mcp.mdx +++ b/integrations/mcp.mdx @@ -626,6 +626,8 @@ When a client connects to your MCP server: **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. + +The discovery document your **own** MCP server serves (step 1 above) comes from the SDK, not from the API — and until [payments#447](https://github.com/nevermined-io/payments/issues/447) / [payments-py#277](https://github.com/nevermined-io/payments-py/issues/277) ship, it still advertises a tier-blind `authorization_endpoint`. Until then, point hand-configured clients at the API tier's endpoint above rather than at your server's document. From 9ac427f54ac68eefd57576e231fee59b903034ac Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 15 Sep 2026 13:37:56 +0200 Subject: [PATCH 3/5] docs(auth): state the authorize-URL requirement, not the rollout; disambiguate `network` from the x402 rail field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Folds round 3 on #394: - the hand-configuration instruction now states the requirement ("must end up as …?network=sandbox|live; if the fetched document carries none, append it") on the overview and MCP pages — correct before and after the deploy, and it never names an environment's rollout state (docs ADR 0003); - `network` here is distinct from the x402 request body's `network` (the settlement rail, `eip155:`), which the x402 page warns must never be `sandbox`/`live` — say so where the two could be confused; - a client that appends with a second `?` is now rejected with a message naming the mistake, not silently mis-tiered — tell the reader. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Cpykv9mTQL88E3oXwA11QF --- integrate/authentication/overview.mdx | 2 +- integrations/mcp.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integrate/authentication/overview.mdx b/integrate/authentication/overview.mdx index 2e9f7dba..2e3484ce 100644 --- a/integrate/authentication/overview.mdx +++ b/integrate/authentication/overview.mdx @@ -94,7 +94,7 @@ Sandbox and Live are separate authorization servers with separate discovery docu } ``` -`network` is `sandbox` or `live` — the same parameter the embeddable widgets take. The device flow's `verification_uri` carries it too. **Keep the query string** when you append your own parameters (RFC 6749 §3.1 requires it; standard clients do this already). A discovery-driven client gets the right value for free; if you configure an OAuth provider **by hand** — a connector's "authorization URL" form field, say — copy the endpoint from the tier's discovery document verbatim, query string included. An authorize link without `network` is served on whichever tier the user's browser last used (Live by default), and a connector registered only on Sandbox is then refused as *"Connector not authorized"* until the user switches manually. +`network` is `sandbox` or `live` — the same parameter the embeddable widgets take, and distinct from the x402 request body's `network` field, which names the settlement rail (`eip155:`), never a tier. The device flow's `verification_uri` carries it too. **Keep the query string** when you append your own parameters (RFC 6749 §3.1 requires it; standard clients do this already — one that appends with a second `?` is rejected outright with an error naming the mistake, rather than silently landing on the wrong tier). A discovery-driven client gets the right value for free; if you configure an OAuth provider **by hand** — a connector's "authorization URL" form field, say — copy the endpoint from the tier's discovery document, query string included. **The endpoint must end up as `…/oauth/authorize?network=sandbox` or `…?network=live`; if the document you fetch carries no `network`, append it yourself.** An authorize link without `network` is served on whichever tier the user's browser last used (Live by default), and a connector registered only on Sandbox is then refused as *"Connector not authorized"* until the user switches manually. ### Reading a 401 diff --git a/integrations/mcp.mdx b/integrations/mcp.mdx index 10034fed..fd55d091 100644 --- a/integrations/mcp.mdx +++ b/integrations/mcp.mdx @@ -625,7 +625,7 @@ When a client connects to your MCP server: **You don't need to handle any of this manually** - `payments.mcp.start()` configures everything automatically! -**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. +**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 — it must end up as `…/oauth/authorize?network=sandbox` or `…?network=live`; if the document you fetch carries no `network`, append it yourself. The discovery document your **own** MCP server serves (step 1 above) comes from the SDK, not from the API — and until [payments#447](https://github.com/nevermined-io/payments/issues/447) / [payments-py#277](https://github.com/nevermined-io/payments-py/issues/277) ship, it still advertises a tier-blind `authorization_endpoint`. Until then, point hand-configured clients at the API tier's endpoint above rather than at your server's document. From f24bccf31cdc5ee065384cfeff35228f29f5ab24 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 15 Sep 2026 13:42:21 +0200 Subject: [PATCH 4/5] =?UTF-8?q?docs(auth):=20the=20consent=20URL=20and=20t?= =?UTF-8?q?he=20API's=20POST=20/oauth/authorize=20are=20two=20endpoints=20?= =?UTF-8?q?=E2=80=94=20say=20which=20one=20the=20tier=20bullet=20means?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Cpykv9mTQL88E3oXwA11QF --- integrate/authentication/oauth-authorization-code.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrate/authentication/oauth-authorization-code.mdx b/integrate/authentication/oauth-authorization-code.mdx index ba2bf4fc..e0fbb805 100644 --- a/integrate/authentication/oauth-authorization-code.mdx +++ b/integrate/authentication/oauth-authorization-code.mdx @@ -20,7 +20,7 @@ Use the **OAuth 2.1 Authorization Code flow with PKCE** when a browser-based MCP - **An authenticated user.** `POST /oauth/authorize` runs in the signed-in user's context — the browser client calls it with the user's Nevermined API key. Without an authenticated user it fails with `BCK.OAUTH.0007`. - **A pre-registered `client_id`.** Same as the device flow — connectors are onboarded out of band (no dynamic client registration). An unregistered `client_id` is rejected with `BCK.OAUTH.0016`. - **PKCE is mandatory.** Generate a `code_verifier` and its `code_challenge` (`S256`). There is no non-PKCE path. -- **The right tier's authorize URL.** The browser step starts at the `authorization_endpoint` of the tier you are integrating with — `https://nevermined.app/oauth/authorize?network=sandbox` or `…?network=live` — as advertised by that tier's [RFC 8414 document](/integrate/authentication/overview#each-tier-is-its-own-issuer-and-its-authorize-url-says-so). Keep the `network` query parameter when you add `client_id`, `redirect_uri`, `code_challenge` and the rest; it is what makes the consent screen open on the tier your `client_id` is registered on. If you configure the provider by hand rather than via discovery, paste the endpoint with its query string. +- **The right tier's authorize URL.** The browser step starts at the `authorization_endpoint` of the tier you are integrating with — `https://nevermined.app/oauth/authorize?network=sandbox` or `…?network=live` — as advertised by that tier's [RFC 8414 document](/integrate/authentication/overview#each-tier-is-its-own-issuer-and-its-authorize-url-says-so). This is the **web app's consent page**, the URL a connector sends the user's browser to with `client_id`, `redirect_uri`, `code_challenge` and the rest in its **query string** — keep the `network` parameter when you add them; it is what makes the consent screen open on the tier your `client_id` is registered on. It is not the API's `POST /oauth/authorize` in Step 1 below, which the signed-in browser client calls on the API host with those same fields in a **JSON body** (and no `network` — by then the tier is fixed). The consent URL is the only one of the two an integrator ever configures by hand; if you do, paste it with its query string. ## The flow From b7542c76a47bdaad16a382a51a93304fab9392ef Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 15 Sep 2026 13:44:32 +0200 Subject: [PATCH 5/5] =?UTF-8?q?docs(auth):=20x402=20network=20value=20set?= =?UTF-8?q?=20+=20link;=20scope=20the=20SDK=20caveat=20to=20the=20one=20fi?= =?UTF-8?q?eld;=20RFC=208628=20=C2=A73.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Cpykv9mTQL88E3oXwA11QF --- api-reference/oauth-openapi.json | 2 +- integrate/authentication/overview.mdx | 2 +- integrations/mcp.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api-reference/oauth-openapi.json b/api-reference/oauth-openapi.json index 9e6c4e35..3f45230f 100644 --- a/api-reference/oauth-openapi.json +++ b/api-reference/oauth-openapi.json @@ -271,7 +271,7 @@ "device_code": { "type": "string", "description": "Machine secret the agent polls with (opaque)." }, "user_code": { "type": "string", "example": "BCDF-GHJK" }, "verification_uri": { "type": "string", "example": "https://nevermined.app/oauth/device?network=sandbox", "description": "Where the human approves. Carries the API tier (`network=sandbox|live`) — hand it over unchanged." }, - "verification_uri_complete": { "type": "string", "example": "https://nevermined.app/oauth/device?network=sandbox&user_code=BCDF-GHJK", "description": "RFC 8628 §3.3.1: verification_uri with the user_code pre-filled (one query string)." }, + "verification_uri_complete": { "type": "string", "example": "https://nevermined.app/oauth/device?network=sandbox&user_code=BCDF-GHJK", "description": "RFC 8628 §3.2: verification_uri with the user_code pre-filled (one query string)." }, "expires_in": { "type": "integer", "example": 600 }, "interval": { "type": "integer", "example": 5, "description": "Minimum seconds between token polls." } } diff --git a/integrate/authentication/overview.mdx b/integrate/authentication/overview.mdx index 2e3484ce..8c517da0 100644 --- a/integrate/authentication/overview.mdx +++ b/integrate/authentication/overview.mdx @@ -94,7 +94,7 @@ Sandbox and Live are separate authorization servers with separate discovery docu } ``` -`network` is `sandbox` or `live` — the same parameter the embeddable widgets take, and distinct from the x402 request body's `network` field, which names the settlement rail (`eip155:`), never a tier. The device flow's `verification_uri` carries it too. **Keep the query string** when you append your own parameters (RFC 6749 §3.1 requires it; standard clients do this already — one that appends with a second `?` is rejected outright with an error naming the mistake, rather than silently landing on the wrong tier). A discovery-driven client gets the right value for free; if you configure an OAuth provider **by hand** — a connector's "authorization URL" form field, say — copy the endpoint from the tier's discovery document, query string included. **The endpoint must end up as `…/oauth/authorize?network=sandbox` or `…?network=live`; if the document you fetch carries no `network`, append it yourself.** An authorize link without `network` is served on whichever tier the user's browser last used (Live by default), and a connector registered only on Sandbox is then refused as *"Connector not authorized"* until the user switches manually. +`network` is `sandbox` or `live` — the same parameter the embeddable widgets take, and distinct from the x402 request body's `network` field, which names the settlement rail (`eip155:`, or `stripe` / `braintree` / `visa` — [details](/development-guide/nevermined-x402#paymentrequired-response-402)), never a tier. The device flow's `verification_uri` carries it too. **Keep the query string** when you append your own parameters (RFC 6749 §3.1 requires it; standard clients do this already — one that appends with a second `?` is rejected outright with an error naming the mistake, rather than silently landing on the wrong tier). A discovery-driven client gets the right value for free; if you configure an OAuth provider **by hand** — a connector's "authorization URL" form field, say — copy the endpoint from the tier's discovery document, query string included. **The endpoint must end up as `…/oauth/authorize?network=sandbox` or `…?network=live`; if the document you fetch carries no `network`, append it yourself.** An authorize link without `network` is served on whichever tier the user's browser last used (Live by default), and a connector registered only on Sandbox is then refused as *"Connector not authorized"* until the user switches manually. ### Reading a 401 diff --git a/integrations/mcp.mdx b/integrations/mcp.mdx index fd55d091..e2c1b618 100644 --- a/integrations/mcp.mdx +++ b/integrations/mcp.mdx @@ -627,7 +627,7 @@ When a client connects to your MCP server: **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 — it must end up as `…/oauth/authorize?network=sandbox` or `…?network=live`; if the document you fetch carries no `network`, append it yourself. -The discovery document your **own** MCP server serves (step 1 above) comes from the SDK, not from the API — and until [payments#447](https://github.com/nevermined-io/payments/issues/447) / [payments-py#277](https://github.com/nevermined-io/payments-py/issues/277) ship, it still advertises a tier-blind `authorization_endpoint`. Until then, point hand-configured clients at the API tier's endpoint above rather than at your server's document. +The discovery document your **own** MCP server serves (step 1 above) comes from the SDK, not from the API — and until [payments#447](https://github.com/nevermined-io/payments/issues/447) / [payments-py#277](https://github.com/nevermined-io/payments-py/issues/277) ship, it still advertises a tier-blind `authorization_endpoint`. Until then, override **just that field** with the API tier's endpoint above and keep the rest of your server's document — its `token_endpoint` already names the right tier, and its `registration_endpoint` (`/register`, step 2 above) exists only there.