OAuth2 Device Authorization Flow 深度解析

OAuth2 Device Authorization Flow 深度解析

RFC 8628 · 适用于无浏览器或输入受限设备的授权流程


一、为什么需要 Device Flow

标准的 Authorization Code Flow 依赖浏览器重定向,用户需要在授权页面输入账号密码并点击同意。但对于以下场景,这套流程完全行不通:

  • 智能电视 / 流媒体盒子:有屏幕,但没有键盘,无法输入复杂的 URL 和密码
  • CLI 工具(如 gh auth login、AWS CLI SSO):运行在终端,无法弹出浏览器
  • IoT 设备:资源受限,可能没有显示屏,更没有浏览器
  • 游戏主机:手柄操作输入极不方便

Device Flow 的核心思路是把**“需要授权的设备""完成授权的浏览器”**彻底解耦——设备只负责展示一个短码并轮询,用户用另一台有浏览器的手机或 PC 完成授权。

💡 类比:你在电影院的自助取票机(受限设备)上扫码,用手机(有浏览器的设备)完成支付和身份核验,取票机等待确认后吐出票。取票机全程不需要输入密码,也不需要有屏幕键盘。


二、完整授权流程

角色说明

角色说明
受限设备TV、CLI、IoT 等,发起授权请求并轮询
授权服务器颁发 device_code / user_code,验证用户授权
用户使用有浏览器的手机或 PC 完成授权

时序图

受限设备                    授权服务器                    用户(手机/PC)
   │                            │                              │
   │── ① POST /device_authorization ──────────────────────────>│
   │     client_id, scope       │                              │
   │                            │                              │
   │<── ② device_code, user_code, verification_uri, expires_in, interval ──│
   │                            │                              │
   │ ┌─────────────────────────────────────────────────────┐   │
   │ │ ③ 设备展示给用户:                                   │   │
   │ │    请访问:https://example.com/device               │   │
   │ │    输入代码:BDFH-JLNP                              │   │
   │ └─────────────────────────────────────────────────────┘   │
   │                            │                              │
   │                            │<── ④ 用户打开 verification_uri ──│
   │                            │<── ⑤ 输入 user_code 并登录授权 ──│
   │                            │                              │
   │── ⑥ 轮询 POST /token ──────>│                              │
   │     device_code            │  (用户尚未完成)               │
   │<── authorization_pending ──│                              │
   │                            │                              │
   │  ... 按 interval 继续轮询 ...│                              │
   │                            │                              │
   │── ⑧ 再次轮询 POST /token ──>│                              │
   │<── ⑨ access_token ─────────│                              │
   │        refresh_token       │                              │
   │                            │                              │
   │  ✓ 授权完成                │                              │

分步说明

第 ① 步:设备请求授权

设备向授权服务器的 /device_authorization 端点发起 POST 请求:

POST /device_authorization HTTP/1.1
Content-Type: application/x-www-form-urlencoded

client_id=your_client_id&scope=read:profile

第 ② 步:获取 device_code 和 user_code

授权服务器返回:

{
  "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
  "user_code": "BDFH-JLNP",
  "verification_uri": "https://example.com/device",
  "verification_uri_complete": "https://example.com/device?user_code=BDFH-JLNP",
  "expires_in": 900,
  "interval": 5
}

第 ③ 步:设备展示短码

设备将 user_codeverification_uri 展示给用户,可以是屏幕文字、二维码或终端输出。

第 ④⑤ 步:用户在另一台设备上完成授权

用户用手机或 PC 打开 verification_uri,输入 user_code,完成登录和授权确认。

第 ⑥⑦ 步:设备轮询(等待中)

设备按 interval 秒间隔持续请求 /token

POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&client_id=your_client_id

在用户完成授权前,服务器返回 authorization_pending,设备继续等待。

第 ⑧⑨ 步:轮询成功,获得 Token

用户授权完成后,下次轮询即可获得 Token:

{
  "access_token": "eyJhbGciOiJSUzI1NiJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
  "scope": "read:profile"
}

三、关键参数说明

参数归属方说明
device_code设备持有轮询时使用,用户不可见,高熵随机字符串
user_code展示给用户短码,设计为易于手动输入(通常 8 位,含连字符)
verification_uri展示给用户用户在浏览器中打开的链接
verification_uri_complete可选,展示给用户附带 user_code 的完整链接,可生成二维码,减少输入错误
interval设备遵守轮询间隔秒数,必须严格遵守,不得低于此值
expires_in设备遵守device_code 和 user_code 的有效期(秒)

四、轮询错误码处理

