为什么 OAuth 的 client_id 不能当秘密:一次 Device OAuth 安全加固实践

发布时间:2026/7/3 5:55:34

为什么 OAuth 的 client_id 不能当秘密:一次 Device OAuth 安全加固实践 家好今天想分享一个我们在做 OAuth Device Flow 时遇到的真实问题。Device Flow 很适合 CLI、桌面端、电视、IoT 这类不方便输入密码的场景。用户在设备上看到一个链接或验证码打开浏览器完成授权设备端再轮询 token。但我们很快遇到一个安全困扰client_id 是公开的。 别人看到以后完全可以说 我不申请自己的 client_id 了直接拿你的用。这听起来像是client_id泄露问题但本质上不是。OAuth 里的client_id本来就不是 secret。它只是应用标识不是应用身份证明。回到顶部问题在哪里原来的 Device Flow 大概是客户端 - /oauth2/device_authorization 带 client_id拿 device_code / user_code 客户端 - /oauth2/token 带 client_id device_code 轮询 token服务端能校验client_id 是否注册 scope 是否允许 device_code 是否属于 client_id 轮询 IP 是否一致这些都有价值但挡不住一个问题别人拿到 client_id 自己发起 device flow 用户完成授权 别人也能拿 token因为服务端只知道“这是某个client_id的请求”不知道“这是不是官方客户端的某个真实安装实例”。回到顶部不要把 client_secret 塞进客户端一个直觉方案是给客户端加client_secret。但这在 CLI、桌面端、移动端里基本是假安全。只要 secret 跟客户端一起发出去它迟早能被提取。混淆、加壳、硬编码都只是增加一点逆向成本不是强认证。所以我们换了个思路不要试图隐藏 client_id 而是让仅有 client_id 不够用回到顶部client instance 的想法我们引入了client_instance_id。它表示某一次安装、某台机器、某个本地运行实例。流程是1. 客户端首次启动生成一对本地密钥 2. 私钥留在本机 3. 公钥上传给服务端 4. 服务端返回 client_instance_id 5. 后续 OAuth 请求都带 client_instance_id注册接口类似POST /oauth2/client/instances/register请求里带{ client_id: xxx, public_jwk: { kty: EC, crv: P-256, x: ..., y: ... }, platform: darwin-arm64, version: 1.2.3 }服务端记录client_instance_id client_id public_jwk jkt platform version status这里的jkt是 JWK thumbprint也就是公钥指纹。回到顶部DPoP 是什么仅有client_instance_id还不够因为别人也可以伪造这个参数。所以我们配合 DPoP。DPoP 全称是 Demonstrating Proof of Possession。它解决的是请求方不只是知道一个 ID 它还必须证明自己持有某把私钥客户端每次请求时都会加一个 headerDPoP: signed-jwt这个 JWT 由客户端本地私钥签名。里面包含{ htu: https://auth.example.com/oauth2/token, htm: POST, iat: 1710000000, jti: random-id }服务端可以校验签名是否有效 htu 是否是当前 URL htm 是否是当前 method iat 是否在时间窗口内 jti 是否重放 proof 里的公钥 thumbprint 是否等于注册实例的 jkt这样client_instance_id回答你声称自己是哪个实例DPoP 回答你真的持有这个实例登记过的私钥吗回到顶部DPoP 解决什么不解决什么DPoP 很有价值但不要误解它。它能解决偷到 device_code 也不一定能 poll token 偷到 access token 也不一定能调用 API 偷到 refresh token 也不一定能刷新前提是服务端真的做了绑定和校验。但它不能单独解决别人拿你的 client_id 自己生成一把 key 然后完整发起一次新的 device flow所以 DPoP 不是“官方客户端证明”。它证明的是“私钥持有”。如果要证明这是官方客户端还需要叠加实例注册准入 版本白名单 发布签名 平台 attestation scope 分级 限流和风控回到顶部这次实践的结论这次实践给我的最大感受是client_id 不是 secret 不要把公开标识当认证更合理的做法是把风险拆开client_id 标识应用 client_instance_id 标识安装实例 DPoP 证明私钥持有 scope 和策略控制权限 灰度开关控制上线风险最终目标不是让client_id变得不可见而是让“只拿到client_id”不再足够。这就是我们这次 Device OAuth 加固的实践。后续真正打开服务端校验时关键会是三件事device_code 绑定实例 refresh_token 绑定实例 API token 绑定 DPoP proof

相关新闻