如何使用OAuth2
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 获取端点,不要把生产域名硬编码到业务代码中:
GET /.well-known/oauth-authorization-server重点字段如下:
{
"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=codegrant_type=authorization_codegrant_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 或刷新 token | confidential 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 token | client authentication |
GET | /api/oauth/authorizations | 查看当前用户已授权的 app | Kax 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 和客户端认证方式:
GET $KAX_ISSUER/.well-known/oauth-authorization-server
Accept: application/json成功响应:200 OK。响应带有 Cache-Control: public, max-age=300,结构如下:
{
"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:当前只支持 PKCES256。scopes_supported:当前支持的 scope 列表。token_endpoint_auth_methods_supported:confidential client 可以使用 Basic 或表单认证;授权码交换的 public client 可以使用none,但必须有 PKCE verifier。
discovery 配置无效时可能返回 503:
{
"error": {
"code": "temporarily_unavailable",
"message": "OAuth issuer 配置无效"
}
}2.2 Authorization endpoint
授权入口是浏览器导航请求,不建议使用后端 fetch 代替浏览器跳转:
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/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浏览器未登录时,确认页会先要求登录;登录成功后回到授权确认页。用户批准后,回调地址收到:
https://client.example.com/oauth/callback?code=kax_code_xxx&state=RANDOM_STATE用户取消时,回调地址收到:
https://client.example.com/oauth/callback?error=access_denied&state=RANDOM_STATE授权路由不返回如下形式的成功 JSON,客户端不能把它当作普通 API 调用:
{
"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,成功和错误响应都带有:
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:
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:
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_id | public client 必填;confidential client 可由 Basic 提供。 |
redirect_uri | 必须与授权请求中的值完全一致。 |
code_verifier | 发起授权时生成并保存的原始 verifier,不能重新生成。 |
成功响应:200 OK。
{
"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:
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/1.1 400 Bad Request
Cache-Control: no-store
Pragma: no-cache{
"error": "invalid_grant",
"error_description": "授权码无效或已过期"
}其他常见错误:
{
"error": "invalid_client",
"error_description": "客户端认证失败"
}{
"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 获取当前授权用户的公开信息:
GET $KAX_ISSUER/api/oauth/userinfo
Authorization: Bearer $KAX_ACCESS_TOKEN
Accept: application/jsontoken 必须包含 openid 或 profile,否则返回 403。只有 token 包含 email scope 时才返回邮箱;即使用户本身有邮箱,未授权 email 时响应中的 email 也为 null。
成功响应:200 OK。
{
"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/1.1 401 Unauthorized{
"error": {
"code": "OAUTH_MISSING_TOKEN",
"message": "缺少 OAuth token"
}
}scope 不足时:
HTTP/1.1 403 Forbidden{
"error": {
"code": "insufficient_scope",
"message": "OAuth token 权限不足"
}
}2.5 API key 列表
获取当前用户的 API key 公开信息,需要 apikeys:read scope:
GET $KAX_ISSUER/api/oauth/apikeys
Authorization: Bearer $KAX_ACCESS_TOKEN
Accept: application/json成功响应:200 OK。
{
"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 的字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | API key 的 ID,用于详情路由。 |
name | string | API key 名称。 |
key_prefix | string | 脱敏后的 key 前缀,不能还原完整 key。 |
api_version | v1 或 v2 | key 使用的 API 版本。 |
plan | string | key 关联的套餐标识。 |
multiplier | number | 计费倍率。 |
expires_at | string 或 null | 过期时间;通常为 ISO 8601 字符串。 |
created_at | string | 创建时间。 |
last_used_at | string 或 null | 最近使用时间。 |
is_active | number | 1 表示启用,0 表示停用。 |
quota_limit | number 或 null | 配额上限。 |
quota_used | number | 已使用配额。 |
supported_models | array | 当前 key 可使用的模型及能力信息。 |
v2_upstream_filters | object,可选 | 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 获取公开详情:
GET $KAX_ISSUER/api/oauth/apikeys/key_xxx
Authorization: Bearer $KAX_ACCESS_TOKEN
Accept: application/json成功响应:200 OK。结构与列表中的单个元素相同,但外层是 api_key:
{
"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:
{
"error": {
"code": "OAUTH_RESOURCE_NOT_FOUND",
"message": "API Key 不存在"
}
}2.7 撤销 token
撤销 access token 或 refresh token。该路由要求 confidential client 认证:
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:
{}未知、过期、已经撤销或格式有效但不存在的 token 也返回同样的 200 空对象,客户端不能据此判断 token 是否曾经存在。认证失败或请求参数无效时返回 OAuth 协议错误,例如:
{
"error": "invalid_client",
"error_description": "客户端认证失败"
}2.8 查看已授权 app
这是面向用户账户管理的路由,使用 Kax API 登录会话,不使用标准 OAuth Bearer token。浏览器请求示例:
GET $KAX_ISSUER/api/oauth/authorizations
Cookie: token=<KAX_SESSION_TOKEN>
Accept: application/json成功响应:200 OK。
{
"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/1.1 401 Unauthorized{
"error": "请先登录 Kax API"
}2.9 撤销 app 授权
撤销某个 client_id 对当前用户的授权。client_id 必须进行 URL 路径编码:
DELETE $KAX_ISSUER/api/oauth/authorizations/kax_client_xxx
Cookie: token=<KAX_SESSION_TOKEN>
Accept: application/json成功响应:200 OK。
{
"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、revoke | token 对象或 {} | { "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 创建,或通过管理员接口创建:
POST /api/admin/oauth/apps
Content-Type: application/json
Cookie: token=<KAX_SESSION_TOKEN>{
"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 示例:
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,并绑定当前登录流程。必须使用:
code_challenge_method=S256不要使用 plain,也不要复用 state、code_verifier 或授权码。
5. 发起授权
客户端跳转到 discovery 返回的授权端点:
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. 处理回调
批准后,客户端回调地址会收到:
https://client.example.com/oauth/callback?code=kax_code_xxx&state=RANDOM_STATE客户端必须按以下顺序处理:
- 读取
state,与服务端 session 中原始值使用常量时间比较。 state不匹配、缺失或 session 已过期时,立即终止流程,不交换授权码。- 检查是否存在
error。access_denied应作为用户取消处理,不要重试授权码交换。 - 只在服务端使用一次性
code交换 token。 - 交换成功后删除 session 中的
state和code_verifier。
授权码只能使用一次,生命周期不超过 5 分钟。授权码不能直接当作 Bearer token,也不能放入日志、分析参数、URL 重定向链或浏览器 localStorage。
只有已经通过校验的回调地址才会收到错误回调。客户端不能依赖错误回调来校验未登记的地址。
7. 交换 token
Confidential client
服务端应用可以使用 HTTP Basic:
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:
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。
成功响应示例:
{
"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 中:
GET https://api.example.com/api/oauth/userinfo
Authorization: Bearer kax_at_xxxscope 要求:
userinfo:需要openid或profile;只有申请email时才返回邮箱。- API key 列表:需要
apikeys:read。
GET https://api.example.com/api/oauth/apikeys
Authorization: Bearer kax_at_xxx获取单个 API key:
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:
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:
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:
GET /api/oauth/authorizations
Authorization: Bearer <KAX_ACCESS_TOKEN>DELETE /api/oauth/authorizations/{client_id}
Authorization: Bearer <KAX_ACCESS_TOKEN>撤销 app 授权后,属于该授权的 access token、refresh token 和未使用授权码都不能继续使用。客户端应清理本地 token,并在下次需要时重新授权。
11. 错误处理
Token endpoint 使用标准 OAuth 错误结构:
{
"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 并重新授权。