- RFC 8628: OAuth 2.0 Device Authorization GrantDenniss, Bradley, Jones, Tschofenig (IETF, August 2019) - sections 3.1-3.5 are the flow, 5 the security analysis, 6.1 the user-code format
- RFC 6749, section 4.5: Extension Grantshow the device flow's URN grant_type plugs into the framework
- RFC 9700: Best Current Practice for OAuth 2.0 Securitypublic-client rules the device inherits: refresh rotation, sender-constraining
RFC 8628 (Denniss, Bradley, Jones, Tschofenig) exists for devices that "lack a browser to perform a user-agent-based authorization or are input constrained" - smart TVs, streaming sticks, consoles, printers, and the quiet workhorse use: CLI tools on machines you do not want to type secrets into (gcloud auth login, gh auth login, Docker, SSH sessions, containers).
The idea: the device cannot render a login page, but the user has a phone in reach. So the flow splits across two screens. The device asks the authorization server for a short human-typeable code and shows it; the user opens a URL on a different device, signs in there, and types the code; meanwhile the device politely polls the token endpoint until the human finishes. The spec's operating requirements (section 1) are modest: the device is online, can make outbound HTTPS, can display a URI plus code, and the user has a secondary device. It is explicitly not for capable native apps - those get the code flow (RFC 8252).
Note what the device never sees: credentials, session cookies, or the consent click. Its only input is a code the user could also read over your shoulder - which is why the security analysis centres on that code.
POST /device_authorization HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded client_id=1406020730 scope=example_scope
A separate endpoint from the authorization endpoint - the browser never visits it. TLS is a MUST, and the spec asks clients to start the flow only "when prompted by the user and not automatically, such as when the app starts" (3.1).
HTTP/1.1 200 OK
Content-Type: application/json
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "https://example.com/device",
"verification_uri_complete": "https://example.com/device?user_code=WDJB-MJHT",
"expires_in": 1800,
"interval": 5
}The example values here are RFC 8628 section 3.2's own published example - the same WDJB-MJHT user code appears in the RFC.
| Field | Status | Role |
|---|---|---|
device_code | REQUIRED | The machine's half of the pair - never displayed to the user, high entropy (section 5.2) |
user_code | REQUIRED | The human's half - short, typeable, shown on the TV screen |
verification_uri | REQUIRED | "Should be short and easy to remember as end users will be asked to manually type it" |
verification_uri_complete | OPTIONAL | URL with the code baked in - designed for QR codes and NFC (3.3.1) |
expires_in | REQUIRED | Lifetime of both codes; the whole session dies when it lapses |
interval | OPTIONAL | Minimum seconds between polls; clients MUST default to 5 when absent (3.2) |
POST /token HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS client_id=1406020730
A registered extension grant - the grant_type is the URNurn:ietf:params:oauth:grant-type:device_code (section 7.2). This is the one OAuth grant where polling is the protocol: "it is expected for the client to try the access token request repeatedly in a polling fashion based on the error code in the response" (3.4).
| Response (3.5) | Meaning | Client behavior |
|---|---|---|
authorization_pending | User has not finished on the second device yet | Keep polling - but "before each new request, the client MUST wait at least" interval seconds (5 if none was given) |
slow_down | You are polling too fast | The interval "MUST be increased by 5 seconds for this and all subsequent requests" - a cumulative, permanent bump. Three slow_downs means interval +15s for the rest of the session. (Explainers that say "wait 5 more seconds once" have it wrong.) |
access_denied | The user declined | Stop. A normal outcome; surface it as one |
expired_token | The device session lapsed | Stop; MAY restart the flow, but SHOULD wait for user intent first |
| anything else | A real error | The client MUST stop polling |
One more rule from 3.5 worth quoting: on connection timeouts the client "MUST unilaterally reduce their polling frequency", with exponential backoff RECOMMENDED. Polling politely is the contract that keeps authorization servers willing to offer this grant.
Section 6.1 is a small masterclass in designing for humans. Recommendation: letters only, case-insensitive, digits avoided, and vowels removed - the base-20 charset BCDFGHJKLMNPQRSTVWXZ - so no entry can spell a real word. Format with dashes (WDJB-MJHT); the server strips them, uppercases input, and drops out-of-charset characters on the way in. An 8-character base-20 code carries roughly 34.5 bits of entropy per the spec's own math in section 5.1 - deliberately low, because a human types it, and safe only because the server rate-limits guesses.
Two myths to retire. There is no "20-bit entropy requirement" anywhere in the RFC - section 5.1 leaves length and entropy to the authorization server's discretion. And the security pairing is asymmetric by design: the user_code is weak because humans type it, while thedevice_code (never displayed) "SHOULD" be very high entropy (section 5.2). The surprise in 5.1: brute-forcing a user_code wins an attacker an authorization grant on their own account, attached to your device - the mirror image of a stolen bearer token, and the reason the flow survives short codes.
For QR-capable devices, verification_uri_complete skips typing entirely (3.3.1) - but the server SHOULD then still display the user_code and ask the user to confirm it matches the device's screen. That confirmation step is the tripwire against the attack below.
| Failure | What happens | The fix |
|---|---|---|
| Remote phishing (5.4) | Attacker starts the flow on their device, then emails the victim: "visit this URL and enter this code." The victim's approval lands on the attacker's device | The spec's mitigations: say what is being authorized, display device info, keep user_code lifetimes short - and treat the code-confirmation step as non-optional in your own implementation (the spec calls it "particularly important", with a SHOULD on the server to ask users to verify the code) |
| User-code brute force (5.1) | Short codes invite guessing at scale | Rate-limit per client and per account; short expires_in; the math in 5.1 shows ~5 allowed guesses keeps success at 2^-32 |
| Rude polling | Fixed tight loops ignore interval and slow_down, tripping rate limits and locking devices out | Honour interval (5s default), apply slow_down cumulatively, back off on timeouts (3.5) |
| Device treated as confidential | Embedded credentials are extractable by the device owner - "users in possession of the device can reverse-engineer it and extract the credentials" (5.6) | Model it as the public client it is: refresh tokens sender-constrained or rotated (RFC 9700 §2.2.2) |
| Shoulder surfing / screen capture (5.5) | The code on a lounge TV is readable by anyone in the room, and the TV's own software may spy on the session | Short lifetimes, one code per session, and the consent screen showing what was requested |