Skip to main content

Device Authorization

The OAuth 2.0 Device Authorization Grant (RFC 8628) brings OAuth to devices with internet connectivity but limited input capabilities. This flow is designed for smart TVs, streaming devices, IoT hardware, printers, remote terminal sessions, AI agents, and other connected devices where typing credentials or opening a browser isn't practical or possible. Here's how it works: the device to be authenticated displays a URL and a short code, prompting you to open that URL on your phone or computer to authorize access. After successful authorization, the device gets an access token, optionally a refresh token, and — when the openid scope is granted — an ID token. The two devices don't need to communicate directly; the authorization happens through the OAuth provider.

This document provides an overview of the Ory's device authorization grant flow, with a step-by-step example of its implementation, configuration options, and guidance on creating custom user interfaces for the verification screen.

Overview of the flow

Here is the high-level overview for the device authorization grant flow:

  1. The user attempts to log in to the device. This initiates the device to request authorization from the authorization server.
  2. When the authorization server responds, the user is instructed to visit a URL and enter the provided user code, which they do on a different device.
  3. On the different device the user visits the URL, enters the user code, logs in, and grants access to the device.
  4. In the meantime, the device polls the authorization server. Once the user authenticates and grants access, the authentication server sends an access token to the device, which is used to access the protected resource.

Step 1: Device requests authorization

The user attempts to log in through the limited input device. The device sends a POST request to the authorization server to initiate the flow with the following parameters:

  • client_id: The ID of the client (device) that's making the request
  • scope (optional): The scope of the access request, which specifies which resources the requesting device can access

The authorization server responds with the following information:

  • device_code: A unique code to identify the authorization request
  • user_code: A code the user enters at the verification URL
  • verification_uri: The URL where the user authorizes the device
  • verification_uri_complete: The URL where the user authorizes the device, with the user_code already filled in
  • expires_in: The lifespan of the device code (in seconds)
  • interval: The polling interval (in seconds) for the client to check if the user has authorized the device yet

Step 2: Display user code and verification URI

The device shows the user the user_code and verification_uri it received from the authorization server. Depending on the device, this can be in the form of a URL, QR code, acoustically, or any other form that the device can communicate with the user.

Step 3: User grants permission

The user visits the provided URI on a separate device, such as a phone. What they see there depends on which URI they opened:

  • verification_uri asks them to type the code their device shows.
  • verification_uri_complete already carries the code, for example when it was opened from a QR code. The verification screen shows the code and asks the user to check that it matches the one on their device, and lets them reject the request instead of continuing. RFC 8628 requires this confirmation step, because a code that reached the browser without being typed proves nothing about who started the flow.

The user is then prompted to log in, if not already authenticated, and grants or denies permission to the client (device). After granting permission, the user is redirected to a page confirming they are successfully logged in. Denying permission sends them to the error page configured in urls.error, and the device receives access_denied on its next poll. Rejecting the code at the verification screen ends on that same error page, but the device is not told: it keeps waiting until the code expires.

Step 4: Device polls for the access token

While the user is authorizing the device, the device polls the token endpoint of the authorization server to check whether the user has completed the authorization process by making a POST request with the following parameters:

  • client_id: The ID of the client that's making the request
  • device_code: The device code returned from the authorization request
  • grant_type: This must always be urn:ietf:params:oauth:grant-type:device_code

After the user grants their consent, the authorization server responds with the tokens:

  • access_token: The token the device uses to access the protected resource
  • refresh_token: Returned when the offline_access scope was granted, used to obtain new access tokens
  • id_token: Returned when the openid scope was requested and granted, containing the OpenID Connect claims about the authenticated user
{
"access_token": "ory_at_...",
"refresh_token": "ory_rt_...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"expires_in": 3599,
"scope": "openid offline_access",
"token_type": "bearer"
}

Polling responses

Until the user acts, each poll returns an error instead of tokens:

