OAuth 2.0 流程
实现带 PKCE 的授权码流程,安全访问第三方 API。
为什么
OAuth 是你的应用请求权限代表用户在另一服务行动的方式(读他们的 GitHub 仓库、发 Slack 消息)。带 PKCE 的授权码流程是 web 和移动应用唯一安全流程——隐式流程已弃用因在 URL 片段泄漏 token。PKCE(代码交换证明密钥)防被截获的授权码被攻击者交换,即使在公开客户端。搞错意味着要么用户连不上账户,要么更糟,攻击者劫持 OAuth 流程获其在第三方服务的数据访问。
何时使用
应用需代表用户调第三方 API 时。用库约 2-4 小时。仅需 SSO 可跳过(直接用认证提供方)。
如何做
1. 在提供方注册应用获 client_id + client_secret - 为每个环境配重定向 URI(无通配符) - secret 存 env,永不进客户端 bundle 2. 生成 PKCE code_verifier(43-128 随机字符)和 code_challenge - code_challenge = base64url(sha256(code_verifier))——S256 方法 - verifier 存会话以便令牌交换时可用 3. 重定向用户到提供方 authorize 端点 - 含:response_type=code、client_id、redirect_uri、scope、state、code_challenge、code_challenge_method=S256 - state 是返回时验证的随机串(防 CSRF) 4. 处理重定向回来:用 code + verifier 换 access_token + refresh_token - POST 到 token 端点带 grant_type=authorization_code - 验证 state 匹配所发;不匹配则中止 5. 用 access_token 调 API;过期时刷新 - 令牌加密存 DB(web 永不存 localStorage) - 过期 80% 时主动刷新避免请求中 401
示例
import { randomBytes, createHash } from "crypto";
// 步骤 2:PKCE
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
session.set("pkce_verifier", verifier);
// 步骤 3:重定向到 authorize
const authUrl = new URL("https://github.com/login/oauth/authorize");
authUrl.searchParams.set("client_id", env.GH_CLIENT_ID);
authUrl.searchParams.set("redirect_uri", `${env.URL}/api/oauth/callback`);
authUrl.searchParams.set("scope", "repo read:user");
authUrl.searchParams.set("state", session.get("oauth_state"));
authUrl.searchParams.set("code_challenge", challenge);
authUrl.searchParams.set("code_challenge_method", "S256");
redirect(authUrl.toString());
// 步骤 4:用 code 换 token
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json" },
body: JSON.stringify({ client_id, client_secret, code, code_verifier: verifier, redirect_uri }),
});常见问题
- ⚠用隐式流程(response_type=token)因步骤少——它在 URL 栏泄漏 token。
- ⚠不验证 state 参数,攻击者可发起流程并注入自己的 code。
- ⚠access token 存 localStorage,任何 XSS 脚本可读——用 httpOnly cookie 或服务端会话。
- ⚠忘刷新 token,集成在 1 小时后 access token 过期时坏。
最佳实践
- ✓总用 PKCE,即使服务端应用——5 行加法防 code 截获攻击。
- ✓请求最小所需 scope,让用户看到确切授予的权限。
- ✓优雅处理'用户拒绝同意'——不报错,显示页解释无连接不能做什么。
- ✓用库(lucia-auth、oslo 或提供方 SDK)——手写 OAuth 太多坑难做对。