# MCP OAuth (Preview) (/docs/mcp/oauth)



> **In preview** — OAuth login flows are still stabilizing. API keys work everywhere OAuth does; prefer keys unless you need short-lived scoped tokens.

The Voidnet acts as an OAuth 2.0 authorization server, issuing JWT access tokens for production systems.

## Token types [#token-types]

### JWT access tokens [#jwt-access-tokens]

Access tokens are JSON Web Tokens (JWTs) signed with RS256. They are three base64url-encoded segments separated by dots:

```
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature
```

Each token contains:

* **`sub`** — Buyer UUID
* **`scope`** — Permission scope (`mcp:tools` for tool calls, `llm:completions` for chat completions)
* **`exp`** — Expiration time (default 1 hour)
* **`iat`** — Issued at time
* **`iss`** — Voidnet issuer URL
* **`aud`** — Voidnet issuer URL (fixed at mint time; no client-controlled audience input)
* **`kid`** — Key ID identifying the signing key in the JWKS

### API keys [#api-keys]

The Voidnet also accepts API keys (`vnb-sk-*`) as a simpler alternative. See the [Buyer Guide](https://docs.openvoidnet.com/docs/mcp/buyer) for API key management.

## Grant types [#grant-types]

### client\_credentials [#client_credentials]

Exchange your API key for a short-lived JWT:

```bash
curl -X POST http://localhost:8090/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6"
```

Successful response:

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "mcp:tools"
}
```

The returned `access_token` is a JWT usable in any `Authorization: Bearer` header.

**Notes:**

* No separate `client_secret` is needed — the API key itself is the secret
* An optional `scope` parameter is accepted (`mcp:tools` default, `llm:completions` for chat)
* The `aud` claim is always the Voidnet issuer URL — the token endpoint accepts no client-controlled audience input

### authorization\_code (+ PKCE, interactive clients) [#authorization_code--pkce-interactive-clients]

For OAuth-aware MCP clients (Claude Desktop, Cursor, VS Code). The gateway hosts the login+consent page — no redirect to any accounts app:

1. Open `GET /authorize?client_id=<key>&redirect_uri=<exact-callback>&response_type=code&code_challenge=<43-char-base64url>&code_challenge_method=S256&scope=mcp:tools&state=<opaque>` — renders the login+consent form.
2. User submits email+password via `POST /authorize` — issues a single-use authorization code (5-minute TTL) and redirects to `redirect_uri?code=...&state=...`.
3. Exchange the code at `POST /oauth/token` (`grant_type=authorization_code`, plus `code`, `redirect_uri`, `code_verifier`) — returns access token **and** refresh token.

PKCE `S256` is mandatory; `redirect_uri` must be an exact `http(s)` match.

### refresh\_token [#refresh_token]

Refresh tokens live 30 days and rotate on every use (single-use — reuse is rejected):

```bash
curl -X POST http://localhost:8090/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "client_id=vnb-sk-a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6" \
  -d "refresh_token=<previous-refresh-token>"
```

The response contains a new access token **and** a new refresh token — store both, discard the old refresh token.

## Using tokens [#using-tokens]

All requests to the Voidnet use the `Authorization: Bearer` header:

```bash
# API key (local gateway on 8090, prod on 8080 / api.openvoidnet.com)
curl -X POST http://localhost:8090/v1-beta/mcp/acmecorp/weather-tools \
  -H "Authorization: Bearer vnb-sk-..." \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/list" \
  -H "Mcp-Name: tools/list" \
  -d '{"jsonrpc": "2.0", "id": "1", "method": "tools/list"}'

# JWT access token
TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -X POST http://localhost:8090/v1-beta/mcp/acmecorp/weather-tools \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/list" \
  -H "Mcp-Name: tools/list" \
  -d '{"jsonrpc": "2.0", "id": "1", "method": "tools/list"}'
```

Every billable routed call carries `X-Voidnet-Request-Id` downstream to the publisher and upstream to the buyer. Match that value to console Usage Logs `requestId` to reconcile call by call. Non-billable discovery and list calls, including cache-served lists, carry no metering row.

The Voidnet auto-detects the credential type — three dot-separated segments means JWT, otherwise API key.

### Token expiration [#token-expiration]

Tokens have an `exp` claim. When a token expires, the Voidnet returns `api_key_expired` (401):

1. Detect the `401` response
2. Request a new token from `POST /oauth/token`
3. Retry the request with the new token

## Publisher authentication [#publisher-authentication]

When the Voidnet proxies a request to a publisher's server, it authenticates using the publisher's API key (`vnp-sk-*`):

```
Authorization: Bearer vnp-sk-27fc6268dc64a9ba2c4cb92489e9175cbf404e260beb268b976f1a56582eff3
```

Publisher keys are:

* Generated in the Console when publishing an app
* The Voidnet always sends `vnp-sk-*` regardless of how the buyer authenticated
* Sent to the publisher server on every proxied request

## OAuth endpoints [#oauth-endpoints]

The Voidnet acts as an OAuth 2.0 authorization server. Full interactive reference (parameters, responses, Try-it) lives on the generated [MCP API page](https://docs.openvoidnet.com/docs/reference/api/mcp):

| Endpoint                                      | Description                                                                       |
| --------------------------------------------- | --------------------------------------------------------------------------------- |
| `POST /oauth/token`                           | Issue access tokens (`client_credentials`, `authorization_code`, `refresh_token`) |
| `GET /authorize`                              | Render the login+consent form (interactive clients)                               |
| `POST /authorize`                             | Process login+consent, issue authorization code, redirect                         |
| `GET /.well-known/oauth-authorization-server` | RFC 8414 AS metadata                                                              |
| `GET /.well-known/oauth-protected-resource`   | RFC 9728 resource metadata                                                        |
| `GET /.well-known/jwks.json`                  | Voidnet's public signing keys                                                     |

## Credential management [#credential-management]

**API keys** are managed through the Voidnet:

* **Generate** — Create a new key from the API Keys section
* **Revoke** — Immediately invalidate a compromised key
* **Rotate** — Generate a new key, update your configuration, revoke the old one

**OAuth access tokens** are obtained programmatically:

* **client\_credentials** — Exchange an API key for a JWT at `POST /oauth/token`
* Tokens expire after a configurable period (default 1 hour)

## Credential security best practices [#credential-security-best-practices]

* Store API keys and tokens in environment variables, not in code
* Use a secrets manager in production
* Rotate API keys periodically
* Revoke compromised keys immediately
* Never expose credentials in client-side code, logs, or version control
* Use short-lived JWT access tokens in production systems instead of raw API keys
