Animated Architecture Patterns

Device Authorization Grant

Sign in on a TV or CLI by approving a short code on your phone.

Also known as Device code flow, Device flow

TV app no keyboard, no browser Authorization server example.com Phone the user's second device browser MoviesSign inpress OK on the remote On your phone, go toexample.com/deviceand enter the codeWDJB-MJHTchecking every 5 schecking every 10 s Your library Sign-in declinedaccess_deniedCode expired: press OKexpired_token client_id=tv-appscope=library access_tokenrefresh_token Response: 200 OK{"device_code": "GmRhmhcxhwAzkoEq…", "user_code": "WDJB-MJHT", "verification_uri": "https://example.com/device", "verification_uri_complete": "https://…/device?user_code=WDJB-MJHT", "expires_in": 900, "interval": 5} TV polls POST /token: device_code + grant_typeurn:ietf:params:oauth:grant-type:device_code 5 sauthorization_pending 10 sauthorization_pending 12 sslow_down too soon: wait 10 s from now on 22 sauthorization_pending ⋮every 10 s until the user approves 52 saccess_token + refresh_token times: seconds after the code was issued Device authorization endpoint POST /device_authorization Token endpoint POST /token Verification page example.com/device: code, sign-in, consent Pending requests device_code user_code status expires GmRhmh…WDJB-MJHT20:30pendingapprovedissued Nq8dWv…QXKT-BHMZ20:30pendingapproved device_code: long, random, kept secret by the TVuser_code: short, for a person to typeNo client secret: the TV app is a public clientBoth expire at 20:30 (expires_in 900) The password and MFA go only to example.com:the TV never sees them. The token endpoint answers from the row's status:pending → authorization_pending or slow_downapproved → access_token + refresh_token Mitigationsconsent names the app and device, and warnsshort expiry (here 15 minutes)rate limits on code entryonly clients that need it may use this grant Attacker's device, far awaystarted its own flowQXKT-BHMZpolling…signed in asthe victim lure: “enter QXKT-BHMZ at example.com/device to keep your account” 20:15 example.com/device Enter the code shownon your deviceWDJB-MJHTContinueContinue Sign inalex@example.com••••••••MFA approved Allow access?MoviesTV app (tv-app)Device: smart TVFrom: near youCan: see your library,stay signed inAllow only if youstarted this on a TVin front of you.DenyAllowAllow You're all setGo back to your TV Messages · nowYour account will beclosed! To keep it, goto example.com/deviceand enter QXKT-BHMZ Allow access?MoviesTV app (tv-app)Device: smart TVFrom: another countryCan: see your library,stay signed inAllow only if youstarted this on a TVin front of you.DenyAllowAllow

Step 1 · Ask for a code. The TV app is a public client, so it sends only its client_id and the scope it wants to the device authorization endpoint. The server stores a pending request and answers with a long, random device_code that the TV keeps to itself, a short user_code (WDJB-MJHT), the verification_uri, an optional verification_uri_complete for a QR code, expires_in (900 s here) and the polling interval (5 s). The TV tells the user where to go and which code to enter.

Step 2 · Approve on the phone. The user opens example.com/device on their phone and enters WDJB-MJHT (scanning the QR code, which carries verification_uri_complete, skips the typing), then signs in the usual way, including MFA. The consent screen names the app and the device, and tells the user to allow it only if they started this on a TV in front of them. They tap Allow and the request is marked approved; the password and the second factor went only to the authorization server, never to the TV.

Step 3 · The TV polls. From the start, the TV polls the token endpoint with grant_type=urn:ietf:params:oauth:grant-type:device_code and its device_code, waiting interval seconds between polls. While the request is pending the answer is authorization_pending; a poll that comes too soon gets slow_down, and the TV adds 5 s to its interval for good. The first poll after approval returns an access token and a refresh token, and the TV shows the user's library.

Step 4 · Other endings, phishing. If the user taps Deny the next poll returns access_denied; if nobody approves within expires_in, it returns expired_token, and the TV gets a new code only when the user asks for one. The risk this flow adds is remote phishing: an attacker starts a flow on their own device and sends the code to a victim with a pretext, and if the victim approves, the attacker's device receives the tokens. Defences: a consent screen that names the app and the device and warns, a short expiry, rate limits on code entry, and enabling the grant only for clients that need it.

The problem

