Authentication is straightforward when a user has a browser, keyboard, and screen.
A laptop can open a login page. A phone can redirect the user to an identity provider. But what happens when the device is a Smart TV, game console, CLI tool, or another device that is not well suited for entering credentials?
This is where the OAuth 2.0 Device Authorization Grant, commonly called Device Flow, becomes useful.
Instead of asking the device to handle the login itself, Device Flow lets the user authenticate on another device while the original device waits for the result.
The basic idea
The device starts the authentication process and receives a set of temporary values from the authorization server.
The user then opens a verification URL on a phone or computer, enters a short code, and completes authentication there.
The original device does not need to display or process the full login interface.
The flow looks roughly like this:
Device
|
| Device Authorization Request
v
Authorization Server
|
| device_code + user_code
v
Device
|
|----------------------> User's Browser / Phone
| |
| | Login + consent
| v
| Authorization Server
|
| Poll Token Endpoint
v
Authorization Server
|
| Access Token
v
Device
The important part is that the device and the user's browser do not have to be the same device.
What the device receives
The device first sends a request to the authorization server's device authorization endpoint.
The response typically contains values such as:
{
"device_code": "4fJ8...",
"user_code": "WDJB-MJHT",
"verification_uri": "https://example.com/device",
"expires_in": 600,
"interval": 5
}
The important fields are:
-
device_code: identifies the authentication request and is used by the device when requesting tokens. -
user_code: a short code the user enters on another device. -
verification_uri: where the user goes to authenticate. -
expires_in: how long the authorization request remains valid. -
interval: how often the device should poll the token endpoint.
The device_code and user_code serve different purposes.
The device keeps the device_code. The user normally only sees the user_code.
The user finishes authentication somewhere else
The device can now display something like:
Open:
https://example.com/device
Enter code:
WDJB-MJHT
The user opens the URL on a phone or computer, enters the code, signs in, and approves the requested access.
Nothing special has to happen on the original device while this is taking place.
It simply waits.
The device polls for the token
After receiving the device authorization response, the device starts polling the token endpoint.
For example:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=4fJ8...
&client_id=my-device
If the user has not finished authentication yet, the authorization server can return:
{
"error": "authorization_pending"
}
The device waits and tries again.
It should not continuously hammer the server. The server can also return:
{
"error": "slow_down"
}
which tells the client to increase the polling interval.
Once the user has successfully authenticated and granted access, the response contains the token:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600
}
The device can then use that access token to call the protected API.
What can happen while the device waits?
The device needs to handle more than just success and authorization_pending.
Common responses include:
| Response | Meaning |
|---|---|
authorization_pending |
The user has not finished yet |
slow_down |
Poll less frequently |
expired_token |
The device authorization request expired |
access_denied |
The user rejected the request |
This is important because Device Flow is inherently asynchronous.
The device cannot assume that authentication will finish immediately, or even finish successfully.
Why use a separate user code?
You might wonder why the user does not simply enter the long device_code.
The answer is usability.
A device_code is intended to be handled by the device and can be long or difficult to type. The user_code is designed for humans and is usually much shorter.
That separation also lets the authorization server associate the human-friendly code with the underlying authorization request without exposing the device's internal identifier as the thing the user has to type.
Security still matters
Device Flow solves an authentication-interface problem, but it does not remove the security requirements around authentication.
The verification endpoint should protect against things such as:
- brute-force attempts against short user codes
- excessive requests
- reuse of expired or already-consumed codes
- unauthorized access to the authorization request
- overly long-lived device codes
User codes should have limited lifetimes and a sufficiently large search space. Once a code has been successfully consumed, it should not remain usable.
The server also needs to handle concurrent requests safely. Two requests attempting to consume the same authorization state should not both be able to obtain authorization.
Device Flow vs. PKCE
Device Flow and Authorization Code Flow with PKCE solve related but different problems.
PKCE is useful when the client can redirect the user through a browser but needs protection against authorization-code interception.
Device Flow is useful when the device itself is not a practical place to perform the browser-based login.
A CLI application is a good example:
CLI
|
| "Go to this URL and enter this code"
v
Browser
|
| Login + consent
v
Authorization Server
|
v
CLI receives token
Trying to force a full browser login experience into a terminal would make the client unnecessarily complicated.
Device Flow moves that responsibility to a device that is actually good at it.
Where Device Flow makes sense
You will commonly see this pattern with:
- Smart TVs
- game consoles
- command-line applications
- media players
- IoT devices
- devices with limited input capabilities
- applications where the authentication device and usage device are different
The common characteristic is not necessarily weak hardware.
It is that the device is a poor place to perform interactive authentication.
A practical implementation checklist
When implementing Device Flow, make sure the client:
- stores the
device_codesecurely - displays the verification URI and user code clearly
- respects the polling interval
- handles
authorization_pendingandslow_down - stops when the request expires
- handles user denial
- securely stores the resulting access token
The authorization server should, at minimum:
- expire device authorization requests
- rate-limit verification and token endpoints
- prevent user-code guessing and abuse
- ensure authorization state cannot be reused incorrectly
- issue tokens only after the required user authorization has completed
The bigger engineering idea
Device Flow is a useful example of a broader engineering principle.
A component does not have to perform every part of a process itself.
A Smart TV is good at displaying content, but terrible at typing a password with a remote control. A CLI is excellent for commands, but not necessarily for interactive web authentication.
Instead of forcing the device to do something badly, Device Flow gives each part of the system a job it is better suited to perform:
Device β starts the request and waits
Browser β handles interactive authentication
Server β manages authorization and tokens
Device β uses the resulting access token
That separation is what makes the flow practical.
OAuth 2.0 Device Flow is therefore less about adding another authentication screen and more about moving authentication to the place where it can be performed properly.
Top comments (0)