ErrorWhat the device should do
authorization_pendingKeep polling. The user hasn't finished authorizing yet.
slow_downIncrease the polling interval, then keep polling.
access_deniedStop polling. The user denied the request at the login or consent screen.
expired_tokenStop polling. The device code expired; start a new flow.
invalid_grantStop polling. The device code is unknown or already used.

Device codes are single-use. After a device code is exchanged for tokens, every further exchange of that code fails with invalid_grant — including a duplicate request that was already in flight. Treat invalid_grant as terminal: don't retry it. Start a new device authorization request to get a fresh device code.

Reusing a device code revokes its access and refresh tokens

Presenting a device_code again after it was exchanged revokes the access and refresh tokens issued from it. Ory can't tell whether the first or the second request came from your device, so it assumes the code leaked and withdraws those tokens.

Keep this in mind when your device retries:

  • Retry only when the token request failed. A response that timed out may still have issued tokens, and retrying with the same device_code revokes them.
  • Only the client the device_code was issued to can trigger this revocation. A request from a different client_id is rejected without touching the tokens.

Configuration options

Configure the user interface

To enable and configure the device authorization grant in Ory Hydra, adjust the following settings in your configuration file:

urls:
device:
# The verification UI is where the user inputs the user-code
verification: http://path/to/device/verification/ui
# The success UI is where the user is sent to after successful authorization
success: http://path/to/device/success

Configure user code entropy

Depending on your security needs and your traffic load, you should choose the appropriate user_code entropy. The oauth2.device_authorization.user_code.entropy_preset configuration supports 3 values:

  • high: user_code is 8 characters long and consists of alphanumeric characters, excluding some ambiguous symbols
  • medium: user_code is 8 characters long and consists of only upper case alphabetic characters
  • low: user_code is 9 characters long and consists of only numeric characters

It is also possible to configure the length and character set directly:

oauth2:
device_authorization:
user_code:
length: 8
character_set: abcdefghijklmnopqrstuvwxyz0123456789

It is important to strike the right balance between security and user experience here. Higher entropy enhances security and protects against an attacker randomly guessing valid user-codes. This is especially important when more concurrent device flows are being performed. As users will need to manually enter the user code, the higher the entropy, the more difficult it will be for the user to enter the user code. For a better user experience, ambiguous characters should be avoided, for example O and 0 on any display, or 1 and 7 on a 7-segment display. This isn't of any concern when the user doesn't need to input the user-code manually, for example when scanning a QR code.

Device verification UI implementation

When your page is opened with a user_code in the query, show that code and have the user confirm it matches the code on the device before you accept it, and give them a way to reject the request. RFC 8628 section 3.3.1 requires the confirmation: the code reached the browser without the user typing it, so it is the only step that ties the request to a device the user actually holds.

Here is a sample UI implementation for device verification:

import { Configuration, OAuth2Api } from "@ory/client"
import { Request, Response } from "express"

const ory = new OAuth2Api(
new Configuration({
basePath: `https://${process.env.ORY_PROJECT_SLUG}.projects.oryapis.com`,
accessToken: process.env.ORY_API_KEY,
}),
)

// Please note that this is an example implementation.
// In a production app, please add proper error handling.
export async function handleLogin(request: Request, response: Response) {
const challenge = request.query.device_challenge?.toString() ?? ""

// Show the form if it was not submitted. A user_code in the query came from
// verification_uri_complete, so render it for the user to confirm instead of
// asking them to type it.
if (request.method === "GET") {
response.render("device", {
challenge,
userCode: request.query.user_code?.toString() ?? "",
})
return
}

// Accept the code the user confirmed or typed, never the one from the query.
return await ory
.acceptUserCodeRequest({
deviceChallenge: challenge,
acceptDeviceUserCodeRequest: {
user_code: request.body.user_code,
},
})
.then(({ redirect_to }) => {
response.redirect(String(redirect_to))
})
}