Skip to content
Ashish.
All posts
Diagram illustrating the split between a headless IoT device and a user's smartphone during OAuth2 Device Authorization Grant.

OAuth2 Device Grant: IoT & CLI Auth (RFC 8628)

This article explains the OAuth2 Device Authorization Grant for securing IoT devices and CLI tools using RFC 8628.

By Ashish SrivastavaPart 1 of OAuth2 Security Series

The Headless Problem: Why Standard Flows Fail on Devices

When you attempt to log into a service like GitHub or AWS from a standard web browser, the flow is seamless because the browser acts as both the client and the user interface. You click a link, enter your password, and the browser receives the tokens. This is the "Authorization Code Flow." However, this model collapses when the client has no keyboard, no screen, or no ability to launch a system browser. Consider a smart thermostat in a wall socket or a command-line interface (CLI) tool running on a server with no graphical environment. These are "headless" clients.

If you try to force a standard flow on a headless device, you hit a deadlock. The device needs to open a URL to get a token, but it cannot display that URL to the user to complete the login. If you embed a username and password directly in the device firmware (the "Resource Owner Password Credentials" flow), you create a massive security liability: that secret is hard-coded, immutable, and potentially visible to anyone with physical access to the device.

The solution defined in RFC 8628 is the Device Authorization Grant. This flow fundamentally splits the authentication process into two distinct channels: the Device Channel (the constrained IoT or CLI device) and the User Channel (the user's smartphone or desktop browser). The device does not authenticate the user; instead, it requests permission to act on the user's behalf, and the user confirms that permission on their own trusted device.

Technical diagram showing a split authentication flow. Left side : A constrained IoT thermostat with a small screen displaying a code. Right side : A user's smartphone opening a browser to enter the code. An arrow flows from the thermostat to an Auth Server, and another from t…

The Mechanism: Polling for Authorization

The core mechanism of this grant type is a polling loop. The device does not wait for a callback; it actively asks the authorization server, "Has the user approved me yet?" This turns the authentication event into a state machine with three distinct states: authorization_pending, access_denied, and complete.

Let's trace the mechanism with named actors. Imagine Device A is a smart light bulb, and User B is holding a smartphone.

  1. Requesting the Device Code: Device A sends a POST request to the authorization server's /token endpoint. It includes its own client ID and a list of requested scopes (e.g., read:lights).

    POST /token HTTP/1.1
    Host: auth.example.com
    Content-Type: application/x-www-form-urlencoded
     
    client_id=client_123&scope=read:lights&grant_type=urn:ietf:params:oauth:grant-type:device_code

    The server responds with a JSON object containing two critical artifacts: a device_code and a user_code. The device_code is a long, random string that identifies the specific session on Device A. The user_code is a short, human-readable string (like ABCD-EFGH) designed to be typed into a browser. The response also includes verification_uri (where to go) and verification_uri_complete (which may auto-fill the code if the browser is the same device, though this is an implementation convention rather than a protocol guarantee).

    {
      "device_code": "X7Y9Z2...",
      "user_code": "ABCD-EFGH",
      "verification_uri": "https://auth.example.com/verify",
      "verification_uri_complete": "https://auth.example.com/verify?user_code=ABCD-EFGH",
      "expires_in": 900,
      "interval": 5
    }
  2. The User Action: Device A displays the verification_uri and user_code on its small screen or via LED blips. User B navigates to the URI on their smartphone and enters ABCD-EFGH. The server records this mapping: "User B just authenticated the device_code for Client client_123."

  3. The Polling Loop: While User B is typing, Device A begins polling the /token endpoint. It sends the device_code and client_id to ask for an access token.

    POST /token HTTP/1.1
    Host: auth.example.com
    Content-Type: application/x-www-form-urlencoded
     
    grant_type=urn:ietf:params:oauth:grant-type:device_code
    device_code=X7Y9Z2...
    client_id=client_123

    The server checks its internal state. If User B hasn't finished typing yet, the server returns a specific error code: authorization_pending.

    {
      "error": "authorization_pending",
      "error_description": "The authorization request is still pending"
    }

    Crucially, the server response includes an interval parameter (e.g., 5 seconds). This dictates the minimum time Device A must wait before polling again. This is a DoS protection mechanism. Without it, a malicious device could hammer the server with thousands of requests per second. The device must respect this backoff.

  4. Token Issuance: Once User B submits the form, the server updates the state to complete. On the next successful poll from Device A (after the required interval), the server returns the access_token, refresh_token, and expires_in.

    {
      "access_token": "eyJhbG...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "dGhpcyBpcyBhIHRlc3Q...",
      "scope": "read:lights"
    }

    Now Device A can make API calls to control the light.

Implementation in IoT and CLI Environments

The utility of this flow becomes clear when applied to specific constraints.

IoT Scenario: The Smart Thermostat A Nest-like thermostat has a tiny LCD and no Wi-Fi browser. It cannot run a full OAuth redirect loop. Using the Device Grant, the thermostat displays "Go to google.com/setup and enter code: 123456". The user scans a QR code (which contains the verification_uri_complete) or types the code into their phone. The thermostat polls the server every 5 seconds. If the user takes 2 minutes to find their phone, the thermostat doesn't hang; it simply waits in the authorization_pending state. If the user code expires (usually 15 minutes), the server returns expired_token, and the device must restart the entire process.

CLI Scenario: The AWS Command Line When a developer runs aws s3 ls for the first time on a fresh terminal, the CLI needs credentials but has no interactive password prompt for SSO. The CLI generates a device code and prints it to the console. It then opens a browser window on the host machine (or instructs the user to open one). The CLI polls for the token. Once the user approves the action in the browser, the CLI receives the token and stores it locally. This is preferred over embedding an IAM secret key in a script because the secret key is static, whereas the device flow allows for short-lived tokens and refresh token rotation without re-entering credentials every time.

Security Mechanics and Tradeoffs

The Device Authorization Grant is not without risks. The primary vulnerability lies in the "shoulder surfing" aspect of the user code. Because the user code is short and alphanumeric, it is susceptible to being guessed or observed by someone standing near the device. To mitigate this, RFC 8628 mandates that the user_code must have a short lifespan (typically 15 minutes) and a low entropy compared to the device_code. The device_code itself is high-entropy and never exposed to the user, ensuring that even if the user code is stolen, the attacker cannot redeem it without knowing the device_code (which is only known to the device and the server).

Furthermore, this flow requires the client to be a "public client" in OAuth terms. Public clients are those that cannot securely store a secret (like a CLI tool or a browser extension). Because they cannot prove their identity with a client secret, they must rely on the user's explicit consent via the secondary device.

Opinion: While this flow is robust for IoT, it introduces a latency penalty. The user experience is inherently slower than a single-click login because the device must wait for the user to switch contexts. For high-frequency API interactions, this is unacceptable. Therefore, this flow is best reserved for initial provisioning or infrequent administrative access, not for real-time data synchronization loops.

Another critical mechanism is the handling of access_denied. If a user explicitly clicks "Deny" on the authorization page, the server returns access_denied to the polling device. The device must stop polling and report an error to the user. It should not retry indefinitely, as this would create a denial-of-service vector against the user's consent.

Finally, this flow integrates with PKCE (Proof Key for Code Exchange, RFC 7636). Although PKCE was originally designed for the Authorization Code flow, it is an optional but recommended extension for public clients to prevent token interception. Implementing PKCE adds a layer of defense by requiring the device to generate a code_verifier and a code_challenge during the initial phase, ensuring that an attacker who intercepts the device_code cannot redeem it without the verifier.

Common Pitfalls

Implementing the Device Authorization Grant often leads to specific errors if not handled carefully.

  1. Ignoring the Polling Interval: Developers sometimes ignore the interval parameter returned by the server and poll at a fixed, faster rate (e.g., every 1 second). This violates the protocol and can trigger rate-limiting or IP bans on the authorization server. Always implement a jittered backoff based on the server's interval.
  2. Hardcoding Verification URIs: Attempting to hardcode the verification_uri in the device firmware is risky because server domains change. The device must rely on the verification_uri provided dynamically in the initial response, allowing for server-side configuration changes without firmware updates.
  3. Infinite Retry Loops on Errors: If the server returns access_denied or expired_token, the device must stop polling immediately and prompt the user to restart the flow. Continuing to poll after a denial creates unnecessary load and confuses the user, who has already rejected the request.

Practical Takeaways

To successfully deploy this flow, keep these mental models in mind:

  • Split Trust: Never assume the headless device is trustworthy enough to hold secrets. Treat it as a conduit that passes trust to the user's secondary device.
  • Stateless Waiting: The device is not "waiting" in a blocking sense; it is actively checking a state machine. Design your UI to reflect this active waiting (e.g., "Waiting for approval...") rather than a frozen state.
  • Token Lifetime Awareness: Remember that the user_code expires independently of the device_code. If a user takes too long to authorize, the entire flow must be reset, not just the token request.

FAQ

Q: Can I use the Device Authorization Grant for browser-based web applications? A: No. This grant is specifically designed for clients that cannot interactively open a browser (like IoT devices or CLIs). Web applications should use the standard Authorization Code Flow with PKCE.

Q: What happens if the user code expires before I enter it? A: The server will return an expired_token error to the polling device. The device must then restart the entire flow from step 1, generating a new device code and user code.

Q: Is the Device Grant suitable for high-frequency API calls? A: No. The polling mechanism introduces latency and requires the user to be present to authorize. It is intended for initial provisioning or administrative access, not for frequent data synchronization loops.

Conclusion

The Device Authorization Grant transforms the authentication problem from "how do I get a password into a device?" to "how do I prove I am the owner of this device?". By offloading the credential entry to a trusted secondary channel and using a strict polling mechanism, it secures headless infrastructure without compromising the user experience or the device's limited resources. This mechanism is essential for the modern ecosystem of IoT devices and command-line tools, providing a secure bridge between constrained hardware and robust identity providers.

Related posts