First time I tried to login to Netflix at a hotel TV I almost gave up. The remote was having only four arrow keys and a number pad. My password was 18 characters with symbols. Whoever designed the login screen had either never used it themselves, or they had decided suEering builds character.
After few years, the same TV’s started doing something different. They showed us a short code and an URL. I opened phone, typed the URL, entered the code and we are in. No remote-control circus. No password on a TV.
That is OAuth 2.0 device authorization grant. Most of the people just call it that device flow.
If we run aws sso login, gh auth login, or signed into Spotify on an Xbox, we have already used it. And if you are build a backend for a CLI, an IOT device, a smart TV app or anything where typing a password is really painful or not safe, we should end up implementing it sooner or later.
Here is the step by step
Step1: Assume we are building a CLI called mycli. The user runs:
$ mycli login
Below will happen behind the scenes.
Step 1: The CLI asks for a code
The CLI sends a POST to authorization server:
POST /oauth/device_authorization HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
client_id=mycli-prod&scope=read:repos%20write:reposThe server responds with something like this:
{
"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
}Five fields actually matter:
• device_code is a long, opaque string. The CLI keeps this private.
• user_code is the short, human-friendly one. The CLI shows this on screen.
• verification_uri is where the user goes to enter the code.
• expires_in is how long the exchange is valid. Usually 15 to 30 minutes.
• interval is how often the CLI is allowed to poll for the token. We will get to that.
Step 2: the CLI tells the user what to do
Your CLI prints something like:
Open this URL in your browser:
https://example.com/device
Enter this code:
WDJB-MJHT
Waiting for confirmation...The user goes to their phone or laptop, opens the URL enter the code and signs in normally with SSO and approves the request.
Step 3: CLI polls the token endpoint
When we are using phone, the CLI is in the loop and every five seconds it try to reach the server and get the status
POST /oauth/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=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS
&client_id=mycli-prodThe server’s response is one of the five things, this is the part most of us will get wrong on the first implementation.
Five response we should handle
Authorization_pending – Users not approved yet
slow_down – you are polling too fast. The RFC says you must increase your interval by at least 5 seconds
expired_token – Exchange ran out of time, stop polling
access_denied – User clicked no or closed the page. Stop polling
success – the token is in the body, like a normal OAuth response:
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "v1.MRn7...",
"scope": "read:repos write:repos"
}That is the whole protocol. If we can handle those five cases, we will have a working device flow client.
Few additional things to take care of
1. Treating user_code like it is secret. It is not. The user types it into their phone. Your job is to make it short enough to read but unguessable. The standard is 8 to 9 alphanumeric characters with the confusing ones removed (no 0, O, I, 1). GitHub uses 8 characters in the format XXXX XXXX. That is a good template.
2. No rate limiting on the verification page. The endpoint where users enter the user_code is a brute-force target. With a 9-character code from a reduced alphabet, you have plenty of theoretical entropy, but pending codes at any given moment number in the thousands. Lock it down. Per-IP, per-session, exponential backoff after failures.
3. Ignoring slow_down. I have seen clients that sleep for the same interval forever, no matter what the server says. That works in dev. In production it gets you flagged as abusive.
4. Not expiring the user_code immediately on use. Once the user submits the code, mark it consumed. Atomically. If your check-then-mark logic is not transactional, you have a real bug waiting to happen.
5. Confusing this with PKCE. Device flow is for input-constrained devices. PKCE is for public clients (mobile apps, SPAs) that cannot keep a secret. They look similar on the surface (no client secret) but solve different problems. You sometimes need both. Device flow + PKCE is fine and increasingly common.
Conclusion
The device flow is one of those protocols that looks complicated in the RFC but actually fits on a napkin. Two endpoints, five response cases, Once you have built it once, you will wonder why anyone ever bothered with passwords on TVs.
If you are shipping a CLI today and still asking users to paste personal access tokens into a prompt, do them a favor. Spend an afternoon implementing this. Your users will notice.