A smart TV app, a game console or a command-line tool needs to call an API on a user's behalf. The usual answer, the authorization code flow, sends the user to a browser on the same device and redirects back to the app. That breaks down when the device has no usable browser, or when the only keyboard is a remote control: typing an email address, a long password and a one-time code letter by letter is miserable, and it hands the password to the device. A CLI in an SSH session has a keyboard but no browser on the machine that runs it. What these devices do have is a screen (or some other way to show a short code) and an outbound connection to the internet, and their user nearly always has a phone within reach.

How it works

The OAuth 2.0 Device Authorization Grant (RFC 8628, usually called the device code flow) splits sign-in across two devices: the device that wants the tokens, and a second device with a real browser where the user signs in.

  1. Device authorization request. The device posts its client_id and the scope it wants to the authorization server's device authorization endpoint. A device can't keep a client secret (whoever owns it can extract one), so it is normally a public client and sends no secret (RFC 8628 §5.6).

  2. Device authorization response. The server stores a pending request and returns device_code (a long, random value the device keeps to itself and never displays), user_code (short, for a person to type), verification_uri (short and easy to type), optionally verification_uri_complete (the same address with the code already in it, for a QR code or NFC), expires_in (how long both codes live) and optionally interval (the minimum gap between polls, 5 seconds when the server leaves it out).

  3. User interaction. The device shows the address and the code. On their phone or laptop the user opens the address, signs in the way they always do (passkey, password, MFA, a federated login), enters the code, sees what they are authorizing, and approves or denies. The device never handles a credential.

  4. Polling. Meanwhile the device calls the token endpoint with grant_type=urn:ietf:params:oauth:grant-type:device_code and its device_code, waiting at least interval seconds before each call. Nothing is redirected back to the device: the protocol assumes the phone has no way to reach it, so polling is how the device learns the outcome. Until there is one, the token endpoint answers with one of four errors:

    • authorization_pending: no decision yet, poll again.
    • slow_down: still pending, and the device is polling too fast. It must add 5 seconds to its interval for this request and every later one.
    • access_denied: the user declined. Stop.
    • expired_token: expires_in has passed. Stop, and start a new flow only when the user asks for one.

    Any other error also ends the polling. After a connection timeout the device must poll less often, for example by doubling its interval.

  5. Tokens. After the user approves, the next poll gets an ordinary token response: an access token, usually a refresh token, and an ID token when the device asked for the openid scope.

A server that publishes OAuth metadata (RFC 8414) can announce the grant there: a device_authorization_endpoint, and the grant type URN under grant_types_supported.

When to use it

  • A screen without a good keyboard or browser: smart TVs, streaming sticks, consoles, set-top boxes, smart displays, printers and kiosks.
  • Command-line tools in headless sessions: a CLI over SSH, in a container or on a jump host, with a person at the other end. gh auth login (its browser login shows a one-time device code to enter on github.com) and az login --use-device-code are familiar examples.
  • Not when the device has a usable browser. Use Authorization Code + PKCE, which keeps sign-in on the same device and avoids the phishing problem below. On macOS and Linux, for example, Azure CLI opens the browser for an authorization code sign-in when it can and falls back to the device code flow only when it can't.
  • Not when there is no user. Service-to-service calls use Client Credentials.
  • Not for high-value access you can't otherwise protect. The IETF's best current practice for cross-device flows (RFC 10027, BCP 247, published August 2026) says to use this grant only when the stronger cross-device options aren't possible, to avoid it for sensitive or business-critical resources, and always to add mitigations such as proximity checks or pre-registered devices. It also says not to use cross-device flows at all when both "devices" are the same one.

Trade-offs

  • Remote phishing comes with the design. Nothing links the person who approves to the screen that shows the code. An attacker can start a flow on their own device and send its user code to a victim with a pretext ("enter this code to keep your account"); if the victim signs in and approves, the tokens go to the attacker's device. RFC 8628 §5.4 describes the attack, and it happens in practice: in February 2025 Microsoft reported a campaign by a group it tracks as Storm-2372, running since August 2024, whose fake Microsoft Teams invitations used an attacker's device code as the "meeting ID".
  • Short codes can be guessed. A user code is short so that people can type it, so expiry and rate limits are what stop brute force (see the notes below).
  • QR codes trade safety for convenience. verification_uri_complete saves typing, but a link that already carries the code is even easier to send to a victim. The server should still show the code and ask the user to check that it matches the screen in front of them.
  • The approving device is not the device that gets the tokens. Device compliance, location and risk signals from the phone's session say little about the TV (RFC 8628 §5.3), so policies built on them don't protect it.
  • Someone in the room can get there first. Anyone who can see the screen can enter the code before the owner does and attach the device to their own account (session spying, §5.5).
  • Polling costs capacity. Every waiting device calls the token endpoint every few seconds; interval and slow_down are the server's controls.
  • The client can't prove who it is. Any software can present a public client's client_id, so a consent screen that names the app doesn't prove the app is genuine.