错误码含义处理方式
authorization_pending用户尚未完成授权继续按 interval 轮询,不做任何改变
slow_down轮询过于频繁interval += 5 秒后继续轮询
expired_tokendevice_code 已过期终止当前流程,重新发起(重新请求 device_code)
access_denied用户明确拒绝授权终止,向用户提示授权被拒绝

⚠️ 注意slow_down 不是错误,是服务端在保护自己;收到时必须增加间隔,否则可能被封禁。


五、Python 完整示例

import time
import requests

CLIENT_ID   = "your_client_id"
DEVICE_AUTH = "https://example.com/oauth/device_authorization"
TOKEN_URL   = "https://example.com/oauth/token"

# ① 请求 device_code 和 user_code
resp = requests.post(DEVICE_AUTH, data={
    "client_id": CLIENT_ID,
    "scope": "read:profile",
})
data = resp.json()

device_code      = data["device_code"]
user_code        = data["user_code"]
verification_uri = data["verification_uri"]
interval         = data.get("interval", 5)   # 默认 5 秒
expires_in       = data["expires_in"]

# ② 展示给用户(电视屏幕 / 终端输出)
print(f"请在浏览器打开:{verification_uri}")
print(f"并输入代码:    {user_code}")
print(f"(代码 {expires_in} 秒内有效)")

# ③ 轮询直到成功或超时
deadline = time.time() + expires_in

while time.time() < deadline:
    time.sleep(interval)

    token_resp = requests.post(TOKEN_URL, data={
        "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
        "device_code": device_code,
        "client_id":   CLIENT_ID,
    })
    token_data = token_resp.json()

    if "access_token" in token_data:
        print("✓ 授权成功!")
        print(f"  access_token:  {token_data['access_token']}")
        print(f"  refresh_token: {token_data.get('refresh_token', '—')}")
        break

    error = token_data.get("error")

    if error == "authorization_pending":
        print("  等待用户授权...")          # 继续轮询

    elif error == "slow_down":
        interval += 5                       # 降低频率
        print(f"  降速,新间隔 {interval}s")

    elif error == "expired_token":
        print("✗ 代码已过期,请重新发起授权流程")
        break

    elif error == "access_denied":
        print("✗ 用户拒绝授权")
        break

    else:
        print(f"✗ 未知错误:{token_data}")
        break
else:
    print("✗ 超时,用户未在有效期内完成授权")

六、安全注意事项

客户端(设备侧)

  • user_code 有效期建议 ≤ 15 分钟,过期后需重新发起整个流程
  • 严格遵守 interval,收到 slow_down 须立即加大间隔,不得继续原频率
  • 优先使用 verification_uri_complete(可生成二维码),减少用户手动输入
  • Token 获取后写入安全存储(Keychain / Keystore / OS 凭证管理器),不可明文存入文件

服务端(授权服务器侧)

  • 对每个 device_code 独立做速率限制,防止暴力轮询探测
  • user_code 应设计为大小写不敏感,去除易混淆字符(0 vs O1 vs I vs l
  • 建议在授权页面展示设备信息(IP、请求时间、scope),帮助用户辨别合法请求

七、与其他流程的对比

对比维度Authorization Code + PKCEClient CredentialsDevice Flow
是否需要浏览器✅ 需要❌ 不需要❌ 不需要(另一台设备完成)
是否有用户参与✅ 有❌ 无✅ 有(在另一台设备上)
适用端Web / 移动 App后端服务TV / CLI / IoT
获得 Refresh Token✅ 是❌ 通常无✅ 是
规范出处RFC 6749RFC 6749RFC 8628

八、真实案例

产品 / 工具Device Flow 的具体体现
GitHub CLIgh auth login → 终端输出 code → 浏览器打开 github.com/login/device
Google TV电视屏幕显示 8 位码 → 手机打开 google.com/device 输入
VS CodeRemote 插件授权 → 浏览器完成 GitHub / Microsoft 登录
AWS CLIaws sso login → 浏览器完成 SSO 授权 → CLI 自动获取临时凭证
Spotify游戏主机、智能音箱登录 Spotify 账号

小结

Device Flow 的精妙之处在于对"授权者"和"被授权设备"的解耦:

  • device_code:设备和服务器之间的凭证,用户不可见
  • user_code:人类可读的短码,连接"哪台设备在请求授权"和"用户的授权行为"
  • 轮询机制:设备无需监听回调,主动拉取结果,天然适合无公网地址的设备

2025 年选型建议:TV / 流媒体 / 游戏主机 / IoT 硬件,以及任何"有输出但输入受限"的客户端,Device Flow 是唯一合适的选择。CLI 工具也应优先考虑 Device Flow 而非密码模式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值