OIDC 与用户身份
用 RankHub 登录:发现文档、JWKS、id_token 声明与校验、userinfo,以及和通用 OIDC 库对接时的坑。
RankHub 的 OIDC 是最小子集:够你做「用 RankHub 登录」,不提供 RP 发起的登出、会话管理
(check_session)等扩展。
发现文档
GET https://auth.rankhub.cc/v1/oidc/.well-known/openid-configuration不是标准路径:通用库的自动发现会 404
标准推导是 {issuer}/.well-known/openid-configuration,而这里的 issuer 是
https://auth.rankhub.cc —— 按标准推导会请求
https://auth.rankhub.cc/.well-known/openid-configuration,返回 404。
必须显式指定发现地址:
- 支持
discoveryUrl/metadataUrl的库:填https://auth.rankhub.cc/v1/oidc/.well-known/openid-configuration; - 只接受
issuer并自己拼路径的库(不少 js/Go 客户端):改成手填端点 (authorization / token / userinfo / jwks 四条,见下表),然后把issuer单独固定成https://auth.rankhub.cc; - 最省事的做法:只把 RankHub 当「授权码 + 校验 id_token」用,不依赖任何自动发现。
| 字段 | 值 |
|---|---|
issuer | https://auth.rankhub.cc |
authorization_endpoint | https://auth.rankhub.cc/v1/oauth/authorize(协议端点,非页面) |
token_endpoint | https://auth.rankhub.cc/v1/oauth/token |
userinfo_endpoint | https://auth.rankhub.cc/v1/oidc/userinfo |
jwks_uri | https://auth.rankhub.cc/v1/oidc/jwks |
revocation_endpoint | https://auth.rankhub.cc/v1/oauth/revoke |
introspection_endpoint | https://auth.rankhub.cc/v1/oauth/introspect |
id_token_signing_alg_values_supported | ["ES256"] |
code_challenge_methods_supported | ["S256"] |
response_types_supported | ["code"] |
grant_types_supported | authorization_code、refresh_token、urn:ietf:params:oauth:grant-type:device_code |
发现文档与 JWKS 都带 Cache-Control: public, max-age=3600,可以缓存一小时;签名密钥轮换时
JWKS 会同时出现新旧 kid,按 kid 选密钥,不要按顺序取第一把。
JWKS
curl -s https://auth.rankhub.cc/v1/oidc/jwks{
"keys": [
{
"alg": "ES256",
"crv": "P-256",
"kid": "rankhub-oidc-1",
"kty": "EC",
"use": "sig",
"x": "…",
"y": "…"
}
]
}只有公开参数。注意:JWKS 端点在没有可用签名器时会返回 { "keys": [] }(不是 500),
所以验签失败要先看 JWKS 是不是空的,不要一律当成客户端 bug。
id_token 声明
| 声明 | 说明 |
|---|---|
iss | 恒为 https://auth.rankhub.cc |
sub | 用户唯一标识(RankHub 账号 id),在你的应用内作为主键使用它 |
aud | 你的 client_id(字符串,不是数组) |
iat / exp | 签发/过期;exp = iat + 3600(1 小时) |
jti | <sub>:<iat>,仅用于去重 |
name | 用户显示名;为空时服务端会填「RankHub 用户」 |
email / email_verified | 只有用户账号绑定了邮箱才出现 email 键 |
phone_number | 只有绑定了手机号才出现 |
nonce | 授权请求带了才回显 |
cnf.jkt | 恒存在:令牌绑定的 DPoP 公钥指纹;未绑定令牌的值是哨兵串 unbound(不是真实指纹) |
没有 preferred_username,没有 at_hash / c_hash(所以不要配置库去校验它们)。
校验 id_token
import { createRemoteJWKSet, jwtVerify } from 'jose';
const jwks = createRemoteJWKSet(new URL('https://auth.rankhub.cc/v1/oidc/jwks'));
const { payload } = await jwtVerify(idToken, jwks, {
issuer: 'https://auth.rankhub.cc', // 必须逐字一致
audience: 'your-client-id', // 你的 client_id
algorithms: ['ES256'], // 别放开成 ["none"] 或 HS256
});
if (payload.nonce !== expectedNonce) throw new Error('nonce mismatch');必查项:签名算法白名单(只允许 ES256)、iss、aud、exp(允许少量时钟偏差)、
nonce(带了就必须比对)。cnf.jkt 只在你要做 DPoP 绑定时才有意义,普通 Web 应用忽略它。
userinfo
curl -s https://auth.rankhub.cc/v1/oidc/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"sub": "…",
"name": "…",
"preferred_username": "…",
"email": "…",
"email_verified": true,
"phone_number": null,
"phone_number_verified": false
}preferred_username目前取的就是显示名name,可能与name相同或为空串。email/phone_number未绑定时是null。-
失败也是 200
令牌无效/过期时返回的仍是 HTTP 200,body 是{ "error": "invalid_token", "error_description": "The access token is invalid or expired" }。 很多 OIDC 库只在非 2xx 时才抛错,会把这种响应当成"成功的空用户"。 请显式检查 body 里有没有error字段。 - userinfo 只认
Authorization: Bearer。带 DPoP 方案(Authorization: DPoP+ proof)也支持, 但那条通道会额外要求令牌含openidscope。
scope 与资料的对应
| scope | 能拿到什么 |
|---|---|
openid | sub(+ id_token)。只要登录就至少要有它 |
profile | name、preferred_username |
email | email、email_verified(用户绑定了才有值) |
phone | phone_number、phone_number_verified |
offline_access | 语义上表示"允许离线续期";实际授权码换令牌总会拿到 refresh token |
userinfo 的 Bearer 通道不强制校验 scope,所以即使只申请了 openid 也能拿到公开资料字段。
但仍建议按需申请:用户在授权页看到的是 scope 文案,多申请会降低授权通过率。
登出
没有 end_session_endpoint,post_logout_redirect_uris 也还没有任何端点在使用。所以:
- 清掉你自己应用里的会话(cookie / 本地存储);
- 调
POST https://auth.rankhub.cc/v1/oauth/revoke,bodytoken=<refresh_token>,把令牌族撤掉 —— 响应恒为200 {}(幂等,不需要客户端认证); - 不要指望「点一下退出,RankHub 那边也退登」—— 那需要用户在 RankHub App 内操作。
与通用库对接的检查清单
- 发现地址显式配置成
/v1/oidc/.well-known/openid-configuration; - 库里
issuer固定为https://auth.rankhub.cc(不要用"发现文档里的 issuer 与其 URL 不一致"的错误兜底绕开); -
aud校验成字符串client_id(有些库默认期望数组); - 不要求
at_hash; - userinfo 显式判
error字段; - 不配置 token endpoint 的
private_key_jwt/client_secret_jwt(不支持)。