跳到主要内容
KaxHub Kopilot
  • 首页
  • 模型
  • 文档
登录免费注册
文档
  1. 文档
  2. ›
  3. 授权
  4. ›
  5. 如何使用OAuth2

如何使用OAuth2

发布于 2026/7/17· 更新于 2026/7/19· 发布者:157335596@qq.com

Kax API OAuth 2.0 接入指南

Kax API 使用 Authorization Code + PKCE 流程。推荐所有客户端都使用 PKCE;服务端应用可以同时使用 client_secret,桌面端、移动端和浏览器应用不得把 secret 当作安全凭据。

本文只描述标准 OAuth 端点。文末提到的 /api/user/oauth/* 是兼容接口,不要把它们和标准 OAuth access token 混用。

1. 开始前的准备

接入前需要准备:

  • 一个已经创建并启用的 OAuth app。
  • 一个已经登记的回调地址。
  • 服务端保存 state 和 code_verifier 的位置。
  • 对 confidential client 安全保存 client_secret 的密钥管理系统。

接入前先通过 discovery 获取端点,不要把生产域名硬编码到业务代码中:

http
GET /.well-known/oauth-authorization-server

重点字段如下:

json
{
  "issuer": "https://api.example.com",
  "authorization_endpoint": "https://api.example.com/api/oauth/authorize",
  "token_endpoint": "https://api.example.com/api/oauth/token",
  "userinfo_endpoint": "https://api.example.com/api/oauth/userinfo",
  "revocation_endpoint": "https://api.example.com/api/oauth/revoke"
}

以 discovery 返回的地址为准。生产环境应配置 OAUTH_ISSUER_URL;不要让客户端依赖请求 Host 动态拼接出的 issuer。

当前支持:

  • response_type=code
  • grant_type=authorization_code
  • grant_type=refresh_token
  • PKCE S256
  • scope:openid、profile、email、apikeys:read
  • confidential client 的 HTTP Basic 或表单认证
  • public client 的 PKCE 认证,不提交 client secret

本文中的标准 OAuth 客户端路由如下。除授权路由会返回浏览器重定向外,资源路由和协议路由均返回 JSON:

方法路由用途认证
GET/.well-known/oauth-authorization-server获取 OAuth discovery 配置无
GET/api/oauth/authorize发起授权并进入登录/授权确认页浏览器登录状态
POST/api/oauth/token授权码换 token 或刷新 tokenconfidential client 或 public client + PKCE
GET/api/oauth/userinfo获取当前用户公开信息OAuth access token
GET/api/oauth/apikeys获取当前用户的 API key 列表OAuth access token + apikeys:read
GET/api/oauth/apikeys/{id}获取单个 API key 的公开信息OAuth access token + apikeys:read
POST/api/oauth/revoke撤销 access token 或 refresh tokenclient authentication
GET/api/oauth/authorizations查看当前用户已授权的 appKax API 登录会话
DELETE/api/oauth/authorizations/{client_id}撤销某个 app 的授权Kax API 登录会话

$KAX_ISSUER 表示 discovery 返回的 issuer,实际请求地址应优先使用 discovery 中的 endpoint 字段,不要在客户端硬编码生产域名。

2. 客户端路由与返回结构

本节按客户端实际调用顺序列出所有标准 OAuth 路由。示例中的 $KAX_ISSUER 取自 discovery 响应的 issuer,$KAX_ACCESS_TOKEN、$KAX_REFRESH_TOKEN、$KAX_AUTHORIZATION_CODE 和 $KAX_CLIENT_SECRET 均为敏感值,示例值不能直接使用。

2.1 Discovery

获取服务器支持的端点、授权类型、scope 和客户端认证方式:

http
GET $KAX_ISSUER/.well-known/oauth-authorization-server
Accept: application/json

成功响应:200 OK。响应带有 Cache-Control: public, max-age=300,结构如下:

json
{
  "issuer": "https://api.example.com",
  "authorization_endpoint": "https://api.example.com/api/oauth/authorize",
  "token_endpoint": "https://api.example.com/api/oauth/token",
  "userinfo_endpoint": "https://api.example.com/api/oauth/userinfo",
  "revocation_endpoint": "https://api.example.com/api/oauth/revoke",
  "response_types_supported": [
    "code"
  ],
  "grant_types_supported": [
    "authorization_code",
    "refresh_token"
  ],
  "code_challenge_methods_supported": [
    "S256"
  ],
  "scopes_supported": [
    "openid",
    "profile",
    "email",
    "apikeys:read"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "client_secret_post",
    "none"
  ]
}

字段含义:

  • issuer:OAuth 服务的规范化根地址。
  • authorization_endpoint:浏览器授权入口。该地址成功时返回重定向,不返回授权码 JSON。
  • token_endpoint:授权码交换和 refresh token 轮换入口。
  • userinfo_endpoint:读取 OAuth 用户公开信息的入口。
  • revocation_endpoint:撤销 access token 或 refresh token 的入口。
  • response_types_supported:当前只支持 response_type=code。
  • grant_types_supported:当前支持授权码和 refresh token 两种 grant。
  • code_challenge_methods_supported:当前只支持 PKCE S256。
  • scopes_supported:当前支持的 scope 列表。
  • token_endpoint_auth_methods_supported:confidential client 可以使用 Basic 或表单认证;授权码交换的 public client 可以使用 none,但必须有 PKCE verifier。

discovery 配置无效时可能返回 503:

json
{
  "error": {
    "code": "temporarily_unavailable",
    "message": "OAuth issuer 配置无效"
  }
}

2.2 Authorization endpoint

授权入口是浏览器导航请求,不建议使用后端 fetch 代替浏览器跳转:

http
GET $KAX_ISSUER/api/oauth/authorize?client_id=kax_client_xxx&redirect_uri=https%3A%2F%2Fclient.example.com%2Foauth%2Fcallback&response_type=code&scope=openid%20profile%20email%20apikeys%3Aread&state=RANDOM_STATE&code_challenge=BASE64URL_SHA256_VERIFIER&code_challenge_method=S256

请求参数:

参数必填说明
client_id是已启用 OAuth app 的客户端 ID。
redirect_uri是必须与 app 登记的地址逐字一致。
response_type是只能是 code。
scope是以空格分隔的 scope;只能使用 discovery 中声明的值。
state推荐客户端生成的随机状态值,最大长度为 2048;回调时必须校验。
code_challenge是对 code_verifier 做 SHA-256 后的 Base64URL 编码值,长度为 43。
code_challenge_method是必须是 S256。

参数校验通过后,响应为 302 Found,Location 指向 Kax API 的授权确认页:

http
HTTP/1.1 302 Found
Location: https://api.example.com/oauth?client_id=kax_client_xxx&redirect_uri=https%3A%2F%2Fclient.example.com%2Foauth%2Fcallback&response_type=code&scope=openid+profile&state=RANDOM_STATE&code_challenge=BASE64URL_SHA256_VERIFIER&code_challenge_method=S256

浏览器未登录时,确认页会先要求登录;登录成功后回到授权确认页。用户批准后,回调地址收到:

text
https://client.example.com/oauth/callback?code=kax_code_xxx&state=RANDOM_STATE

用户取消时,回调地址收到:

text
https://client.example.com/oauth/callback?error=access_denied&state=RANDOM_STATE

授权路由不返回如下形式的成功 JSON,客户端不能把它当作普通 API 调用:

json
{
  "code": "kax_code_xxx"
}

错误响应取决于回调地址是否已经通过安全校验:

  • client、回调地址缺失或无法确认时,不使用用户提供的地址重定向,直接返回 400:

    json
    {
      "error": "invalid_request",
      "error_description": "客户端或回调地址无效"
    }
  • 回调地址有效但授权参数无效时,重定向到已登记的回调地址,并附加 error=invalid_request、error_description 和原始 state(如果提供)。

2.3 Token endpoint

token endpoint 必须使用 application/x-www-form-urlencoded,成功和错误响应都带有:

http
Cache-Control: no-store
Pragma: no-cache

客户端认证有两种形式,二者只能选一种:

  • HTTP Basic:Authorization: Basic base64(client_id:client_secret)。
  • 表单认证:在 body 中提交 client_id 和 client_secret。

如果 Basic 中的 client ID 和表单中的 client ID 同时存在但不一致,认证失败。public client 只有在授权码交换时可以省略 client_secret,但必须提交 client_id 和正确的 code_verifier。当前实现的 refresh_token 请求仍需要有效的 client secret。

授权码交换

confidential client:

bash
curl -X POST "$KAX_ISSUER/api/oauth/token" \
  -u "$KAX_CLIENT_ID:$KAX_CLIENT_SECRET" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'code=$KAX_AUTHORIZATION_CODE' \
  --data-urlencode 'redirect_uri=https://client.example.com/oauth/callback' \
  --data-urlencode 'code_verifier=$KAX_CODE_VERIFIER'

public client:

bash
curl -X POST "$KAX_ISSUER/api/oauth/token" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'client_id=$KAX_CLIENT_ID' \
  --data-urlencode 'code=$KAX_AUTHORIZATION_CODE' \
  --data-urlencode 'redirect_uri=https://client.example.com/oauth/callback' \
  --data-urlencode 'code_verifier=$KAX_CODE_VERIFIER'

必须提交的参数:

参数说明
grant_type固定为 authorization_code。
code回调收到的一次性授权码。
client_idpublic client 必填;confidential client 可由 Basic 提供。
redirect_uri必须与授权请求中的值完全一致。
code_verifier发起授权时生成并保存的原始 verifier,不能重新生成。

成功响应:200 OK。

json
{
  "access_token": "kax_at_xxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "kax_rt_xxx",
  "scope": "openid profile email apikeys:read"
}

字段含义:

  • access_token:调用 userinfo 和 API key 资源时使用的 opaque Bearer token。
  • token_type:固定为 Bearer。
  • expires_in:access token 的有效期,单位为秒,当前为 3600。
  • refresh_token:用于轮换 access token 的 opaque refresh token。
  • scope:本次实际授予的 scope,使用空格分隔。

Refresh token

confidential client 使用原来的 client authentication:

bash
curl -X POST "$KAX_ISSUER/api/oauth/token" \
  -u "$KAX_CLIENT_ID:$KAX_CLIENT_SECRET" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode 'refresh_token=$KAX_REFRESH_TOKEN' \
  --data-urlencode 'scope=openid profile'

参数说明:

  • grant_type:固定为 refresh_token。
  • refresh_token:上一次成功响应中的 refresh token。
  • scope:可选,只能是原授权 scope 的子集;不提交时沿用原 scope。

成功响应与授权码交换完全相同,仍返回新的 access_token、refresh_token、expires_in 和 scope。旧 refresh token 会立即失效,客户端必须原子替换本地保存的 token。

token endpoint 的错误响应为 OAuth 协议格式。例如授权码已使用、PKCE verifier 错误或 refresh token 失效时:

http
HTTP/1.1 400 Bad Request
Cache-Control: no-store
Pragma: no-cache
json
{
  "error": "invalid_grant",
  "error_description": "授权码无效或已过期"
}

其他常见错误:

json
{
  "error": "invalid_client",
  "error_description": "客户端认证失败"
}
json
{
  "error": "invalid_request",
  "error_description": "请求必须使用 form-urlencoded"
}

可能的 error 值包括 invalid_request、invalid_client、invalid_grant、invalid_scope、unsupported_grant_type 和 temporarily_unavailable。客户端应根据 error 决定处理方式,不要把 error_description 原样展示给终端用户。

2.4 Userinfo

使用 access token 获取当前授权用户的公开信息:

http
GET $KAX_ISSUER/api/oauth/userinfo
Authorization: Bearer $KAX_ACCESS_TOKEN
Accept: application/json

token 必须包含 openid 或 profile,否则返回 403。只有 token 包含 email scope 时才返回邮箱;即使用户本身有邮箱,未授权 email 时响应中的 email 也为 null。

成功响应:200 OK。

json
{
  "data": {
    "id": "user_xxx",
    "email": "user@example.com",
    "nickname": "Kax用户12345",
    "role": "user",
    "balance": 100,
    "promotional_balance": 25,
    "created_at": "2026-01-02T03:04:05.000Z"
  }
}

字段类型:id、nickname、role、created_at 为字符串;email 和 nickname 可能为 null;balance 和 promotional_balance 为数字。balance 为用户余额,promotional_balance 为活动余额总数。此响应不包含密码、邀请信息、封禁信息、JWT 或 API key 原文。

userinfo 的资源错误使用统一资源错误结构。例如缺少或无效 token:

http
HTTP/1.1 401 Unauthorized
json
{
  "error": {
    "code": "OAUTH_MISSING_TOKEN",
    "message": "缺少 OAuth token"
  }
}

scope 不足时:

http
HTTP/1.1 403 Forbidden
json
{
  "error": {
    "code": "insufficient_scope",
    "message": "OAuth token 权限不足"
  }
}

2.5 API key 列表

获取当前用户的 API key 公开信息,需要 apikeys:read scope:

http
GET $KAX_ISSUER/api/oauth/apikeys
Authorization: Bearer $KAX_ACCESS_TOKEN
Accept: application/json

成功响应:200 OK。

json
{
  "data": {
    "api_keys": [
      {
        "id": "key_xxx",
        "name": "生产环境",
        "key_prefix": "sk-abc...wxyz",
        "api_version": "v2",
        "plan": "pro",
        "multiplier": 1,
        "expires_at": null,
        "created_at": "2026-01-02T03:04:05.000Z",
        "last_used_at": "2026-01-03T04:05:06.000Z",
        "is_active": 1,
        "quota_limit": 100000,
        "quota_used": 1234,
        "supported_models": [
          {
            "id": "model_xxx",
            "name": "示例模型",
            "model_id": "provider/example-model",
            "model_family": "example",
            "endpoint": "/v1/chat/completions",
            "support_vision": 1,
            "support_streaming": 1,
            "support_cache": 1,
            "support_image_input": 1,
            "image_output_enabled": 0,
            "support_cache_write": 1,
            "support_thinking": 1,
            "upstreams": [
              {
                "id": "upstream_xxx",
                "name": "示例上游"
              }
            ]
          }
        ],
        "v2_upstream_filters": {
          "model_xxx": [
            "upstream_xxx"
          ]
        }
      }
    ]
  }
}

api_keys 可能为空数组。每个 API key 的字段如下:

字段类型说明
idstringAPI key 的 ID,用于详情路由。
namestringAPI key 名称。
key_prefixstring脱敏后的 key 前缀,不能还原完整 key。
api_versionv1 或 v2key 使用的 API 版本。
planstringkey 关联的套餐标识。
multipliernumber计费倍率。
expires_atstring 或 null过期时间;通常为 ISO 8601 字符串。
created_atstring创建时间。
last_used_atstring 或 null最近使用时间。
is_activenumber1 表示启用,0 表示停用。
quota_limitnumber 或 null配额上限。
quota_usednumber已使用配额。
supported_modelsarray当前 key 可使用的模型及能力信息。
v2_upstream_filtersobject,可选v2 key 的模型到上游 ID 列表映射;v1 key 不返回此字段。

supported_models 中每个模型的 support_vision、support_streaming、support_cache、support_image_input、support_cache_write 和 support_thinking 是数字标志,通常为 0 或 1。v2 模型可能额外返回 image_output_enabled 和 upstreams;upstreams 的每个元素只有 id 和 name。响应不会返回完整 API key、key hash 或 key_lookup。

2.6 单个 API key

按 API key ID 获取公开详情:

http
GET $KAX_ISSUER/api/oauth/apikeys/key_xxx
Authorization: Bearer $KAX_ACCESS_TOKEN
Accept: application/json

成功响应:200 OK。结构与列表中的单个元素相同,但外层是 api_key:

json
{
  "data": {
    "api_key": {
      "id": "key_xxx",
      "name": "生产环境",
      "key_prefix": "sk-abc...wxyz",
      "api_version": "v2",
      "plan": "pro",
      "multiplier": 1,
      "expires_at": null,
      "created_at": "2026-01-02T03:04:05.000Z",
      "last_used_at": null,
      "is_active": 1,
      "quota_limit": null,
      "quota_used": 0,
      "supported_models": [],
      "v2_upstream_filters": {}
    }
  }
}

API key 不存在或不属于当前用户时返回 404:

json
{
  "error": {
    "code": "OAUTH_RESOURCE_NOT_FOUND",
    "message": "API Key 不存在"
  }
}

2.7 撤销 token

撤销 access token 或 refresh token。该路由要求 confidential client 认证:

bash
curl -X POST "$KAX_ISSUER/api/oauth/revoke" \
  -u "$KAX_CLIENT_ID:$KAX_CLIENT_SECRET" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'token=$KAX_REFRESH_TOKEN' \
  --data-urlencode 'token_type_hint=refresh_token'

参数:

  • token:必填,要撤销的 opaque token。
  • token_type_hint:可选,建议填写 access_token 或 refresh_token;当前服务端对两种 token 都会尝试撤销,不依赖该 hint 判断类型。

成功时返回 200 OK 和空对象,响应带 Cache-Control: no-store 与 Pragma: no-cache:

json
{}

未知、过期、已经撤销或格式有效但不存在的 token 也返回同样的 200 空对象,客户端不能据此判断 token 是否曾经存在。认证失败或请求参数无效时返回 OAuth 协议错误,例如:

json
{
  "error": "invalid_client",
  "error_description": "客户端认证失败"
}

2.8 查看已授权 app

这是面向用户账户管理的路由,使用 Kax API 登录会话,不使用标准 OAuth Bearer token。浏览器请求示例:

http
GET $KAX_ISSUER/api/oauth/authorizations
Cookie: token=<KAX_SESSION_TOKEN>
Accept: application/json

成功响应:200 OK。

json
{
  "data": {
    "authorizations": [
      {
        "client_id": "kax_client_xxx",
        "name": "我的客户端应用",
        "scope": "openid profile email",
        "created_at": "2026-01-02T03:04:05.000Z"
      }
    ]
  }
}

authorizations 可能为空数组。每条记录只包含 client_id、app 名称 name、空格分隔的已授予 scope 和创建时间 created_at,不包含 client secret 或 token。

未登录时返回账户会话错误,而不是标准 OAuth token 错误:

http
HTTP/1.1 401 Unauthorized
json
{
  "error": "请先登录 Kax API"
}

2.9 撤销 app 授权

撤销某个 client_id 对当前用户的授权。client_id 必须进行 URL 路径编码:

http
DELETE $KAX_ISSUER/api/oauth/authorizations/kax_client_xxx
Cookie: token=<KAX_SESSION_TOKEN>
Accept: application/json

成功响应:200 OK。

json
{
  "data": {
    "revoked": true
  }
}

撤销会同时使该 app 对当前用户的 access token、refresh token 和未使用授权码失效。客户端应删除本地 token。此路由即使当前没有对应的有效授权,也返回 revoked: true,因此客户端可以按幂等操作处理。

2.10 响应结构速查

标准 OAuth 协议路由和资源路由使用两种不同的错误结构,客户端不要混用:

路由类别成功结构错误结构
discovery端点配置对象本身{ "error": { "code": "...", "message": "..." } }
authorize浏览器 302,通过回调参数返回 code 或 error直接 JSON OAuth 错误,或重定向后的 OAuth 错误
token、revoketoken 对象或 {}{ "error": "...", "error_description": "..." }
userinfo、apikeys、authorizations{ "data": { ... } }{ "error": { "code": "...", "message": "..." } };账户会话路由的未登录响应为 { "error": "..." }

任何响应中的 token、授权码、client secret 和完整 API key 都必须按敏感凭据处理,不要写入日志、URL、浏览器 localStorage 或普通 cookie。

3. 创建 OAuth app

OAuth app 由管理员在 /dashboard/admin/oauth 创建,或通过管理员接口创建:

http
POST /api/admin/oauth/apps
Content-Type: application/json
Cookie: token=<KAX_SESSION_TOKEN>
json
{
  "user_id": "KAX_USER_ID",
  "name": "我的客户端应用",
  "redirect_uris": [
    "https://client.example.com/oauth/callback"
  ]
}

实际创建响应中的 client_id 和 client_secret 必须分别记录。client_secret 只在创建时返回一次:

  • 只存放在服务端密钥管理系统中。
  • 不要提交到前端、桌面应用、移动应用或代码仓库。
  • 不要写入日志、URL、错误页面或客户端可见响应。

回调地址要求:

  • 生产环境使用 HTTPS。
  • 本地开发允许 http://localhost、http://127.0.0.1 和 http://[::1]。
  • 必须逐字匹配已登记地址,包括协议、主机、端口、路径和尾部斜杠。
  • 不允许 userinfo、密码、hash、通配符或未登记地址。

不要把一个宽泛的回调地址用于多个环境,也不要把用户输入直接拼接到 redirect_uri。管理员停用 app 后,新授权、刷新、资源访问以及该 app 的已有 token 都会失效。

4. 生成 PKCE 参数

生成 43 至 128 个字符的高熵随机 code_verifier,只在客户端的服务端 session 或等价的安全临时存储中保存。不要把 verifier 放进回调 URL。

Node.js 示例:

js
import crypto from "node:crypto";

const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
  .createHash("sha256")
  .update(codeVerifier)
  .digest("base64url");
const state = crypto.randomBytes(32).toString("base64url");

// 服务端 session 中保存 state 和 codeVerifier,并绑定当前登录流程。

必须使用:

text
code_challenge_method=S256

不要使用 plain,也不要复用 state、code_verifier 或授权码。

5. 发起授权

客户端跳转到 discovery 返回的授权端点:

http
GET https://api.example.com/api/oauth/authorize
  ?client_id=kax_client_xxx
  &redirect_uri=https%3A%2F%2Fclient.example.com%2Foauth%2Fcallback
  &response_type=code
  &scope=openid%20profile%20email%20apikeys%3Aread
  &state=RANDOM_STATE
  &code_challenge=BASE64URL_SHA256_VERIFIER
  &code_challenge_method=S256

建议使用 URL API 或等价的编码库构造查询参数,不要手写拼接未编码的 redirect_uri、scope 或 state。

Kax API 会先校验:

  • client_id 是否存在且启用。
  • redirect_uri 是否已登记。
  • response_type 是否为 code。
  • scope 是否全部受支持。
  • PKCE 参数是否有效。
  • state 是否超过允许长度。

校验通过后,Kax API 会跳转到授权确认页。授权确认页的行为是:

  • 浏览器没有登录 Kax API:先跳转登录页,登录成功后回到原授权请求。
  • 浏览器已经登录 Kax API:直接显示授权确认页。
  • 确认页显示当前登录用户昵称和本次请求的权限,不显示 client ID。
  • 用户点击“授权并继续”后,Kax API 才签发授权码。
  • 用户点击取消后,回调中返回 error=access_denied。

不要因为用户已经登录就跳过授权确认页。登录证明的是用户身份,授权确认决定的是该 app 是否获得请求的权限。

6. 处理回调

批准后,客户端回调地址会收到:

text
https://client.example.com/oauth/callback?code=kax_code_xxx&state=RANDOM_STATE

客户端必须按以下顺序处理:

  1. 读取 state,与服务端 session 中原始值使用常量时间比较。
  2. state 不匹配、缺失或 session 已过期时,立即终止流程,不交换授权码。
  3. 检查是否存在 error。access_denied 应作为用户取消处理,不要重试授权码交换。
  4. 只在服务端使用一次性 code 交换 token。
  5. 交换成功后删除 session 中的 state 和 code_verifier。

授权码只能使用一次,生命周期不超过 5 分钟。授权码不能直接当作 Bearer token,也不能放入日志、分析参数、URL 重定向链或浏览器 localStorage。

只有已经通过校验的回调地址才会收到错误回调。客户端不能依赖错误回调来校验未登记的地址。

7. 交换 token

Confidential client

服务端应用可以使用 HTTP Basic:

bash
curl -X POST "$KAX_ISSUER/api/oauth/token" \
  -u "$KAX_CLIENT_ID:$KAX_CLIENT_SECRET" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'code=kax_code_xxx' \
  --data-urlencode 'redirect_uri=https://client.example.com/oauth/callback' \
  --data-urlencode 'code_verifier=THE_SAVED_VERIFIER'

也可以使用表单参数提交 client authentication,但不要同时提交互相冲突的 Basic 和表单凭据。

Public client

public client 不提交 client_secret,但必须提交 client_id 和正确的 code_verifier:

bash
curl -X POST "$KAX_ISSUER/api/oauth/token" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'client_id=kax_client_xxx' \
  --data-urlencode 'code=kax_code_xxx' \
  --data-urlencode 'redirect_uri=https://client.example.com/oauth/callback' \
  --data-urlencode 'code_verifier=THE_SAVED_VERIFIER'

redirect_uri 必须与发起授权时的值完全一致。不要在 token 请求中重新生成 verifier,也不要重复提交已经交换过的 code。

成功响应示例:

json
{
  "access_token": "kax_at_xxx",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "kax_rt_xxx",
  "scope": "openid profile email apikeys:read"
}

响应包含 Cache-Control: no-store 和 Pragma: no-cache。客户端仍应自行避免缓存响应,并且绝不能把 token 放入 URL、日志、前端 localStorage 或不受保护的 cookie。

8. 调用资源

使用 access token 时,把它放在 HTTPS 请求的 Authorization header 中:

http
GET https://api.example.com/api/oauth/userinfo
Authorization: Bearer kax_at_xxx

scope 要求:

  • userinfo:需要 openid 或 profile;只有申请 email 时才返回邮箱。
  • API key 列表:需要 apikeys:read。
http
GET https://api.example.com/api/oauth/apikeys
Authorization: Bearer kax_at_xxx

获取单个 API key:

http
GET https://api.example.com/api/oauth/apikeys/API_KEY_ID
Authorization: Bearer kax_at_xxx

不要把 access token 放在 query string 中。遇到 401 时不要盲目重试同一个 token;先检查过期时间、Authorization header 格式和 app 是否仍启用。

9. 刷新 access token

access token 过期后使用 refresh token:

bash
curl -X POST "$KAX_ISSUER/api/oauth/token" \
  -u "$KAX_CLIENT_ID:$KAX_CLIENT_SECRET" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode 'refresh_token=kax_rt_xxx'

refresh token 使用 rotation:

  • 成功刷新后,旧 refresh token 立即失效。
  • 客户端必须原子地替换保存的新 refresh token。
  • 检测到旧 refresh token 重放时,同一 token family 会被全部撤销。
  • refresh 失败时不要无限重试;通常需要重新走授权流程。

刷新请求使用的 client authentication 必须与客户端类型匹配。当前实现的 refresh_token grant 需要有效的 client_secret,public client 不能只提交 client_id 来刷新 token。

10. 撤销授权和 token

撤销 token:

bash
curl -X POST "$KAX_ISSUER/api/oauth/revoke" \
  -u "$KAX_CLIENT_ID:$KAX_CLIENT_SECRET" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'token=kax_rt_xxx' \
  --data-urlencode 'token_type_hint=refresh_token'

撤销接口对未知、过期和已撤销 token 也返回 200,客户端不应根据响应推断 token 是否曾经存在。

用户可以查看和撤销已经授权的 app:

http
GET /api/oauth/authorizations
Authorization: Bearer <KAX_ACCESS_TOKEN>
http
DELETE /api/oauth/authorizations/{client_id}
Authorization: Bearer <KAX_ACCESS_TOKEN>

撤销 app 授权后,属于该授权的 access token、refresh token 和未使用授权码都不能继续使用。客户端应清理本地 token,并在下次需要时重新授权。

11. 错误处理

Token endpoint 使用标准 OAuth 错误结构:

json
{
  "error": "invalid_grant",
  "error_description": "授权码无效或已过期"
}

常见错误:

  • invalid_client:client authentication 失败。
  • invalid_grant:授权码、refresh token 或 PKCE verifier 无效、过期或已使用。
  • invalid_scope:请求了不支持的 scope。
  • invalid_request:缺少参数或参数组合无效。
  • unsupported_grant_type:不支持的 grant type。
  • insufficient_scope:token 的权限不足以调用资源。

不要把 error_description 原样展示给终端用户,也不要把 client secret、token、code 或 verifier 写入错误日志。可以记录错误类型、请求追踪 ID 和脱敏后的 client 标识。

12. 最容易踩的坑

  • 把 /api/oauth/authorize 当成直接返回 code 的 API。它首先会跳转到登录和授权确认页。
  • 浏览器未登录时期待直接看到授权按钮。正确流程是先登录,登录后回到原始授权请求。
  • 浏览器已登录时又自行要求客户端收集 Kax API 密码。密码只在 Kax API 登录页输入,客户端永远不应获取。
  • 把 client ID 当作权限。client ID 是应用标识;真正请求的权限来自 scope。
  • 把 code 直接作为 Bearer token。code 必须在服务端通过 token endpoint 交换。
  • 不校验 state 就接受回调。这样会产生登录 CSRF 和账号绑定错误。
  • 把 code_verifier 放在 URL 或前端公开存储中。verifier 应只保存在客户端自己的安全临时 session 中。
  • token 请求的 redirect_uri 与授权请求不完全一致。协议、主机、端口、路径和尾部斜杠都可能造成失败。
  • 将 client_secret 打包进 SPA、桌面端或移动端。无法保密的客户端应按 public client 使用 PKCE。
  • refresh 成功后继续使用旧 refresh token。rotation 要求立即替换旧 token。
  • 把 access token 放进 URL、日志、localStorage 或普通 cookie。
  • 用管理员 OAuth app 创建接口代替正常用户登录授权。创建 app 是部署/管理操作,不是终端用户授权操作。
  • 把 /api/user/oauth/* 当成标准 OAuth 资源接口。它们兼容 KAX JWT,和标准 OAuth bearer token 的生命周期、scope 和错误语义不同。

13. 安全清单

  • 全程使用 HTTPS;本地开发例外仅限已登记的 loopback 地址。
  • 每次授权使用新的 state、code_verifier 和授权请求。
  • 服务端保存 state、verifier 和 token,使用加密或受保护的 session 存储。
  • 回调先验证 state,再交换 code。
  • token 请求只在服务端执行,使用 Cache-Control: no-store。
  • access token、refresh token、授权码、client secret、verifier 和完整 API key 不进入日志。
  • 只申请业务真正需要的最小 scope。
  • 对 token、code 和 refresh token 按 opaque secret 处理,不尝试解析其内容。
  • 停用 app、撤销授权或检测到 refresh token 重放后,删除本地 token 并重新授权。

本页目录

产品

  • 模型目录
  • API 文档
  • 控制台

账户

  • 登录
  • 注册
  • 偏好设置

生态与社区

  • QQ 交流群
  • 额度商店

说明

  • 隐私政策
  • 服务条款

© 2026 KaxHub Kopilot · 大模型统一中转站

模型名称与商标归各自所有者所有