BazaarLinkBazaarLink
登入
文件API 参考SDK 参考Agent 应用AI Skills
章节导航

OAuth PKCE

OAuth PKCE

OAuth 2.0 PKCE(Proof Key for Code Exchange)让公开用户端应用程式(浏览器 SPA、行动 App)能够代表个别用户呼叫 API,无需在用户端储存长效的伺服器密钥。

何时使用 OAuth PKCE

  • 建构需要每位用户计费的浏览器 SPA 或行动 App
  • 多租户 SaaS — 每位终端用户支付自己的 API 使用费用
  • 无法在用户端储存共用密钥(公开用户端)
  • 需要每位用户各自独立、可撤销的密钥(不会自动过期),而非单一共用 API 密钥

你的用户会看到什么

请在你自己的登录界面向用户说明这些步骤:

  1. 在你的应用内选择“使用 BazaarLink 登录”(或你自己命名的按钮)。
  2. 浏览器打开 BazaarLink 授权页。若尚未登录,会先登录,登录后自动回到这一页。
  3. 页面会显示你的应用网址,以及允许后会发生的事:为该应用创建一把新的 API 密钥、用量从 BazaarLink 余额扣款、可随时撤销。用户可为密钥命名并设置每月额度上限(均为选填)。
  4. 点击“允许”后回到你的应用;点击“拒绝”则不会创建密钥、你的应用也不会收到通知,用户会看到“已拒绝访问”页面。
  5. 之后若要撤销,打开“API 密钥”页面(https://bazaarlink.ai/keys),停用或删除以你的应用命名的那把密钥。

工作方式

步骤 1 — 发起认证 — 产生随机的 code_verifier(长随机字串)及其 SHA-256 杂凑值作为 code_challenge,然后将用户重新导向至 BazaarLink 认证页面。

https://bazaarlink.ai/auth?callback_url=https://yourapp.com/callback&code_challenge=CODE_CHALLENGE&code_challenge_method=S256

授权网址参数

  • callback_url(必填)— 用户点击允许后回到的网址,规则见下方。
  • code_challenge(必填)— code_verifier 的 SHA-256 再做 base64url 编码(43 字符,不含 = 补位)。
  • code_challenge_method(必填)— 必须是 S256。
  • state(选填)— 你自己的不透明防 CSRF 值,1 到 256 个可见 ASCII 字符。BazaarLink 会原样在授权码之后以 &state=… 带回。格式不正确的 state 会让授权页显示“授权请求无效”。
  • key_label(选填)— 最多 100 个字符。会预填授权页的密钥名称(用户仍可修改)。过长或空白的标签会被忽略。
  • 无回调的工具(CLI/桌面程序)— 省略 callback_url。用户允许后,页面会显示一次性授权码(5 分钟内有效、只能用一次),让用户输入到你的工具;换 key 的方式与跳转得到的授权码完全相同。页面无法显示是哪个应用在请求,请提醒用户只在自己启动的工具里输入。

不支持

  • workspace_id 与 required_workspace_id — 不支持。密钥一律属于用户自己的账号。
  • code_challenge_method=plain — 不支持(只收 S256)。

callback_url 规则

  • 必须是 https://。http:// 只接受 localhost、127.0.0.1 与 [::1](任何端口),此时授权页会显示本机开发警告。
  • 不可含片段(#),网址内不可含账号密码,最长 2048 字符。
  • 授权页会显示你的 callback 主机名,让用户看得出是哪个应用在请求授权。
  • 用户点击允许后,浏览器会被重定向到你的 callback_url 并附上 code=…(若你有带 state,也会附上 state=…;既有的查询字符串会保留)。授权码 10 分钟内有效且只能使用一次。

步骤 2 — 交换授权码取得 API 密钥 — 用户授权后,BazaarLink 会将用户重定向到您的 callback_url 并附上一次性授权码。授权码 10 分钟内有效且只能使用一次;code_verifier 错误会让它立刻作废。请 POST code、code_verifier 与 code_challenge_method(三者均必填),即可获取该用户的 API 密钥。

curl https://bazaarlink.ai/api/v1/auth/keys \
-H "Content-Type: application/json" \
-d '{
  "code": "<CODE_FROM_CALLBACK_URL>",
  "code_verifier": "<CODE_VERIFIER>",
  "code_challenge_method": "S256"
}'

# Response
# { "key": "sk-bl-...", "user_id": "..." }

步骤 3 — 使用用户专属密钥 — 返回的密钥是普通的 BazaarLink API 密钥,属于授权的那位用户 — 用量从该用户的余额扣款,不会算在你头上。

curl https://bazaarlink.ai/api/v1/chat/completions \
-H "Authorization: Bearer sk-bl-..." \
-H "Content-Type: application/json" \
-d '{"model": "google/gemini-3.5-flash", "messages": [{"role": "user", "content": "Hello"}]}'

JavaScript 示例(浏览器)

// 1) Create a verifier + S256 challenge, then send the user to BazaarLink
const b64url = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = b64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
sessionStorage.setItem("bl_verifier", verifier);
// "state" is optional: BazaarLink hands it back unchanged next to the code.
const state = b64url(crypto.getRandomValues(new Uint8Array(16)));
sessionStorage.setItem("bl_state", state);
location.href =
"https://bazaarlink.ai/auth?callback_url=" + encodeURIComponent(location.origin + "/callback") +
"&code_challenge=" + challenge + "&code_challenge_method=S256&state=" + state;

// 2) On /callback: check your own state, then exchange the one-time code for a user key
const params = new URLSearchParams(location.search);
if (params.get("state") !== sessionStorage.getItem("bl_state")) throw new Error("state mismatch");
const res = await fetch("https://bazaarlink.ai/api/v1/auth/keys", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
  code: params.get("code"),
  code_verifier: sessionStorage.getItem("bl_verifier"),
  code_challenge_method: "S256",
}),
});
if (!res.ok) throw new Error((await res.json()).error?.message ?? "exchange failed"); // 403 invalid_code: ask the user to authorize again
const { key } = await res.json(); // use as: Authorization: Bearer <key>
安全性:只支持 S256
请将 code_challenge_method 设为 S256(SHA-256 杂凑)。只支持 S256:使用 plain 或未带 code_challenge_method 的请求会被以 HTTP 400 拒绝。
注意
用户点击“拒绝”时,BazaarLink 不会跳回你的 callback_url,也不会通知你的应用,只会显示“已拒绝访问”页面,这样 /auth 不会变成开放重定向。请在你的应用内处理“未授权”状态(例如超时或重试按钮)。

