RankHub

授权码 + PKCE

标准授权码流程:把用户送到托管同意页、拿回 code、换令牌。含参数表、可复制的代码与全部失败分支。

这是 RankHub 的主流程:Web 应用、SPA、移动端、桌面端都用它。

浏览器打开哪一条授权地址?

第三方应该用托管同意页 https://auth.rankhub.cc/oauth/authorize。

https://auth.rankhub.cc/v1/oauth/authorize 是协议端点:它要求调用方在 auth 域已有登录会话 (没有就 401 Missing session cookie),并且返回的是 { "redirectUrl": "…" } 这样的 JSON, 而不是页面。把它当登录页用会得到一段 JSON。

第一步:把用户送到同意页

GET https://auth.rankhub.cc/oauth/authorize
  ?client_id=your-client-id
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback
  &response_type=code
  &scope=openid%20profile
  &state=<随机串>
  &nonce=<随机串>
  &code_challenge=<BASE64URL(SHA256(verifier))>
  &code_challenge_method=S256
参数必填说明
client_id是开发者平台里的 Client ID;应用必须已过审且启用,否则 invalid_client
redirect_uri是必须与注册值逐字一致
response_type是只接受 code
scope否空格分隔;缺省 openid profile;必须是「默认 scope ∪ 注册 scope」的子集
state强烈建议原样回写,用来防 CSRF / 对应请求;内嵌模式必填
nonce建议回写进 id_token 的 nonce,用来防重放
code_challenge公开客户端必填BASE64URL(SHA256(verifier)),不带 padding
code_challenge_method公开客户端必填只能是 S256(plain 不支持)
prompt=none可选静默尝试:有 auth 域会话就直接回码,没有就回 error=login_required。不要依赖它做登录态保持

同意页对用户显示:应用名、Logo、主页,以及每条 scope 的中文说明(身份标识 / 基本资料 / 邮箱 / 手机号 / 离线访问 / 游戏资料 …)。用户批准后浏览器被送回你的 redirect_uri。

# 批准
https://app.example.com/auth/callback?code=<一次性授权码>&state=<原样回写>

# 拒绝
https://app.example.com/auth/callback?error=access_denied&state=<原样回写>

# 静默模式且没有可用登录态
https://app.example.com/auth/callback?error=login_required&state=<原样回写>

第二步:生成 PKCE 与 state

// 浏览器 / Node 18+ 都可用(Web Crypto)
const b64url = (bytes: Uint8Array) =>
  btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

export async function createPkce() {
  const verifier = b64url(crypto.getRandomValues(new Uint8Array(48)));   // 43~128 字符
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
  const challenge = b64url(new Uint8Array(digest));
  const state = b64url(crypto.getRandomValues(new Uint8Array(24)));
  const nonce = b64url(crypto.getRandomValues(new Uint8Array(24)));
  return { verifier, challenge, state, nonce };
}

把 verifier / state / nonce 存在发起授权的那一侧(浏览器 sessionStorage、后端会话、 移动端安全存储),回调时用它们做校验。verifier 绝不能出现在授权请求里。

第三步:换令牌

curl -sS https://auth.rankhub.cc/v1/oauth/token \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code' \
  -d 'code=<回调里的 code>' \
  -d 'redirect_uri=https://app.example.com/auth/callback' \
  -d 'client_id=your-client-id' \
  -d 'code_verifier=<第一步的 verifier>'

机密客户端再加认证(二选一,不要同时用):

# 方式 A:HTTP Basic(client_id / client_secret 都要 URL 编码)
curl -sS https://auth.rankhub.cc/v1/oauth/token \
  -u 'your-client-id:your-client-secret' \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d 'grant_type=authorization_code&code=…&redirect_uri=…&code_verifier=…'

# 方式 B:放进 body
curl -sS https://auth.rankhub.cc/v1/oauth/token \
  -d 'grant_type=authorization_code&code=…&redirect_uri=…&client_id=…&client_secret=…&code_verifier=…'

请求体可以是 application/x-www-form-urlencoded 也可以是 JSON,字段名一样。

成功响应(协议形状,不套 RankHub 的 v1 信封):

{
  "access_token": "at_…",
  "token_type": "Bearer",
  "expires_in": 600,
  "scope": "openid profile",
  "refresh_token": "rt_…",
  "id_token": "eyJhbGciOiJFUzI1NiJ9.…"
}
字段说明
access_token不透明串,不是 JWT(at_ 前缀)。不要尝试本地解析
token_typeBearer(未绑定)或 DPoP(绑定了 DPoP 公钥)。按它决定怎么带令牌
expires_inaccess token 剩余秒数(默认 600)。读它,不要硬编码
scope实际授予的 scope(空格分隔),可能与请求略有出入
refresh_token授权码换令牌总会下发(不管有没有 offline_access),每次都轮换
id_token仅当 scope 含 openid 时出现;JWS,默认 ES256
cnf只在 token_type=DPoP 时出现:{ "jkt": "<公钥指纹>" }

令牌能调什么,不只由 scope 决定

游戏数据只能经 /v1 访问,全部在 /v1/games/**(旧的 /v0 游戏面只服务 RankHub 自有客户端)。 授予了 game:write 也不等于能调导入 / 同步 / 任务面 —— 那些形状对第三方一律 403 game.firstPartyOnly。边界见权限范围与可用 API。

浏览器只能走公开客户端

client_secret 一旦进了浏览器就等于泄露。前端(SPA)请注册成 公开客户端(none), 用 PKCE + JS SDK 或自己实现;机密客户端只允许在服务端换令牌。

回调页要做的四件事

校验 state

与发起时保存的 state 严格比较,不一致直接中止(不要继续换令牌)。

处理 error

error=access_denied 是用户拒绝(提示后可重试);error=login_required 只在 prompt=none 静默尝试时出现,属于正常结果,别弹错误框。

换令牌并校验 nonce

拿到 id_token 后校验 iss / aud / exp,以及 nonce 是否回显(见 OIDC)。

立刻把 code 从地址栏去掉

history.replaceState 抹掉 ?code=…,避免被复制分享或进历史记录。

常见失败与原因

现象原因
同意页直接报「应用不可用 / invalid_client」应用没通过审核、被停用,或 client_id 写错
授权页 400 redirect_uri is not registered for this client回调地址与注册值不完全一致(斜杠、大小写、端口、末尾参数)
授权页 400 PKCE S256 code_challenge is required公开客户端没带 code_challenge,或用了 plain
换令牌 400 Invalid or already used authorization codecode 用过一次、超过 10 分钟、或换了 redirect_uri
换令牌 400 redirect_uri mismatch换令牌时的 redirect_uri 与授权时不同(必须逐字一致)
换令牌 400 code is bound to a different DPoP key授权时用了 DPoP、换令牌时没带同一把 key 的 proof
换令牌 401 invalid_clientsecret 错、认证方式与注册的不一致、或 Basic 与 body 同时传

错误响应恒为两键:{ "error": "…", "error_description": "…" }。完整清单见错误与排障。

下一步

本页内容