How a command-line tool or smart TV authenticates securely without its own browser or comfortable text input
The classic OAuth2 Authorization Code Flow assumes the client can open a browser and receive a redirect, which practically doesn't work for a CLI tool, a smart TV, or an IoT device without an on-screen keyboard. The OAuth2 Device Authorization Grant, specified in RFC 8628, solves this problem by having the user perform authorization on a separate device with a full-featured browser, while the original device waits in the background for the result.
Table of Contents
- 1. Why classic OAuth2 doesn't work for headless clients
- 2. The device flow step by step
- 3. User code design: short, unambiguous, typo-resistant
- 4. Correctly respecting polling intervals
- 5. Device code expiration and timeout behavior
- 6. Typical use cases beyond CLI tools
- 7. Security aspects to consider during implementation
- 8. Managing the token lifecycle after a successful device flow
- 9. OAuth2 flows at a glance
- 10. Summary
- 11. FAQ
1. Why classic OAuth2 doesn't work for headless clients
The standard Authorization Code Flow requires the client to register a redirect URI, to which the authorization server redirects the user after successful login, which assumes the client itself can control an embedded or native browser and receive the redirect. A CLI tool running on a server without a graphical interface, a smart TV with limited remote-control text input, or an IoT device without any screen at all fundamentally can't meet this requirement.
Earlier, insecure workarounds like directly asking for username and password within the CLI tool itself (Resource Owner Password Credentials Grant) are problematic for several reasons: the tool has to handle the user's actual credentials, two-factor authentication is barely representable through it, and the user has to blindly trust that the tool won't misuse or forward the credentials.
2. The device flow step by step
The client (say, a CLI tool) first requests a device code and a short, easily human-typeable user code from the authorization server, together with a verification URL. The tool displays this URL and the user code directly in the terminal (say, "Open https://example.com/device and enter code ABCD-1234"), while itself periodically polling the authorization server in the background to check whether authorization has completed yet.
The user opens the displayed URL on any other device with a full-featured browser (typically their own smartphone or laptop), logs in normally there, enters the short user code, and confirms the authorization. Once this confirmation happens, the originally waiting CLI tool receives the actual access and refresh tokens on its next polling request and can proceed normally.
<?php
declare(strict_types=1);
final class DeviceAuthorizationController
{
public function requestDeviceCode(): array
{
$deviceCode = bin2hex(random_bytes(32));
$userCode = strtoupper(bin2hex(random_bytes(4)));
$this->deviceCodeRepository->store($deviceCode, $userCode, [
'status' => 'pending',
'expiresAt' => new \DateTimeImmutable('+10 minutes'),
]);
return [
'device_code' => $deviceCode,
'user_code' => $userCode,
'verification_uri' => 'https://example.com/device',
'expires_in' => 600,
'interval' => 5,
];
}
public function pollToken(string $deviceCode): array
{
$entry = $this->deviceCodeRepository->find($deviceCode);
if ($entry === null || $entry->isExpired()) {
return ['error' => 'expired_token'];
}
if ($entry->status === 'pending') {
return ['error' => 'authorization_pending'];
}
return $this->tokenService->issueTokens($entry->userId);
}
}
3. User code design: short, unambiguous, typo-resistant
The user code needs to be readable off a screen and typed on a different keyboard by humans without errors, which is why design decisions like omitting easily confused characters (0 and O, 1 and I and l) and grouping into short blocks with a separator (say, ABCD-1234 instead of ABCD1234) significantly reduce the error rate during manual entry. RFC 8628 explicitly recommends a restricted character set for exactly this reason.
The length of the user code is a compromise between security (too short a code can potentially be guessed or brute-forced) and usability (too long a code is tedious and error-prone to type). An eight-character code from a restricted, unambiguous character set is a common, battle-tested compromise between these two requirements.
4. Correctly respecting polling intervals
The authorization server returns an interval field in its initial response, telling the client at most how often it may poll the token endpoint without being rejected for being too aggressive. A client that ignores this interval and polls too frequently risks a slow_down error response, forcing it to increase the interval going forward, instead of continuing to poll at the original, too-short cadence.
This rate limiting protects the authorization server from excessive load caused by many concurrently waiting device flow clients, but also makes sense from a user perspective: a user who deliberately takes time to open the verification URL on another device creates no real need for per-second polling during that time anyway.
5. Device code expiration and timeout behavior
The device code itself has a limited validity period (usually 10 to 15 minutes, communicated in the expires_in field of the initial response), after which the authorization process finally fails, even if the user hasn't visited the verification URL yet. The client should visibly display this expiration time to the user in the terminal and issue a clear error message once it expires, instead of polling forever.
This limited validity is a deliberate security measure: an indefinitely valid device code would enlarge the window for an attacker trying to use an intercepted or guessed user code for their own, fraudulent authorization, while the actual user has long since forgotten about the process.
6. Typical use cases beyond CLI tools
Beyond classic command-line tools, the device flow is the standard approach for smart TV apps (Netflix, YouTube, and similar services use it for initial device linking), for IoT devices with very limited or no input capability, and for development tools that need to authenticate against a cloud API without embedding a full-featured browser themselves (such as GitHub's CLI, gh auth login).
The shared characteristic of all these scenarios is a device with limited input or browser capabilities, combined with the assumption that the user simultaneously has access to a second, fully equipped device, which is a reasonable, practical assumption for the vast majority of real usage scenarios.
7. Security aspects to consider during implementation
The verification page, where the user enters the user code, should clearly inform the user about the nature of the authorization (which device, which application, which requested permissions), so a user can recognize and reject a malicious or accidental request, instead of blindly confirming every authorization request. This transparency matters especially because the device flow is structurally vulnerable to phishing, if an attacker tricks a user into confirming a device code the attacker controls on the legitimate verification page.
The authorization server should additionally limit the number of failed user code entry attempts, to prevent brute-force attacks on the comparatively short, human-readable code, analogous to rate limiting on classic login forms.
8. Managing the token lifecycle after a successful device flow
After completing the device flow, the client receives the same access and refresh tokens as with any other OAuth2 grant type, which is why subsequent token management (expiration, refresh, revocation) doesn't differ from the standard flow. A CLI tool should securely store the received refresh token in the operating system's keychain instead of a plain-text configuration file, to prevent accidental reading by other local processes or accidental committing into a repository.
For devices without an operating system keychain (such as some IoT devices), at least a restrictive filesystem permission should be set for the stored tokens, combined with a short access token lifetime, so a compromised but locally stored token offers only a limited window for abuse.
9. OAuth2 flows at a glance
The table below compares the device flow with other OAuth2 grant types.
| Grant type | Suited for | Requires a browser on the client |
|---|---|---|
| Authorization Code | Web applications, mobile apps | Yes |
| Device Authorization Grant | CLI tools, smart TVs, IoT | No, only on a second device |
| Client Credentials | Service-to-service without user context | No, no user interaction needed |
| PKCE extension | Public clients without a client secret | Yes, supplements Authorization Code |
Mironsoft
OpenAPI design, Symfony APIs, and API security
APIs that external teams can integrate without back-and-forth questions?
We review existing REST APIs for inconsistent error formats, missing OpenAPI documentation, and security gaps, then build an API that is clearly documented, versioned, and hardened against abuse.
API Review
Checking the OpenAPI spec, error formats, and status codes for consistency.
Symfony Implementation
Using DTOs, Serializer, and Validator for clean, type-safe request/response models.
Security Audit
Hardening rate limiting, auth schemes, and input validation against real attack surfaces.
10. Summary
Device Flow: The Essentials at a Glance
Core idea
Authorization happens on a second device with a full-featured browser, while the original client waits in the background.
User code
Designed to be short, unambiguous, and typo-resistant, typically eight characters from a restricted character set.
Polling discipline
The client must respect the server-specified interval, or risk a slow_down response.
Limited validity
The device code expires after 10-15 minutes, limiting the abuse window for attackers.