/auth 页面与 POST /api/v1/auth/keys 由 https://bazaarlink.ai 提供(不是 api.bazaarlink.ai)。获取的密钥在 https://bazaarlink.ai/api/v1 与 https://api.bazaarlink.ai/v1 两个 base URL 上都可使用。

若用户在授权时设置额度上限,该上限为每月额度,按账号的月周期重置。

错误

错误格式为 OpenAI 风格 {"error":{"message","type","code"}}。POST /api/v1/auth/keys 可能返回:

  • 400 invalid_code_challenge_method — 未带 code_challenge_method、为 plain,或不是 S256。
  • 400 — JSON 内容无效,或缺少 code/code_verifier。
  • 403 invalid_code — 授权码不存在、已过期或已使用、code_verifier 错误,或该账号已不被允许。以上情况刻意返回相同信息,调用方无法区分。code_verifier 错误也会让授权码作废:请用户重新授权。
  • 429 rate_limited — 同一 IP 10 分钟内交换超过 30 次(见 Retry-After 标头)。
  • 429 key_total_cap — 该账号已持有 200 把 API 密钥的上限。
  • 405 — 只接受 POST。

从你的服务器创建授权码(管理密钥)

个人管理密钥(Management Key)的拥有者可以不经过浏览器授权页,直接用 POST /api/v1/auth/keys/code 创建授权码(与 OpenRouter 兼容)。授权码会绑定该管理密钥的拥有者,因此交换到的是该拥有者账号下的标准密钥,额度、周期、到期时间与名称都由这里决定。适用于你自己的工具已持有管理密钥、想替某个 App 发一把受限密钥的情况。交换步骤(POST /api/v1/auth/keys)与上面相同。

  • callback_url(必填)与 code_challenge + code_challenge_method=S256(必填)— 规则与授权网址相同。
  • key_label(选填)— 最多 100 个字符,密钥名称。默认为“App: <你的 callback 主机名>”。
  • limit(选填)— 美元额度上限,0 或以上;0 或未带代表不限额。
  • usage_limit_type(选填)— daily、weekly 或 monthly:额度多久重置一次。只在有 limit 时生效;默认 monthly。
  • expires_at(选填)— 密钥到期时间:未来的 ISO 8601 UTC 时间且须含秒,例如 2026-12-31T23:59:59Z。只到分钟的格式会被拒绝。未带代表密钥一直有效,直到被撤销。
  • workspace_id — 不支持(400)。spawn_agent 与 spawn_cloud 会被接受但忽略。
