Skip to content

第14篇:服务与集成 · 第6节 OAuth 2.0 + PKCE — 认证与 Token 管理

控制台类应用常通过 OAuth 2.0 授权码 + PKCE 获取访问令牌,浏览器完成 consent(如 consent.anthropic.com 教学名),CLI 监听 localhost 回调或使用 设备码流(视产品)。本节讲流程、Token 存储与刷新


学习目标

能力项说明
流程描述授权码 + PKCE:code_verifier / code_challenge / redirect
安全为何 CLI 需要 PKCE;state 防 CSRF
存储refresh token 加密-at-rest、文件权限
刷新在 401 前 proactive refresh vs reactive
撤销登出与 token 作废的产品语义

生活类比:酒店前台临时房卡

办理入住(授权)时,前台给你一张限时房卡access token),同时系统在后台给你延期资格refresh token)。房卡丢了要补办刷新),离店注销revoke)。PKCE 像是当场对暗号:即使有人偷听对话,也拿不到能开门的完整密钥。


PKCE 参数生成(示意)

typescript
// oauth/pkce.ts — 教学示意
import crypto from "node:crypto";

export function generatePkcePair() {
  const verifier = base64Url(crypto.randomBytes(32));
  const challenge = base64Url(
    crypto.createHash("sha256").update(verifier).digest()
  );
  return { code_verifier: verifier, code_challenge: challenge };
}

function base64Url(buf: Buffer): string {
  return buf
    .toString("base64")
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

授权 URL 组装

typescript
export function buildAuthorizeUrl(opts: {
  base: string;
  clientId: string;
  redirectUri: string;
  scope: string;
  state: string;
  codeChallenge: string;
}) {
  const u = new URL("/oauth/authorize", opts.base);
  u.searchParams.set("response_type", "code");
  u.searchParams.set("client_id", opts.clientId);
  u.searchParams.set("redirect_uri", opts.redirectUri);
  u.searchParams.set("scope", opts.scope);
  u.searchParams.set("state", opts.state);
  u.searchParams.set("code_challenge", opts.codeChallenge);
  u.searchParams.set("code_challenge_method", "S256");
  return u.toString();
}

用 code 换 token

typescript
export async function exchangeCode(params: {
  tokenEndpoint: string;
  clientId: string;
  code: string;
  redirectUri: string;
  codeVerifier: string;
}): Promise<{ access_token: string; refresh_token?: string; expires_in?: number }> {
  const body = new URLSearchParams({
    grant_type: "authorization_code",
    code: params.code,
    redirect_uri: params.redirectUri,
    client_id: params.clientId,
    code_verifier: params.codeVerifier,
  });
  const res = await fetch(params.tokenEndpoint, {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body,
  });
  if (!res.ok) throw new Error(`token ${res.status}`);
  return res.json();
}

Mermaid:授权码 + PKCE

图2:401 时刷新


Token 持久化

字段存储建议
access_token内存优先;落盘则加密
refresh_token必须限制权限位;加密
expires_at计算绝对时间,提前刷新
typescript
export async function saveTokenBundle(
  filePath: string,
  bundle: object,
  key: Buffer
) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
  const pt = Buffer.from(JSON.stringify(bundle), "utf8");
  const enc = Buffer.concat([cipher.update(pt), cipher.final()]);
  const tag = cipher.getAuthTag();
  const payload = { iv: iv.toString("base64"), tag: tag.toString("base64"), enc: enc.toString("base64") };
  await fs.writeFile(filePath, JSON.stringify(payload), { mode: 0o600 });
}

概念说明
consent UI用户显式同意 scope
scope 最小化仅申请必要权限
state随机串,回调时校验

与错误处理(第2节)衔接

事件映射
invalid_grantauth + 重新登录
token 刷新网络错误network + 退避

表:localhost 回调 vs 设备码

模式优点缺点
localhostUX 顺滑端口冲突/无浏览器环境
设备码适合 SSH 远程步骤多

小结

OAuth 2.0 + PKCE 让 CLI 安全代用户调用 API:code_verifier 保密、challenge 公开;token 加密存储刷新401 路径打通 consent 与 API 客户端。


自测

  1. 为何隐式流(implicit)不适合现代 CLI?
  2. state 若省略会怎样?
  3. refresh 成功后是否要重试原请求而非让用户重试?

上一节05-lsp.md · 下一节07-feature-flags.md

本项目仅用于教育学习目的。Claude Code 源码版权归 Anthropic, PBC 所有。