Skip to content

Device Code (RFC 8628) ​

The device-authorization grant — also called "device code" or "device flow" — exists for clients that cannot run a browser or have no convenient way to type a password. Smart TVs, gaming consoles, CLI tools, IoT devices, point-of-sale terminals.

The most familiar example is signing into Netflix on a new TV: the screen displays a short code (ABCD-EFGH) and a URL (netflix.com/tv); you open that URL on your phone, type the code, and approve. The TV — which never saw your password — then receives an access token.

Specs referenced on this page
  • RFC 8628 — OAuth 2.0 Device Authorization Grant
  • RFC 6749 — OAuth 2.0 Authorization Framework (terminology)
Vocabulary refresher
  • device_code — the long, opaque identifier the OP issues for the device to poll with. Never shown to the user; sent only to the device.
  • user_code — the short, human-readable code (e.g. BDWP-HQPK) shown on the device's screen and typed into the verification page.
  • verification_uri — the URL printed on the device's screen (https://op.example.com/device); the user opens it on their phone.
  • verification_uri_complete — the same URL pre-filled with the user_code; if the device can render a QR code, the user just scans it and skips the typing step.
  • interval — how often, in seconds, the device should poll /token. The OP raises this value when it returns slow_down.

How the flow runs ​

The user approves on a second screen while the device polls
The device authorization grant. The device asks for a device code and a short user code, shows the user where to go, and polls the token endpoint while the user approves on a second screen. Once approval lands, the next poll returns tokens.Request the codesWait for approvalDeviceTV · CLI · consoleOPgo-oidc-providerUserphone · second screenPOST /device_authorizationclient_id · scope200 · device_code, user_codeverification_uri · intervalshow the URL and the codeop.example.com/deviceuser_code=ABCD-EFGHPOST /token · pollgrant_type=…:device_code400 · authorization_pendingGET /device · user_codelog in, consent, approvedevice_code → subjectPOST /token · next poll200 · access_tokenid_token? · refresh_token?
The device holds only the device_code and never sees a password; the short user_code exists so a person can retype it on a real keyboard. The poll repeats every interval seconds until the approval lands.

The device never holds the user's password. The user never types anything on the device. The two surfaces meet at the OP through the short user_code.

Polling responses ​

The token endpoint (/token) returns one of four shapes per poll:

ResponseMeaningWhat the device should do
400 authorization_pendingThe user has not approved (or denied) yet.Wait interval seconds and try again.
400 slow_downThe device polled too fast.Increase the interval by 5 seconds for this and all subsequent requests (RFC 8628 §3.5). The OP persists the new interval atomically with LastPolledAt so a multi-replica deployment cannot be tricked into resetting it.
400 access_deniedThe user clicked deny on the verification page, an embedder revoked the record, or the device ignored slow_down five times (poll_abuse).Stop polling. Show "Sign-in cancelled".
400 expired_tokenThe device_code outlived expires_in (default 600 s), or an earlier poll already redeemed it.Stop polling. Restart the flow if the user wants to retry.
200 { access_token, ... }The user approved.Treat as a normal token response.

The device may poll no faster than interval (default 5 s, with one second of jitter tolerance). A poll sooner than that is answered slow_down, and the fifth such offence ends the record with access_denied.

Each device_code record moves through a closed set of states, and the token endpoint's answer follows from the state:

Record stateReached whenNext poll answers
PendingPOST /device_authorization created itauthorization_pending
Approvedthe verification page called ApproveUserCodetokens, and the record becomes Consumed
Deniedthe user denied, the embedder revoked it, or a lockout fired (poll_abuse, or user_code_lockout from the record-bound verify helpers)access_denied
Consumeda poll redeemed the approvalexpired_token

Redemption is a single atomic Approved-to-Consumed transition, so two racing polls cannot both receive tokens. The loser gets expired_token.

user_code is brute-forceable by design

The user_code is short on purpose — long codes are unusable. That makes it brute-forceable in principle: an attacker who can hit /device faster than the user can type wins. The library ships op/devicecodekit. For the first manual entry, call VerifyUserCodeByAttemptKey with an opaque, server-side ceremony key, then call ApproveUserCode after the user authenticates and consents. Do not use the raw user_code as the attempt key. Deps contains a mutex-backed limiter and must be retained and passed by pointer; never copy it. The default limiter is process-local, so multi-instance deployments need a shared atomic AttemptLimiter.

What the OP serves and what you build ​

op.WithDeviceCodeGrant(op.DeviceCodeGrant{...}) mounts /device_authorization, advertises device_authorization_endpoint in discovery, and adds the device grant to the enabled set. It does not serve the verification page. You host that page (default URL <issuer>/device, override with DeviceCodeGrant.VerificationURI), authenticate the user, and call the devicecodekit helpers:

go
deps := &devicecodekit.Deps{DeviceCodes: st.DeviceCodes(), Grants: st.Grants()} // keep one, pass the pointer

ok, err := devicecodekit.VerifyUserCodeByAttemptKey(ctx, deps, ceremonyKey, typedCode)
// ok && err == nil: show consent, then
err = devicecodekit.ApproveUserCode(ctx, deps, typedCode, subject, authTime)
// or devicecodekit.DenyUserCode(ctx, deps, typedCode, devicecodekit.DenyReasonUserDenied)

DeviceCodeGrant.TTL and DeviceCodeGrant.PollInterval set the expires_in and interval the device is told. Zero keeps the default (10 minutes and 5 seconds); a negative value fails op.New.

The grant resolves an end user when the device is approved, so op.New requires a user store: op.WithUserStore or an op.PrimaryPassword store in the login flow.

Redeeming an approval records a consent grant for that client, so tokens from the device flow are covered by RevokeSubject and grant management, and report the public (pairwise) subject at introspection and /userinfo. A later /auth request from the same client for the same scopes skips the consent screen. devicecodekit.Revoke(ctx, deps, deviceCode, reason) cascades to the access and refresh tokens issued under that grant, through whichever token stores you set on Deps (AccessTokens or GrantRevocations, OpaqueAccessTokens, RefreshTokens). Set Deps.Grants as well, or the grant record itself stays behind.

When to use it ​

Pick the device flow when one of the device-side constraints holds:

  • No browser — set-top boxes, smart TVs, voice assistants.
  • No keyboard / clumsy keyboard — TV remotes, game-controller D-pads.
  • CLI tools that ship without a web server (gcloud auth login, gh auth login, kubectl oidc-login).
  • Headless automation contexts where pairing happens once at provisioning.

Avoid it for browser-able clients (regular SPAs, native apps with custom URL schemes) — authorization_code + PKCE is shorter, safer, and gives a richer UX. RFC 8628 §3 frames device flow as the fallback when the canonical flow is impractical.

See it run ​

examples/31-device-code-cli drives the full RFC 8628 round trip from a single binary: it stands up the OP, prints a boxed user_code panel + the verification_uri_complete shortcut, simulates browser approval after a few seconds, and polls until the OP issues an access_token + id_token.

sh
(cd examples/31-device-code-cli && GOWORK=off go run -tags example .)

The example is split into role-tagged files (op.go for the OP wiring, cli.go for the device-side polling, device.go for the simulated browser approval, probe.go for self-verification) so each surface is readable in isolation.

  • Use case: device-code wiring — op.WithDeviceCodeGrant, devicecodekit.VerifyUserCodeByAttemptKey, the verification page contract, and how to cascade-revoke issued tokens when a device is unenrolled.
  • Authorization code + PKCE — the default flow whenever the client can open a browser.
  • CIBA primer — the conceptual sibling for "user is on a different channel" but without a code-on-screen ceremony.