curl https://bazaarlink.ai/api/v1/auth/keys/code \
-H "Authorization: Bearer sk-bl-YOUR_MGMT_KEY" \
-H "Content-Type: application/json" \
-d '{
  "callback_url": "https://yourapp.com/callback",
  "code_challenge": "<CODE_CHALLENGE>",
  "code_challenge_method": "S256",
  "key_label": "My app",
  "limit": 10,
  "usage_limit_type": "monthly",
  "expires_at": "2026-12-31T23:59:59Z"
}'

# Response
# { "data": { "id": "<ONE_TIME_CODE>", "app_id": 123456789, "created_at": "2026-10-22T12:00:00.000Z" } }
# Then POST { "code": "<ONE_TIME_CODE>", "code_verifier": "<CODE_VERIFIER>", "code_challenge_method": "S256" } to /api/v1/auth/keys

响应:{ "data": { "id", "app_id", "created_at" } }。id 就是要拿去交换的一次性授权码(10 分钟内有效、只能用一次)。BazaarLink 没有 App 注册机制,app_id 是由你的 callback 地址推导出的固定整数:同一地址永远得到相同的 app_id。

  • 401 unauthorized — 没带 Bearer 密钥,或密钥不存在。
  • 403 management_key_required — 该密钥不是个人管理密钥(标准密钥与组织管理密钥都会被拒绝)。
  • 403 account_not_allowed — 该密钥的拥有者已被停权或删除。
  • 400 — invalid_callback_url、invalid_code_challenge_method、invalid_code_challenge、invalid_expires_at、workspace_not_supported,或内容格式错误。
  • 429 rate_limited — 同一拥有者每小时超过 30 个授权码,或同一 IP 每小时超过 120 个(见 Retry-After)。

你拿到的密钥

  • 一把标准的 BazaarLink API 密钥(sk-bl-…),与在“API 密钥”页面创建的相同,不会自动过期。
  • 用量从用户的 BazaarLink 余额扣款。若用户在授权页设置额度上限,该上限为每月额度。
  • 两个 base URL 都可使用:https://bazaarlink.ai/api/v1 与 https://api.bazaarlink.ai/v1。
  • 会计入账号 200 把密钥的上限,并出现在用户的“API 密钥”列表中,用户可随时停用或删除。
  • 被停权或已删除的账号无法授权应用;停权前发出的授权码在交换时会被拒绝(403 invalid_code)。

你会得到什么

  • 授权码 + PKCE 流程(公开用户端应用程式)
  • 每用户计费:各用户用自己的余额支付自己的用量
  • 每位用户一把独立密钥,列在该用户的“API 密钥”页面,用户可随时撤销
客服
您好!有什么可以协助?
请留下消息,我们会尽快回复。
OAuth PKCE | BazaarLink