为什么 OAuth 的 client_id 不能当秘密:一次 Device OAuth 安全加固实践
2026/7/3 5:55:31 网站建设 项目流程

家好,今天想分享一个我们在做 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 它还必须证明自己持有某把私钥

客户端每次请求时都会加一个 header:

DPoP: <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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询