Device flow: Auth with Logto
This guide assumes you have created an Application of type "Native" with device flow as the authorization flow in the Logto Console.
Introduction
The OAuth 2.0 device authorization grant (device flow) is designed for devices with limited input capabilities, such as smart TVs, game consoles, CLI tools, and IoT devices. It allows users to start the sign-in process on the device but complete authentication on a separate device with a browser, like a phone or laptop.
Since the device itself cannot handle a browser-based sign-in flow, the device displays a short code and a URL. The user visits that URL on another device, enters the code, and signs in. Meanwhile, the original device polls Logto until the authorization is complete.
Get application credentials
In your Logto Console, navigate to your application details page to get the following credentials:
- App ID: The unique identifier of your application (also known as
client_id). - Logto endpoint: Your Logto authorization server endpoint. You can find it in the Logto Console under "Application details".
For Logto Cloud, the endpoint is https://{your-tenant-id}.logto.app.
Device flow apps are public clients, so no App Secret is required.
Request a device code
Start the device flow by sending a POST request to the device authorization endpoint:
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'
The response includes:
| Field | Description |
|---|---|
device_code | A unique code for your app to use when polling the token endpoint. |
user_code | A short code to display to the user for them to enter in the browser. |
verification_uri | The URL where the user enters the user_code. |
verification_uri_complete | A URL with the user_code pre-filled. Users can visit this URL directly to skip manual code entry — you can present it as a QR code, a clickable link, or any other method. |
expires_in | The lifetime in seconds of device_code and user_code. Stop polling after this expires. |
Display the verification URL to the user
Show the user_code and verification_uri on your device screen.
Alternatively, you can use verification_uri_complete which has the code pre-filled — the user only needs to confirm. How you present it is up to you: a QR code, a clickable link, etc.
Poll for tokens
While the user completes authentication in the browser, your device should poll the token endpoint. Your app should wait at least 5 seconds between polling requests:
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'
Replace DEVICE_CODE with the device_code value from the device authorization response.
Stop polling when:
- You receive a successful token response.
- The
expires_intime from the device code response has elapsed. - You receive a non-retryable error such as
expired_tokenoraccess_denied.
Token response
After the user approves, the response includes:
| Field | Description |
|---|---|
access_token | The access token. This is an opaque string by default; when a resource is requested, it is a JWT with aud set to the resource URI. |
id_token | The ID token containing user identity claims. Only present when openid scope is requested. |
refresh_token | Used to obtain new tokens without re-authentication. Only present when offline_access scope is requested. |
token_type | Always Bearer. |
expires_in | Token lifetime in seconds. |
scope | The scopes granted by the authorization server. |
Checkpoint: Test your device flow
Now, test your device flow integration:
- Run your app and trigger the device flow to get a
device_codeanduser_code. - Open the
verification_uriin a browser and enter theuser_code, or useverification_uri_completeto skip manual code entry. - Complete the sign-in process in the browser.
- Verify that your app receives tokens after polling.
Get user information
Decode ID token claims
The id_token returned in the token response is a standard JSON Web Token (JWT). You can decode the Base64URL-encoded payload (the second part of the JWT, split by .) to access basic user claims without an additional network request.
The decoded payload contains claims like sub (user ID), name, email, etc., depending on the scopes requested.
For production use, you should validate the JWT signature before trusting its claims. Use the JWKS from your Logto endpoint (https://your.logto.endpoint/oidc/jwks) to verify the token.
Fetch from userinfo endpoint
The ID token contains basic claims based on the requested scopes. Some extended claims (like custom_data, identities) are only available via the OIDC UserInfo endpoint:
curl --request GET 'https://your.logto.endpoint/oidc/me' \
--header 'Authorization: Bearer ACCESS_TOKEN'
Replace ACCESS_TOKEN with the opaque access token (not the JWT resource token) obtained from the token response. The response is a JSON object containing the user's claims based on the granted scopes.
Request additional claims
You may find some user information is missing in the ID token. This is because OAuth 2.0 and OpenID Connect (OIDC) are designed to follow the principle of least privilege (PoLP), and Logto is built on top of these standards.
By default, limited claims are returned. If you need more information, you can request additional scopes to access more claims.
A "claim" is an assertion made about a subject; a "scope" is a group of claims. In the current case, a claim is a piece of information about the user.
Here's a non-normative example the scope - claim relationship:
The "sub" claim means "subject", which is the unique identifier of the user (i.e. user ID).
Logto SDK will always request three scopes: openid, profile, and offline_access.
To request additional scopes, include them in the scope parameter of the device authorization request. For example, to request the user's email and phone:
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 and claims
Here's the list of supported scopes and the corresponding claims:
Standard OIDC scopes
openid (default)
| Claim name | Type | Description |
|---|---|---|
| sub | string | The unique identifier of the user |
profile (default)
| Claim name | Type | Description |
|---|---|---|
| name | string | The full name of the user |
| username | string | The username of the user |
| picture | string | URL of the End-User's profile picture. This URL MUST refer to an image file (for example, a PNG, JPEG, or GIF image file), rather than to a Web page containing an image. Note that this URL SHOULD specifically reference a profile photo of the End-User suitable for displaying when describing the End-User, rather than an arbitrary photo taken by the End-User. |
| created_at | number | Time the End-User was created. The time is represented as the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z). |
| updated_at | number | Time the End-User's information was last updated. The time is represented as the number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z). |
Other standard claims include family_name, given_name, middle_name, nickname, preferred_username, profile, website, gender, birthdate, zoneinfo, and locale will be also included in the profile scope without the need for requesting the userinfo endpoint. A difference compared to the claims above is that these claims will only be returned when their values are not empty, while the claims above will return null if the values are empty.
Unlike the standard claims, the created_at and updated_at claims are using milliseconds instead of seconds.
email
| Claim name | Type | Description |
|---|---|---|
string | The email address of the user | |
| email_verified | boolean | Whether the email address has been verified |
phone
| Claim name | Type | Description |
|---|---|---|
| phone_number | string | The phone number of the user |
| phone_number_verified | boolean | Whether the phone number has been verified |
address
Please refer to the OpenID Connect Core 1.0 for the details of the address claim.
Scopes marked with (default) are always requested by the Logto SDK. Claims under standard OIDC scopes are always included in the ID token when the corresponding scope is requested — they cannot be turned off.
Extended scopes
The following scopes are extended by Logto and will return claims through the userinfo endpoint. These claims can also be configured to be included directly in the ID token through Console > Custom JWT. See Custom ID token for more details.
custom_data
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| custom_data | object | The custom data of the user |
identities
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| identities | object | The linked identities of the user | |
| sso_identities | array | The linked SSO identities of the user |
roles
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| roles | string[] | The roles of the user | ✅ |
urn:logto:scope:organizations
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| organizations | string[] | The organization IDs the user belongs to | ✅ |
| organization_data | object[] | The organization data the user belongs to |
These organization claims can also be retrieved via the userinfo endpoint when using an opaque token. However, opaque tokens cannot be used as organization tokens for accessing organization-specific resources. See Opaque token and organizations for more details.
urn:logto:scope:organization_roles
| Claim name | Type | Description | Included in ID token by default |
|---|---|---|---|
| organization_roles | string[] | The organization roles the user belongs to with the format of <organization_id>:<role_name> | ✅ |
API resources and organizations
We recommend to read 🔐 Role-Based Access Control (RBAC) first to understand the basic concepts of Logto RBAC and how to set up API resources properly.
Request access for API resources
To access a specific API resource, include the resource parameter in the device authorization request:
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'
Once the user completes authorization and you receive a refresh token, you can fetch JWT access tokens for the API resource:
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'
The response will contain a JWT access_token with aud set to your API resource indicator.
The refresh_token is only available when the offline_access scope is included in the initial device authorization request. Always store and use the latest refresh_token, as Logto uses token rotation.
Fetch organization tokens
If organizations is new to you, please read 🏢 Organizations (Multi-tenancy) to get started.
To request organization-related information, add the urn:logto:scope:organizations scope in the device authorization request:
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'
Once the user is signed in, you can fetch organization tokens using the refresh 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'
The response will contain an access token scoped to the specified organization.
Organization API resources
To fetch an access token for an API resource within an organization, include both the resource and organization_id parameters:
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'