Implementing OAuth Device Code Flow for a headless MCP CLI
Takayuki Kawazoe walks through implementing RFC 8628 OAuth Device Code Flow in a headless MCP CLI client, covering endpoint design, code generation, and Redis state storage with real production code.
Score breakdown
Understand this pattern to add secure, spec-compliant user authentication to any MCP server or CLI tool that runs in SSH, CI, or other browserless environments.
- 01The article implements RFC 8628 (OAuth Device Authorization Grant) for MCP clients running in headless environments with no GUI browser.
- 02The `/oauth/device/authorize` endpoint only issues codes to clients whose `grant_types` explicitly includes `urn:ietf:params:oauth:grant-type:device_code`.
- 03`device_code` is generated with `secrets.token_urlsafe(32)` for high entropy; `user_code` uses a restricted alphabet dropping confusable characters (0/O, 1/I/L) and is formatted as `ABCD-EFGH`.
Takayuki Kawazoe describes a concrete implementation of RFC 8628 — the OAuth Device Authorization Grant — built into a service called Codens' Auth. The motivation is practical: MCP clients frequently run in headless environments (SSH, CI containers, servers without a GUI) where the standard authorization code flow's browser redirect is simply unavailable. The Device Code Flow solves this by splitting authentication across two devices: the CLI displays a short code and polls for a token, while the user approves from any browser they already have.
The `/oauth/device/authorize` endpoint accepts a `client_id` and `scope`, verifies the client is active and explicitly opted into the `urn:ietf:params:oauth:grant-type:device_code` grant type, then issues both codes.
The `/oauth/device/authorize` endpoint accepts a `client_id` and `scope`, verifies the client is active and explicitly opted into the `urn:ietf:params:oauth:grant-type:device_code` grant type, then issues both codes. The response includes `device_code`, `user_code`, `verification_uri`, `expires_in` (900 seconds), and `interval` (5 seconds) — with the article emphasizing that the server, not the client, must dictate these timing values per the RFC. The two codes are generated differently by design: `device_code` uses `secrets.token_urlsafe(32)` for maximum entropy since a leak would expose the token, while `user_code` uses a restricted alphabet (`ABCDEFGHJKMNPQRSTUVWXYZ23456789`) that drops visually confusable characters like `0/O` and `1/I/L`, formatted as `ABCD-EFGH` to reduce transcription errors.
State is stored in Redis using two keys per authorization request: a primary key mapping `device_code` to the full state JSON, and an index key mapping `user_code` back to `device_code`. Both keys share the same 900-second TTL and are written atomically via a Redis pipeline. The article's source text is truncated before the explanation of why the dual-key design was chosen.
Key facts
- 01The article implements RFC 8628 (OAuth Device Authorization Grant) for MCP clients running in headless environments with no GUI browser.
- 02The `/oauth/device/authorize` endpoint only issues codes to clients whose `grant_types` explicitly includes `urn:ietf:params:oauth:grant-type:device_code`.
- 03`device_code` is generated with `secrets.token_urlsafe(32)` for high entropy; `user_code` uses a restricted alphabet dropping confusable characters (0/O, 1/I/L) and is formatted as `ABCD-EFGH`.
- 04The server response dictates both the poll `interval` (5 seconds) and `expires_in` (900 seconds) — clients must not hardcode these values.
- 05Redis stores two keys per request: a primary `device:code:{device_code}` key holding full state JSON, and an index `device:user_code:{user_code}` key pointing back to the `device_code`.
- 06Both Redis keys are written atomically via a pipeline and share a 900-second TTL.
- 07The implementation is part of a service called Codens' Auth.
Topics
Summary and scoring are generated automatically from the original article. We always link back to the publisher and never republish images or paywalled content. Last processed Jun 9, 2026 · 09:19 UTC. How this works →