设备流:使用 Logto 认证 (Authentication)
本指南假设你已在 Logto 控制台创建了类型为“原生应用”,并将授权流设置为设备流的应用程序。
简介
OAuth 2.0 设备授权许可(设备流)专为输入能力有限的设备设计,例如智能电视、游戏主机、CLI 工具和 IoT 设备。它允许用户在设备上启动登录流程,但在带有浏览器的其他设备(如手机或笔记本电脑)上完成认证 (Authentication)。
由于设备本身无法处理基于浏览器的登录流程,设备会显示一个短码和一个 URL。用户在另一台设备上访问该 URL,输入代码并登录。与此同时,原始设备会轮询 Logto,直到授权完成。
获取应用凭据
在 Logto 控制台中,进入你的应用详情页以获取以下凭据:
- App ID:你的应用的唯一标识符(也称为
client_id)。 - Logto endpoint:你的 Logto 授权服务器端点。你可以在 Logto 控制台的“应用详情”中找到。
对于 Logto Cloud,端点为 https://{your-tenant-id}.logto.app。
设备流应用属于公开客户端,因此不需要 App Secret。
请求设备码
通过向设备授权端点发送 POST 请求来启动设备流:
curl --request POST 'https://your.logto.endpoint/oidc/device/auth' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your-application-id' \
--data-urlencode 'scope=openid offline_access profile'
响应内容包括:
| 字段 | 描述 |
|---|---|
device_code | 你的应用在轮询令牌端点时使用的唯一代码。 |
user_code | 显示给用户,在浏览器中输入的短码。 |
verification_uri | 用户输入 user_code 的 URL。 |
verification_uri_complete | 已预填 user_code 的 URL。用户可直接访问此 URL 跳过手动输入——你可以将其展示为二维码、可点击链接或其他方式。 |
expires_in | device_code 和 user_code 的有效期(秒)。过期后应停止轮询。 |
向用户展示验证 URL
在你的设备屏幕上显示 user_code 和 verification_uri。
或者,你也可以使用已预填代码的 verification_uri_complete,用户只需确认即可。你可以选择以二维码、可点击链接等方式展示。
轮询令牌
当用户在浏览器中完成认证 (Authentication) 时,你的设备应轮询令牌端点。你的应用每次轮询请求之间应至少等待 5 秒:
curl --request POST 'https://your.logto.endpoint/oidc/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your-application-id' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
--data-urlencode 'device_code=DEVICE_CODE'
将 DEVICE_CODE 替换为设备授权响应中的 device_code。
停止轮询 的时机:
- 收到成功的令牌响应。
- 设备码响应中的
expires_in时间已过。 - 收到不可重试的错误,如
expired_token或access_denied。
令牌响应
用户批准后,响应内容包括:
| 字段 | 描述 |
|---|---|
access_token | 访问令牌。默认是一个不透明令牌 (Opaque token);当请求了 resource 时,是一个 JWT,aud 为资源 URI。 |
id_token | 包含用户身份声明 (Claims) 的 ID 令牌。仅在请求了 openid 权限 (Scope) 时返回。 |
refresh_token | 用于在无需重新认证 (Authentication) 的情况下获取新令牌。仅在请求了 offline_access 权限 (Scope) 时返回。 |
token_type | 始终为 Bearer。 |
expires_in | 令牌有效期(秒)。 |
scope | 授权服务器授予的权限 (Scopes)。 |
检查点:测试你的设备流
现在,测试你的设备流集成:
- 运行你的应用并触发设备流以获取
device_code和user_code。 - 在浏览器中打开
verification_uri并输入user_code,或直接使用verification_uri_complete跳过手动输入。 - 在浏览器中完成登录流程。
- 验证你的应用在轮询后是否收到令牌。
获取用户信息
解码 ID 令牌声明 (Claims)
令牌响应中的 id_token 是标准的 JSON Web Token (JWT)。你可以解码 Base64URL 编码的负载部分(JWT 的第二段,以 . 分隔)来访问基本的用户声明 (Claims),无需额外的网络请求。
解码后的负载包含如 sub(用户 ID)、name、email 等声明 (Claims),具体取决于请求的权限 (Scopes)。
生产环境下,你应在信任声明 (Claims) 前验证 JWT 签名。使用你的 Logto 端点的 JWKS(https://your.logto.endpoint/oidc/jwks)来验证令牌。
从 userinfo 端点获取
ID 令牌包含基于请求权限 (Scopes) 的基本声明 (Claims)。部分扩展声明(如 custom_data、identities)仅可通过 OIDC UserInfo 端点 获取:
curl --request GET 'https://your.logto.endpoint/oidc/me' \
--header 'Authorization: Bearer ACCESS_TOKEN'
将 ACCESS_TOKEN 替换为从令牌响应中获得的不透明访问令牌 (Opaque token)(不是 JWT 资源令牌)。响应为包含用户声明 (Claims) 的 JSON 对象,内容基于授予的权限 (Scopes)。
请求额外声明 (Claims)
你可能会发现 ID 令牌中缺少某些用户信息。这是因为 OAuth 2.0 和 OpenID Connect (OIDC) 遵循最小权限原则 (PoLP),而 Logto 构建于这些标准之上。
默认情况下,返回的声明(Claim)是有限的。如果你需要更多信息,可以请求额外的权限(Scope)以访问更多的声明(Claim)。
“声明(Claim)”是关于主体的断言;“权限(Scope)”是一组声明。在当前情况下,声明是关于用户的一条信息。
以下是权限(Scope)与声明(Claim)关系的非规范性示例:
“sub” 声明(Claim)表示“主体(Subject)”,即用户的唯一标识符(例如用户 ID)。
Logto SDK 将始终请求三个权限(Scope):openid、profile 和 offline_access。
如需请求更多权限 (Scopes),可在设备授权请求的 scope 参数中添加。例如,若需请求用户的邮箱和手机号:
curl --request POST 'https://your.logto.endpoint/oidc/device/auth' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your-application-id' \
--data-urlencode 'scope=openid offline_access profile email phone'
权限 (Scopes) 与声明 (Claims)
以下是支持的权限 (Scopes) 及其对应的声明 (Claims) 列表:
标准 OIDC 权限 (Scopes)
openid(默认)
| Claim name | Type | Description |
|---|---|---|
| sub | string | 用户的唯一标识符 |
profile(默认)
| Claim name | Type | Description |
|---|---|---|
| name | string | 用户的全名 |
| username | string | 用户名 |
| picture | string | 终端用户头像的 URL。该 URL 必须指向一个图片文件(例如 PNG、JPEG 或 GIF 图片文件),而不是包含图片的网页。请注意,该 URL 应专门指向适合在描述终端用户时显示的头像,而不是终端用户拍摄的任意照片。 |
| created_at | number | 终端用户创建的时间。该时间以自 Unix 纪元(1970-01-01T00:00:00Z)以来的毫秒数表示。 |
| updated_at | number | 终端用户信息最后更新时间。该时间以自 Unix 纪元(1970-01-01T00:00:00Z)以来的毫秒数表示。 |
其他 标准声明 (Claims) 包括 family_name、given_name、middle_name、nickname、preferred_username、profile、website、gender、birthdate、zoneinfo 和 locale 也会包含在 profile 权限 (Scope) 中,无需请求 userinfo 端点。与上表声明 (Claims) 不同的是,这些声明 (Claims) 仅在其值不为空时返回,而上表声明 (Claims) 的值为空时会返回 null。
与标准声明 (Claims) 不同,created_at 和 updated_at 声明 (Claims) 使用的是毫秒而不是秒。
email
| Claim name | Type | Description |
|---|---|---|
string | 用户的电子邮件地址 | |
| email_verified | boolean | 电子邮件地址是否已被验证 |
phone
| Claim name | Type | Description |
|---|---|---|
| phone_number | string | 用户的电话号码 |
| phone_number_verified | boolean | 电话号码是否已被验证 |
address
关于 address 声明 (Claim) 的详细信息,请参阅 OpenID Connect Core 1.0。
带有 (默认) 标记的权限 (Scopes) 总是由 Logto SDK 请求。当请求相应权限 (Scope) 时,标准 OIDC 权限 (Scopes) 下的声明 (Claims) 总是包含在 ID 令牌 (ID token) 中——无法关闭。
扩展权限 (Scopes)
以下权限 (Scopes) 由 Logto 扩展,并将通过 userinfo 端点 返回声明 (Claims)。这些声明 (Claims) 也可以通过 控制台 > 自定义 JWT 配置为直接包含在 ID 令牌 (ID token) 中。详见 自定义 ID 令牌 (ID token)。
custom_data
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| custom_data | object | 用户的自定义数据 |
identities
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| identities | object | 用户关联的身份 | |
| sso_identities | array | 用户关联的 SSO 身份 |
roles
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| roles | string[] | 用户的角色 (Roles) | ✅ |
urn:logto:scope:organizations
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| organizations | string[] | 用户所属的组织 (Organizations) ID | ✅ |
| organization_data | object[] | 用户所属的组织 (Organizations) 数据 |
这些组织 (Organizations) 声明 (Claims) 也可以在使用 不透明令牌 (Opaque token) 时通过 userinfo 端点获取。但不透明令牌 (Opaque tokens) 不能作为组织令牌 (Organization tokens) 用于访问组织专属资源。详见 不透明令牌 (Opaque token) 与组织 (Organizations)。
urn:logto:scope:organization_roles
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| organization_roles | string[] | 用户所属组织 (Organizations) 的角色 (Roles),格式为 <organization_id>:<role_name> | ✅ |
API 资源与组织 (Organizations)
我们建议首先阅读 🔐 基于角色的访问控制 (RBAC),以了解 Logto RBAC 的基本概念以及如何正确设置 API 资源。
请求 API 资源访问权限
如需访问特定 API 资源,在设备授权请求中添加 resource 参数:
curl --request POST 'https://your.logto.endpoint/oidc/device/auth' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your-application-id' \
--data-urlencode 'scope=openid offline_access' \
--data-urlencode 'resource=https://your-api-resource-indicator'
用户完成授权并获得刷新令牌后,你可以为该 API 资源获取 JWT 访问令牌:
curl --request POST 'https://your.logto.endpoint/oidc/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your-application-id' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=REFRESH_TOKEN' \
--data-urlencode 'resource=https://your-api-resource-indicator'
响应将包含 aud 为你的 API 资源指示器的 JWT access_token。
仅在初始设备授权请求中包含 offline_access 权限 (Scope) 时才会返回 refresh_token。Logto 使用令牌轮换,请始终存储并使用最新的 refresh_token。
获取组织令牌 (Organization tokens)
如果你对 组织 (Organizations) 不熟悉,请阅读 🏢 组织 (Organizations)(多租户) 以了解基础。
如需请求组织相关信息,在设备授权请求中添加 urn:logto:scope:organizations 权限 (Scope):
curl --request POST 'https://your.logto.endpoint/oidc/device/auth' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your-application-id' \
--data-urlencode 'scope=openid offline_access urn:logto:scope:organizations' \
--data-urlencode 'resource=urn:logto:resource:organizations'
用户登录后,你可以使用刷新令牌获取组织令牌 (Organization token):
curl --request POST 'https://your.logto.endpoint/oidc/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your-application-id' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=REFRESH_TOKEN' \
--data-urlencode 'organization_id=your-organization-id'
响应将包含限定于指定组织的访问令牌。
组织 API 资源
如需获取组织内 API 资源的访问令牌,请同时包含 resource 和 organization_id 参数:
curl --request POST 'https://your.logto.endpoint/oidc/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=your-application-id' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=REFRESH_TOKEN' \
--data-urlencode 'organization_id=your-organization-id' \
--data-urlencode 'resource=https://your-api-resource-indicator'