OAuth2 Device Flow for CLI Tools and Headless Clients
AI generated
{ }
GET
OAuth2 · Device Flow
OAuth2 Device Flow for CLI Tools
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.

15 min read OAuth2 Device Flow RFC 8628

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.

11. FAQ: Device Flow: The Essentials at a Glance

1Is the device flow more secure than the classic Authorization Code Flow?
Not inherently more secure, but designed for a different use case, and comparably secure when implemented correctly.
2Can I use the device flow for regular web applications too?
Technically possible but unusual and unnecessary, since web applications can normally receive a redirect without issue.
3How long should the device code's validity be?
10 to 15 minutes is common, a compromise between giving the user enough time and limiting the abuse window.
4What happens if the user rejects the authorization?
The authorization server returns an access_denied error instead of the token on the next polling request.
5Do all OAuth2 providers support the device flow?
No, it's an optional grant type. Major providers like Google, GitHub, and Microsoft support it, smaller ones often don't.
6How do I protect against phishing attacks via the device flow?
Through clear display of authorization details on the verification page and educating users not to enter an unverified, unfamiliar code.
7Should the user code be case-sensitive?
Usually not, to avoid typos from accidental case mismatches. The server-side comparison is done case-insensitively.
8How do I implement rate limiting for the polling endpoint?
Via the server-specified interval value combined with server-side enforcement that rejects too-frequent requests with slow_down.
9Can a device code be used multiple times for different users?
No, every device code is bound to exactly one authorization session and becomes invalid after use or expiration.
10Is RFC 8628 compatible with OpenID Connect?
Yes, the device flow can be combined with OpenID Connect scopes to obtain identity information in addition to authorization.