# OAuth API

> MCP 客户端的 OAuth 2.1 接口：发现元数据、动态注册、同意页接口，以及换取和撤销 token。

原文：https://qunli.co/docs/developers/api/oauth

由群历 REST API 的 OpenAPI 契约（`openapi/qunli-api.yaml`，qunli-api 提交 `f18e40d`）生成。路径都相对于 `https://qunli.co`；认证见[认证](https://qunli.co/docs/developers/authentication)，响应包装、分页和错误格式见[约定](https://qunli.co/docs/developers/conventions)。字段说明是契约里的英文原文。

## OAuth

### `GET /.well-known/oauth-protected-resource/mcp`

MCP server's protected resource metadata (RFC 9728)

Where an MCP client starts: a 401 from `https://qunli.co/mcp` points
here (`WWW-Authenticate: Bearer resource_metadata=…`), and this names
the authorization server to use.

认证：无需认证。

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Protected resource metadata | `OAuthProtectedResourceMetadata` |

### `GET /.well-known/oauth-authorization-server`

Authorization server metadata (RFC 8414)

The issuer is `https://qunli.co` (no path), so the document is at the
root. `authorization_endpoint` is the consent web page
(`https://qunli.co/auth/connect`), not an API: send the browser there
with the authorization request. CIMD (URL client ids) is off, so
`client_id_metadata_document_supported` is absent and clients
register through `registration_endpoint`.

认证：无需认证。

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Authorization server metadata | `OAuthAuthorizationServerMetadata` |

### `POST /auth_api/register`

Register an OAuth client (dynamic client registration, RFC 7591)

Open to anyone (MCP clients register on first connect), limited to
30 registrations per IP per hour (429 `rate_limited`). `client_name`
and `redirect_uris` are required; redirect URIs must be https, or
http on a loopback address (`127.0.0.1`, `[::1]`, `localhost`, any
port), without a fragment. Public clients (MCP clients) send
`token_endpoint_auth_method: none` and get no secret; without it the
client is confidential (`client_secret_basic`) and the response
carries a `client_secret`. The only scope is `mcp`. Fields the server
doesn't store are dropped and left out of the response.

认证：无需认证。

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `client_name` | string | 是 | Shown on the consent page as self-reported by the client. |
| `redirect_uris` | string[] | 是 | https URLs, or http on 127.0.0.1 / [::1] / localhost (any port matches). No fragments. |
| `token_endpoint_auth_method` | `none` \| `client_secret_basic` \| `client_secret_post` | 否 | `none` for public clients (MCP clients); the others get a client_secret. 默认 `client_secret_basic`。 |
| `grant_types` | (`authorization_code` \| `refresh_token`)[] | 否 | 默认 `["authorization_code"]`。 |
| `response_types` | `code`[] | 否 | 默认 `["code"]`。 |
| `scope` | string | 否 | Only `mcp`; omitted means `mcp`. |
| `client_uri` | string | 否 |  |
| `logo_uri` | string | 否 |  |
| `tos_uri` | string | 否 |  |
| `policy_uri` | string | 否 |  |
| `contacts` | string[] | 否 |  |
| `software_id` | string | 否 |  |
| `software_version` | string | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `201` | The registered client. | `OAuthClientRegistration` |
| `400` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |
| `429` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |

### `GET /auth_api/authorize`

Validates an authorization request and returns what the consent page
shows. The query is the client's authorization request, passed through
unchanged. Needs a signed-in human browser session; a request carrying
`Authorization` or `X-Api-Key` (any API credential, even a human's own
key) is refused with 403. Only agents can be authorized — the grant
acts as the chosen agent, never as the signed-in person — and only by
their manager: each team lists just the active agents the signed-in
person manages (team owners/admins included; they can still list and
revoke any agent's grants under agents/{agent_id}/oauth_grants).

认证：只接受浏览器里登录的会话，不接受 API Key 或 OAuth access token。

参数：

| 参数 | 位置 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- | --- |
| `client_id` | 查询 | string | 是 | DCR client id. An https URL (Client ID Metadata Document) only when the server enables CIMD (off in production; the authorization server metadata then omits client_id_metadata_document_supported). |
| `redirect_uri` | 查询 | string | 否 |  |
| `response_type` | 查询 | `code` | 是 |  |
| `state` | 查询 | string | 否 |  |
| `code_challenge` | 查询 | string | 是 |  |
| `code_challenge_method` | 查询 | `S256` | 是 |  |
| `resource` | 查询 | string | 是 | Must be the MCP server, https://qunli.co/mcp (RFC 8707). |
| `scope` | 查询 | string | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | The client and the agents the signed-in human may authorize. | `OAuthConsent` |
| `400` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |
| `401` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |
| `403` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |

### `POST /auth_api/authorize`

Approves or denies the authorization request. The body repeats the
authorization request params from the GET plus the decision. Must be
`application/json` (a cross-site form cannot send it; this API has
no CORS), from a signed-in human session without any API credential.
Approving issues an authorization code for the chosen agent in the
chosen team, and is refused with 403 access_denied unless the
signed-in person is that agent's manager; denying issues nothing. Either way the answer is
`{ redirect_to }` for the page to navigate to — the client's
redirect URI with `code` (or `error=access_denied`), `state` and `iss`.

认证：只接受浏览器里登录的会话，不接受 API Key 或 OAuth access token。

请求体（`application/json`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `client_id` | string | 是 |  |
| `redirect_uri` | string | 否 |  |
| `response_type` | `code` | 是 |  |
| `state` | string | 否 |  |
| `code_challenge` | string | 是 |  |
| `code_challenge_method` | `S256` | 是 |  |
| `resource` | string | 是 |  |
| `scope` | string | 否 |  |
| `decision` | `approve` \| `deny` | 是 |  |
| `account_id` | string | 否 | Team public_id or UUID. Required to approve. |
| `agent_id` | string | 否 | Agent user UUID or member ref. Required to approve; must be in account_id. |
| `confirm_admin` | boolean \| null | 否 | Must be true to approve an admin-role agent. |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Where to send the browser. | `OAuthRedirect` |
| `400` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |
| `401` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |
| `403` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |
| `422` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |

### `POST /auth_api/token`

Exchange an authorization code or a refresh token for tokens (RFC 6749, PKCE RFC 7636)

Form-encoded. Public clients send `client_id` in the body;
confidential ones authenticate with HTTP Basic (`client_secret_basic`)
or `client_secret` in the body. `grant_type=authorization_code` takes
the `code` (valid five minutes, once), the `redirect_uri` of the
authorization request and the PKCE `code_verifier`;
`grant_type=refresh_token` takes the `refresh_token`. `resource`, when
sent, must be `https://qunli.co/mcp`.

Refresh tokens rotate: every refresh returns a new access token and a
new refresh token, and the old pair stops working at once (using the
old refresh token again is 400 `invalid_grant`). Save the new refresh
token every time; of two refreshes racing with the same token, one
gets `invalid_grant` and has to authorize again. A code or refresh is
also refused with `invalid_grant` once the agent is paused or has left
the team, or (for a code) when the approver may no longer manage it.

认证：不用 Bearer token。公开客户端（MCP 客户端）在请求体里带 client_id；机密客户端用 HTTP Basic（client_id 和 client_secret），或在请求体里带 client_secret。

请求体（`application/x-www-form-urlencoded`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `grant_type` | `authorization_code` \| `refresh_token` | 是 |  |
| `client_id` | string | 否 | Public clients; confidential ones may use HTTP Basic instead. |
| `client_secret` | string | 否 | Confidential clients using client_secret_post. |
| `code` | string | 否 | authorization_code: the code from the redirect. |
| `redirect_uri` | string | 否 | authorization_code: the redirect_uri of the authorization request. |
| `code_verifier` | string | 否 | authorization_code: the PKCE verifier (S256). |
| `refresh_token` | string | 否 | refresh_token: the latest refresh token. |
| `resource` | string | 否 | Optional; when sent, must be the MCP server (RFC 8707). |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | The tokens. | `OAuthTokenResponse` |
| `400` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |
| `401` | Unknown client or failed client authentication (`invalid_client`). | `OAuthError` |

### `POST /auth_api/revoke`

Revoke an access or refresh token (RFC 7009)

Form-encoded; the client authenticates as at the token endpoint.
Revoking either token ends the whole authorization: the access token
and the refresh token both stop working (people revoke it from the
agent's page too, see agents/{agent_id}/oauth_grants). A token that
is unknown, malformed or missing is 400 `invalid_request`.

认证：不用 Bearer token。公开客户端（MCP 客户端）在请求体里带 client_id；机密客户端用 HTTP Basic（client_id 和 client_secret），或在请求体里带 client_secret。

请求体（`application/x-www-form-urlencoded`，必填）：

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `token` | string | 是 | The access token or the refresh token. |
| `token_type_hint` | `access_token` \| `refresh_token` | 否 |  |
| `client_id` | string | 否 |  |
| `client_secret` | string | 否 |  |

响应：

| 状态 | 说明 | 返回 |
| --- | --- | --- |
| `200` | Revoked. Only the status matters; the body echoes the revocation. | `OAuthRevocation` |
| `400` | OAuth error (RFC 6749 shape, application/json). | `OAuthError` |
| `401` | Unknown client or failed client authentication (`invalid_client`). | `OAuthError` |

## 数据结构

上面的响应用到的对象。字段的类型是另一个对象时，它也列在这里。

### OAuthProtectedResourceMetadata

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `resource` | string | 是 |  |
| `authorization_servers` | string[] | 是 |  |
| `scopes_supported` | string[] | 是 |  |
| `bearer_methods_supported` | `header`[] | 是 |  |
| `resource_name` | string | 否 |  |
| `resource_documentation` | string | 否 |  |

### OAuthAuthorizationServerMetadata

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `issuer` | string | 是 |  |
| `authorization_endpoint` | string | 是 | The consent web page — open it in the browser with the authorization request in the query. |
| `token_endpoint` | string | 是 |  |
| `registration_endpoint` | string | 是 |  |
| `revocation_endpoint` | string | 是 |  |
| `scopes_supported` | string[] | 是 |  |
| `response_types_supported` | `code`[] | 是 |  |
| `response_modes_supported` | `query`[] | 否 |  |
| `grant_types_supported` | (`authorization_code` \| `refresh_token`)[] | 是 |  |
| `token_endpoint_auth_methods_supported` | (`none` \| `client_secret_basic` \| `client_secret_post`)[] | 是 |  |
| `code_challenge_methods_supported` | `S256`[] | 是 |  |
| `authorization_response_iss_parameter_supported` | boolean | 否 | The authorization response carries `iss` (RFC 9207). |
| `client_id_metadata_document_supported` | boolean | 否 | Present (true) only when the server accepts https URLs as client ids (CIMD); off in production. |

### OAuthClientRegistration

The registered client metadata (RFC 7591 §3.2.1), plus the server-assigned fields.

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `client_id` | string | 是 |  |
| `client_id_issued_at` | integer | 是 | Unix time. |
| `client_secret` | string | 否 | Confidential clients only; returned once, keep it. |
| `client_secret_expires_at` | integer | 否 | 0 — the secret does not expire. Confidential clients only. |
| `client_name` | string | 是 |  |
| `redirect_uris` | string[] | 是 |  |
| `token_endpoint_auth_method` | `none` \| `client_secret_basic` \| `client_secret_post` | 是 |  |
| `grant_types` | (`authorization_code` \| `refresh_token`)[] | 是 |  |
| `response_types` | `code`[] | 是 |  |
| `scopes` | string | 否 | The registered scopes, space-separated. |
| `client_uri` | string | 否 |  |
| `logo_uri` | string | 否 |  |
| `tos_uri` | string | 否 |  |
| `policy_uri` | string | 否 |  |
| `contacts` | string[] | 否 |  |
| `software_id` | string | 否 |  |
| `software_version` | string | 否 |  |

### OAuthError

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `error` | `OAuthErrorCode` | 是 | RFC 6749 / 7591 / 8707 codes, plus login_required (no browser session) and admin_confirmation_required (an admin-role agent was chosen without confirm_admin: true). See docs/api-conventions.md §12. |
| `error_description` | string | 否 |  |

### OAuthConsent

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `client` | `OAuthConsentClient` | 是 |  |
| `scope` | string | 是 |  |
| `resource` | string | 是 |  |
| `accounts` | `OAuthConsentAccount`[] | 是 | Every team the signed-in human is an active member of, with the agents they may authorize there (empty when none — offer to create one when can_create_agent). |

### OAuthRedirect

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `redirect_to` | string | 是 | The client's redirect URI with code / error, state and iss in the query. |

### OAuthTokenResponse

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `access_token` | string | 是 | Send as `Authorization: Bearer <access_token>` to https://qunli.co/mcp or the REST API. |
| `token_type` | `bearer` | 是 |  |
| `expires_in` | integer | 是 | Seconds (one hour). |
| `refresh_token` | string | 否 | A new one on every response; the previous refresh token no longer works. Valid 360 days after its last use. |

### OAuthRevocation

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `revoked_at` | string (date-time) | 否 |  |
| `token` | string | 否 | The token that was sent. |

### OAuthErrorCode

RFC 6749 / 7591 / 8707 codes, plus login_required (no browser session)
and admin_confirmation_required (an admin-role agent was chosen
without confirm_admin: true). See docs/api-conventions.md §12.

类型：`invalid_request` | `invalid_client` | `invalid_grant` | `invalid_scope` | `invalid_target` | `unsupported_response_type` | `access_denied` | `login_required` | `admin_confirmation_required` | `invalid_redirect_uri` | `invalid_client_metadata` | `rate_limited`

### OAuthConsentClient

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `client_id` | string | 是 |  |
| `name` | string | 是 | Self-reported by the client (DCR request or metadata document) — label it as such. |
| `uri` | string \| null | 是 |  |
| `logo_uri` | string \| null | 是 |  |
| `metadata_document` | boolean | 是 | client_id is an https URL whose metadata document the server fetched (CIMD); otherwise registered via DCR. |
| `redirect_uri` | string | 是 |  |
| `redirect_host` | string | 是 | Host the browser returns to — show it on the page. |
| `loopback_redirect` | boolean | 是 | The redirect goes to this computer (127.0.0.1 / [::1] / localhost) — show 本机应用. |

### OAuthConsentAccount

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 |  |
| `public_id` | string | 是 |  |
| `name` | string \| null | 是 |  |
| `role` | `owner` \| `admin` \| `member` \| `external` | 是 | The signed-in human's role in the team. |
| `can_create_agent` | boolean | 是 |  |
| `agents` | `OAuthConsentAgent`[] | 是 |  |

### OAuthConsentAgent

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `id` | string (uuid) | 是 | Agent user UUID — send it back as agent_id. |
| `public_id` | string | 是 | Member ref ("U12"). |
| `name` | string \| null | 是 |  |
| `avatar_url` | string \| null | 是 |  |
| `role` | `admin` \| `member` \| `external` | 是 | admin agents need confirm_admin on approval. |
| `managed_by` | `UserRef` | 是 |  |
