OAuth 2.1 接入指南
OpenViking 服务端原生实现 OAuth 2.1。任何需要 OAuth 的客户端 — 包括 MCP 客户端(Claude.ai / Claude Desktop / ChatGPT / Cursor)以及其他浏览器应用 — 都可以直接对服务器授权,无需任何第三方代理。协议层(DCR、authorize、 token、metadata)由官方 mcp.server.auth SDK 提供,整体遵循 OAuth 2.1 规范,非 MCP 的 OAuth 客户端也能正常工作。
推荐配置
前提:公网 HTTPS。OAuth 2.1(以及 MCP SDK)对非 localhost 的 issuer 强制要求 HTTPS。请参阅公网访问指南了解如何配置 Caddy 或 nginx 的 HTTPS。
配置 HTTPS — 按公网访问指南设置好
https://ov.your-domain.com(Caddy +.env+docker compose up)。在
~/.openviking/ov.conf启用 OAuth:json{ "oauth": { "enabled": true } }重启(
docker compose restart openviking)。接入客户端。 Claude.ai → Connectors → Add → 输入
https://ov.your-domain.com/mcp。浏览器跳到https://ov.your-domain.com/studio/oauth/consent:如果尚未登录 Studio, 先在弹出的"连接与身份"对话框里粘入 API Key;然后在 consent 卡片上点 Authorize。浏览器自动跳回 Claude.ai,连接器就位。
线上路径就这四步。后续章节解释每一块为什么这样设计、本地怎么不走 HTTPS 做 联调、出问题时怎么用 curl 排查。
为什么需要原生 OAuth
部分 MCP 客户端只接受 OAuth 2.1,不接受 API Key。在此之前唯一的方案是部署社区的 MCP-Key2OAuth Cloudflare Worker 代理,把 OAuth 翻译成 API Key bearer。原生支持解决了:
- 额外部署单元(CF Worker + 2 个 KV namespace)
- 第三方信任面(代理运营方有解密上游 API Key 的能力)
- 用户在浏览器里手动粘贴 API Key 的体验
API Key 认证仍按原方式工作,OAuth 只是叠加层。
工作原理
OpenViking 授权 UI 默认走 OpenViking Studio 内的 consent 页面(与主服务 同源、与 Studio 共用 session)。MCP 客户端打开浏览器授权时:
1. MCP 客户端 POST /mcp → 401 + WWW-Authenticate
2. MCP 客户端 GET /.well-known/oauth-protected-resource (RFC 9728)
3. MCP 客户端 GET /.well-known/oauth-authorization-server (RFC 8414)
4. MCP 客户端 POST /register 动态客户端注册 (RFC 7591)
5. MCP 客户端 GET /authorize?... (浏览器重定向)
6. 服务端 → /studio/oauth/consent?pending=...
consent 页加载,并 fetch /api/v1/auth/oauth/pending/<id>
渲染 client_name / redirect_host / scopes
7. 用户 在 Studio 已登录的 tab 上点 Authorize(也可在 IdentityPicker
里临时粘贴另一个 API Key 完成一次性授权)
8. Studio POST /api/v1/auth/oauth-verify (Authorization: Bearer <api-key>,
body: {pending_id, decision}),服务端把 pending 标记为 verified
并绑定调用方的身份(account / user / role)
9. Studio 轮询 /oauth/authorize/page/status,命中 "approved",
自动跳转回 MCP 客户端的 redirect_uri 并附 auth code
10. MCP 客户端 POST /token (PKCE S256) → access_token (ovat_...)
+ refresh_token (ovrt_...)
11. MCP 客户端 POST /mcp (Authorization: Bearer ovat_...) → 调工具授权 consent 直接发生在 Studio 内,不需要跨标签复制验证码。Studio 的 sessionStorage 里已经持有 API Key(你登录 Studio 时填的那个),consent 页用 它做 Authorization: Bearer 调 verify。
如果当前设备打不开 Studio(例如 CLI MCP 客户端、跨设备授权场景),consent 页底部的 "Use another device →" 链接会回退到服务端 HTML 授权页 /oauth/authorize/page:页面显示 6 字符 display_code,让你在另一台已经 登录 Studio 的设备上打开 /studio/oauth/verify 输入。
服务端还保留 "push 模式" 的 OTP 端点 POST /api/v1/auth/otp 供 CLI/脚本场 景使用。Studio 侧边栏底部"OAuth 设置"入口(桌面端弹出对话框,移动端 跳转 /studio/oauth/setup)里就有 OAuth client OTP 区块,可以一键生成 短期 OTP 交给 MCP 客户端。
快速验证(HTTP,仅本地)
最快确认 OAuth 装配正确的方式是在 127.0.0.1 跑一遍。MCP SDK 接受 http://127.0.0.1 与 http://localhost 作为 issuer URL 而无需 HTTPS — 但 Claude.ai / Claude Desktop 等线上客户端只接受公网 HTTPS,所以这个模式只 适合用 MCP Inspector 之类的本地工具做联调。
在
~/.openviking/ov.conf启用 OAuth:json{ "oauth": { "enabled": true } }启动:
bashdocker compose up -d或不用 Docker:
bashopenviking-server打开 Studio 并登录:访问 http://127.0.0.1:1933/studio,在右上角 打开"连接与身份"对话框,把 API Key 粘进去 → Save。
接一个本地 MCP 客户端(例如 MCP Inspector)到
http://127.0.0.1:1933/mcp。客户端会走上面那套流程,浏览器自动跳到/studio/oauth/consent?pending=...,确认即可拿到 token。如果想用 push 模式,从侧边栏底部"OAuth 设置"入口(/studio/oauth/setup)里的 OAuth client OTP 区块直接生成。
线上接 Claude.ai / Claude Desktop 走公网访问指南。
生产部署(HTTPS)
OAuth 2.1 对非 localhost 的 issuer 强制要求 HTTPS。 公网访问指南详细介绍了 Caddy、nginx、docker compose、 CDN 的配置方法。简要步骤:
- 按公网访问指南 § 添加 HTTPS 配置好
https://your-domain.com,使 1934 端口走 TLS。 - 启用 OAuth:
ov.conf里{ "oauth": { "enabled": true } }。 - 重启:
docker compose restart openviking。 - 在
.env设置OPENVIKING_PUBLIC_BASE_URL=https://your-domain.com(服务端用它作为 OAuth 元数据和WWW-Authenticate的 issuer)。
HTTPS + OAuth 就绪后,按下面的方式接入客户端。
接入仅支持 OAuth 的 MCP 客户端
Claude.ai (Web)
- Settings → Connectors → Add connector。
- 输入
https://my.ov/mcp作为服务器 URL。 - Claude 弹出授权页面,自动跳到
https://my.ov/studio/oauth/consent?pending=...。 - 如未登录 Studio,先在弹出的"连接与身份"对话框里填 API Key(也可在 IdentityPicker 里临时粘一个 key 一次性授权)。
- 在 consent 卡片确认 client_name / redirect_host 后点 Authorize。
- 浏览器自动跳回 Claude,token 已颁发。
如果你在 CLI 设备上触发授权(本地浏览器打不开 consent),把 authorize URL 复制到桌面浏览器,或在 consent 页底部点 "Use another device →" 走 6 字符码的跨设备路径(在另一台已登录 Studio 的设备打开
/studio/oauth/verify输入码)。
Claude Desktop / Claude Code
Claude Desktop 流程相同。Claude Code 直接用 API Key 更简单:
claude mcp add --transport http openviking https://my.ov/mcp \
--header "Authorization: Bearer <api-key>"如果你想让 Claude Code 走 OAuth,体验和 Claude.ai 一致。
ChatGPT (Codex / Plus / Enterprise)
Settings → Beta features → Custom Connectors。输入 MCP URL,ChatGPT 通过 /.well-known/... 文档自动发现 OAuth 端点,走相同的 authorize → token 流程。
Cursor
Cursor 看到 401 + WWW-Authenticate: Bearer resource_metadata=... 后会自动 进入 OAuth 流程。在 Cursor 的 MCP 设置里加 URL 即可。
用 curl 验证完整流程
不需要真实 MCP 客户端:
# 1. 注册客户端
curl -X POST -H "Content-Type: application/json" \
-d '{"redirect_uris":["http://127.0.0.1:9999/cb"],"client_name":"test","token_endpoint_auth_method":"none"}' \
https://my.ov/register
# → {"client_id":"...", ...}
# 2. PKCE 对
VERIFIER=$(openssl rand -base64 64 | tr -d '=+/' | head -c 64)
CHALLENGE=$(printf "%s" "$VERIFIER" | openssl dgst -sha256 -binary | basenc --base64url | tr -d '=')
# 3. 浏览器访问 authorize URL,页面会显示 6 字符码
echo "https://my.ov/authorize?response_type=code&client_id=$CID&redirect_uri=http://127.0.0.1:9999/cb&code_challenge=$CHALLENGE&code_challenge_method=S256&state=xyz"
# 4. 在 Studio consent 页确认(或直接 curl)
# - Studio 路径用 pending_id(authorize 页的 ?pending=... 参数)
# - 跨设备路径用 6 字符 display_code
curl -X POST -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"pending_id":"<pending-id-from-authorize-url>","decision":"approve"}' \
https://my.ov/api/v1/auth/oauth-verify
# 5. 浏览器自动 302 到 /cb?code=ovac_...&state=xyz,记下 code
# 6. 用 auth code 换 token
curl -X POST \
-d "grant_type=authorization_code&code=ovac_...&client_id=$CID&code_verifier=$VERIFIER&redirect_uri=http://127.0.0.1:9999/cb" \
https://my.ov/token
# → {"access_token":"ovat_...","refresh_token":"ovrt_...","expires_in":3600}
# 7. 用 access token 调 MCP
curl -X POST -H "Authorization: Bearer ovat_..." \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \
https://my.ov/mcp配置参考
ov.conf 片段:
{
"oauth": {
"enabled": false, // 默认关闭
"issuer": null, // 例 "https://my.ov"(可选;env 变量优先级更高)
"access_token_ttl_seconds": 3600, // 1 小时
"refresh_token_ttl_seconds": 2592000, // 30 天
"auth_code_ttl_seconds": 300, // 5 分钟
"otp_ttl_seconds": 300, // 5 分钟
"db_filename": "oauth.db" // 相对 storage.workspace
}
}环境变量:
| 变量 | 用途 |
|---|---|
OPENVIKING_PUBLIC_BASE_URL | 最高优先级的公网 origin override(用作 issuer / PRM / WWW-Authenticate) |
OPENVIKING_CONFIG_FILE | ov.conf 路径(也可用 --config) |
Token 模型
| Token | 形态 | 前缀 | TTL | 存储 |
|---|---|---|---|---|
| Access token | secrets.token_urlsafe(40) | ovat_ | 1 小时 | SQLite (SHA-256 索引) |
| Refresh token | secrets.token_urlsafe(40) | ovrt_ | 30 天 | SQLite (SHA-256 索引) |
| Authorization code | secrets.token_urlsafe(40) | ovac_ | 5 分钟 | SQLite (SHA-256 索引) |
| Display code(页面) | 6 字符(去 O/0/I/1) | — | 10 分钟 | SQLite (oauth_pending_authorizations) |
所有 token 都是 opaque(不签发 JWT),服务端没有任何加密密钥需要管理。 每次请求按 SHA-256 哈希查 SQLite,撤销 token 是一次 UPDATE。
Token 与身份
每个 token 在签发时绑定一个 (account_id, user_id, role) 三元组。OAuth token 拥有的权限 = 颁发它时所用 API Key 的权限,不更多也不更少。
OAuth 生命周期 ≤ 授权 Key 生命周期
每个 token 额外记录授权方 API Key 的 SHA-256 指纹。每次 OAuth bearer 鉴权时服务端重算该用户当前的 key 指纹并严格比对,效果:
- 轮换 用户 API Key(
regenerate_key)立即让该用户名下所有 OAuth access / refresh token 失效。无需手动撤销,下一次 bearer 请求即返回 401,客户端需重新走授权流程。 - 删除 用户(
remove_user)同理:指纹查找返回None,所有 OAuth token 立即停用。 - ROOT key 和 trusted-mode 身份无法签发 OAuth(没有 per-user key 可绑定)。
/api/v1/auth/otp和/api/v1/auth/oauth-verify会以 400 拒绝这类调用方。
指纹算法为 sha256(stored_key_value):API Key 哈希未开启时即明文 key 的 SHA-256,开启时即 Argon2id 哈希结果的 SHA-256。两种情况下 stored 值都是创建/轮换时写入一次后不再变动,因此指纹在两次轮换之间稳定。
故障排查
Claude.ai 直接报 "We couldn't connect" 没弹出授权页
Claude.ai 第一步是 GET /.well-known/oauth-protected-resource。如果这一步 404,OAuth 流程就根本不会启动。检查:
curl -i https://my.ov/.well-known/oauth-protected-resource应当返回带 authorization_servers 字段的 JSON。如果是 404,要么 oauth.enabled = false,要么反代没把 /.well-known/... 路径转发到 1933。
"Issuer URL must be HTTPS"
MCP SDK 拒绝非 127.0.0.1 / localhost 的 http:// issuer。三选一:
- 设置
OPENVIKING_PUBLIC_BASE_URL=https://my.ov - 在
ov.conf里把oauth.issuer写成https://... - 仅本地测试时让客户端直连
http://127.0.0.1:1933
跨设备 fallback 页有码,但 /studio/oauth/verify 报 "Invalid code"
码是 6 字符全大写,传输时区分大小写。/studio/oauth/verify 的输入框会 自动转大写。如果手 动输入,注意字母与数字的混淆字符(字母表已经排除了 O、0、I、1)。
Refresh 一次后再用旧 token 被拒
Refresh token 是一次性的。如果旧 refresh 与新 refresh 同时被使用(例如客户 端有 bug),第二个会被拒绝,整条 token 链会被撤销(RFC 9700 §4.14)。客户端必 须重新走 authorize 流程。
/mcp 401 没有 WWW-Authenticate 头
这个头只在 app.state 上有 oauth_provider 时才发出 — 即 oauth.enabled = true。检查:
curl -i https://my.ov/mcp -d '{}' -H 'Content-Type: application/json' | grep -i www-authenticate