Implementation notes

  • User codes (RFC 8628 §6.1): the RFC suggests 8 characters from the 20 consonants BCDFGHJKLMNPQRSTVWXZ (no vowels, so a code can't spell a word), shown with a dash for readability as in WDJB-MJHT: 20⁸ combinations, about 34.5 bits. Where people don't use A–Z keyboards, 9 digits work too (019-450-730, 10⁹). Uppercase the input before comparing, strip dashes and anything outside the character set, and avoid character sets with look-alikes such as 0 and O, or 1, l and I. Rate-limit code entry: §5.1 works out that with an 8-character code like this, the rate limit and lifetime together should allow only about 5 guesses to hold an attacker's chance near 1 in 2³². GitHub, for one, accepts 50 code submissions per hour per app. The device_code is never typed, so make it long and random (§5.2).
  • Consent screen: name the client and the kind of device, say where the request came from, tell the user to continue only if they started this on a device in front of them, and give Deny at least as much prominence as Allow (RFC 10027 §6.1.14).
  • Phishing defences (RFC 10027): assess the risk first; establish proximity where you can (same network, geolocation, Bluetooth); keep codes short-lived and limited to one use; rate-limit; keep scopes narrow and tokens short-lived; sender-constrain tokens.
  • Turn the grant on only for clients that need it. GitHub makes each app enable the device flow, Google requires a separate client type for it, and Microsoft Entra ID's Conditional Access has an authentication flows condition that blocks it. Microsoft recommends blocking the device code flow wherever it isn't needed.
  • Client behaviour: start a flow only when the user asks for one, not when the app starts or after a failure (§3.1); honour interval and slow_down; back off after timeouts; stop on any other error.
  • Tokens on the device: keep the refresh token in the platform's secure storage, such as a keychain, keystore or the OS credential store (gh uses the system credential store and falls back to a plain-text file only when it can't use one). RFC 9700 requires refresh tokens for public clients to be sender-constrained or rotated on every use: see Refresh Token Rotation. If the device also needs to know who signed in, request openid and validate the ID token (OpenID Connect). Give each device its own grant, so that one TV can be signed out without signing the user out everywhere: the app revokes its own tokens on sign-out (RFC 7009), and the user or an admin can revoke that grant on the server. Sessions vs Tokens covers what revocation can and can't reach.
  • Vendor differences (checked October 2026):
    • Microsoft Entra ID: the endpoint is /oauth2/v2.0/devicecode; codes last 15 minutes by default; verification_uri_complete isn't supported; a declined request returns authorization_declined instead of access_denied; a refresh token comes back only when offline_access is in the scope.
    • Google: needs an OAuth client of the "TVs and Limited Input devices" type and allows only a short list of scopes (openid, email, profile, two Drive scopes and two YouTube scopes). The response calls the address verification_url (https://www.google.com/device), the user code is documented as case-sensitive and must be shown exactly as returned, and pending and slow_down answers come with HTTP 428 and 403, so read the error field rather than the status code.
    • GitHub: the device flow must be enabled in the app's settings; users enter an 8-character code at https://github.com/login/device within 15 minutes; slow_down adds 5 seconds; no client secret is needed.
  • Stronger cross-device options. RFC 10027 ranks FIDO cross-device authentication (a phone acting as a passkey authenticator, with proximity proven over Bluetooth) as the best protection, and notes it can be paired with the authorization code flow and PKCE to sign in a TV without the risks of this grant. When that isn't available, it points to CIBA (OpenID Connect Client-Initiated Backchannel Authentication Core 1.0, a final OpenID Foundation specification since September 2021): the device identifies the user, the provider contacts the user's registered device directly (a push notification, for example), and the device collects the tokens by poll, ping or push. With no code to pass along, CIBA is harder to phish, but someone who knows or guesses the user's identifier can still trigger requests.