Device Authorization Grant
|
This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against. OAuth 2.1 is still an Internet-Draft ( This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
The device authorization grant (RFC 8628) exists for clients that can talk to the network but cannot reasonably render a browser or accept an inbound redirect at all: smart TVs, game consoles, streaming devices, printers, and headless CLIs running over SSH. Instead of redirecting the resource owner’s own user agent, the device displays a short code and asks the user to enter it on a second, capable device — a phone or laptop with a real browser. This page covers the full wire-level polling loop, the QR-code shortcut, and the cross-device phishing risk that shortcut introduces, together with RFC 10027’s mitigations.
The flow in one picture
Step 1 — the device requests a code
The device starts the flow by asking the authorization server for a pair of codes: one for itself, one for the human.
POST /device_authorization HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded
client_id=1b4f3c00-3baa
&scope=openid%20profile%20offline_access
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS",
"user_code": "WDJB-MJHT",
"verification_uri": "https://as.example.com/device",
"verification_uri_complete": "https://as.example.com/device?user_code=WDJB-MJHT",
"expires_in": 1800,
"interval": 5
}
| Field | Meaning |
|---|---|
|
A long, high-entropy value the device itself uses to poll the token endpoint. Never shown to the user. |
|
A short, human-typeable value (RFC 8628 recommends a restricted character set that avoids visually ambiguous characters) the user enters on the verification page. Correlates the user’s browser session to this specific device’s pending authorization. |
|
The URL the device tells the user to visit and type |
|
The same URL with |
|
Seconds until |
|
Minimum seconds the device must wait between polling requests; see [slow_down]. |
Step 2 — the device shows the code and starts polling
The device renders user_code and verification_uri prominently (and, optionally, a QR code encoding
verification_uri_complete), then immediately starts polling the token endpoint with the device authorization
grant, no earlier than every interval seconds:
POST /token HTTP/1.1
Host: as.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=1b4f3c00-3baa
The polling loop’s error responses
Every poll before the user finishes returns a 400 with one of these error values — each one tells the
device exactly what to do next:
error |
HTTP status | What the device does |
|---|---|---|
|
400 |
The user has not completed the verification step yet. Wait |
|
400 |
The device is polling too fast. Increase the polling interval by at least 5 seconds and continue — see [slow_down] below. |
|
400 |
The user explicitly declined the authorization request. Stop polling immediately and show a "declined" state; do not retry automatically. |
|
400 |
|
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store
{
"error": "authorization_pending"
}
|
|
Step 3 — the user completes verification on a second device
Independently of the device’s polling loop, the user opens verification_uri (or scans the QR code, landing
directly on verification_uri_complete) in a normal browser on their phone or laptop:
GET /device?user_code=WDJB-MJHT HTTP/1.1
Host: as.example.com
The authorization server authenticates the user through whichever
authentication method applies, then shows
a consent screen naming the device and the scope being requested — this device context is exactly what
RFC 10027 requires to be explicit, for the reasons covered next. Once the user approves, the authorization
server marks the underlying device_code as authorized; the device’s next poll succeeds:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"access_token": "2YotnFZFEjr1zCsicMWpAA",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "openid profile offline_access"
}
The QR-code shortcut
Typing an eight-character code on a TV remote is tedious enough that most real deployments render
verification_uri_complete as a QR code next to the on-screen user_code, letting the user’s phone camera
jump straight to the pre-filled verification page. This is a genuine usability win, but it changes the threat
model: a manually typed user_code requires the user to have read it off the actual device, while a QR code
can be photographed, printed, or displayed anywhere and scanned by someone who never saw the real device at
all — which is exactly the opening the next section covers.
Cross-device phishing and RFC 10027’s mitigations
Because the device showing the code and the browser completing verification are, by design, two different devices, an attacker can insert themselves in the middle: start a device-flow authorization on their own attacker-controlled "device" (which can be nothing more than a script), then trick a victim into completing that authorization on the victim’s own trusted browser — via a phishing e-mail, a fake tech-support call, or a QR code substituted at a public kiosk or printed sticker. The victim sees a real login page on the real authorization server, authenticates normally, and approves what looks like a legitimate request — but the resulting tokens go to the attacker’s device, not the victim’s.
RFC 10027 — the Best Current Practice for Security of Cross-Device Flows — addresses exactly this scenario with mitigations this page’s flow above already reflects:
-
Short-lived codes. A shorter
expires_in(RFC 10027 recommends minutes, not the 30 minutes shown in this page’s example, for higher-risk deployments) shrinks the window an attacker has to lure a victim into completing someone else’s authorization. -
Explicit device context on the consent screen. The verification page must tell the user identifiable information about which device is requesting authorization (device type, approximate location, or a descriptive name set at registration) rather than a bare "approve this login?" prompt — so a user asked to approve a "living-room TV" login while sitting at their desk has a concrete signal something is wrong.
-
Proximity checks. Where feasible, the authorization server correlates signals between the device requesting the code and the browser completing verification — for example, requiring them to be on the same local network, or comparing coarse IP geolocation — and either warns the user or refuses verification when they diverge sharply. This is the strongest mitigation but also the hardest to apply broadly, since it is incompatible with the flow’s core purpose of working across two genuinely separate devices and networks (a phone on mobile data verifying a TV on home Wi-Fi is a normal, legitimate case).
-
Rate limiting and anomaly detection on
device_authorizationrequests themselves, since a phishing campaign at scale needs to mint many device codes in a short window.
|
None of these mitigations make the device authorization grant equivalent in security to authorization code |
Implementation notes for the device
A few practical points that do not show up in the wire-level exchange but matter to anyone building the device side of this flow:
-
Poll on a background timer, not a tight loop. The device must respect
interval(and any laterslow_downadjustment) as a minimum, not a target — polling early risks the authorization server treating the device as abusive and terminating the flow outright rather than simply returningslow_down. -
Show a countdown or a "this code expires soon" cue as
expires_inruns low, so the user is not left staring at a code that silently stopped working. -
Restart cleanly on
expired_token. Request a brand-newdevice_authorizationpair rather than retrying the samedevice_code— it is dead the moment it expires, by design, to bound how long a leaked or shoulder-surfeduser_coderemains usable. -
Treat
access_deniedas final for this attempt. Do not silently loop back into polling; show the user a clear "sign-in was declined" state and let them explicitly choose to try again, which starts Step 1 over. -
Store the token the same way any other client would — see Access and Refresh Tokens for token storage and refresh-token rotation once the device has completed the flow; nothing about how the tokens are used afterwards differs from any other grant.
Device flow vs. authorization code + PKCE
Worth stating plainly, since the two are sometimes confused as interchangeable "browser-based" options:
| Authorization code + PKCE | Device authorization grant | |
|---|---|---|
Who completes login |
The same physical device running the client, in its own browser |
A second device, on a separate browser session |
Redirect required |
Yes — the client must receive an inbound redirect |
No — the device only polls outbound; no inbound connectivity needed at all |
Typical client |
Server-side web app, SPA, native/mobile app, interactive CLI with a local browser |
TV, console, kiosk, printer, headless CLI (SSH session, container shell) |
Phishing surface |
Standard redirect-URI and mix-up risks, covered on Security Best Practices |
Cross-device phishing via a substituted code or QR, covered above under RFC 10027 |
Correlation to the requesting device |
Implicit — the same browser that started the flow receives the redirect |
Explicit — the authorization server must show device context on the consent screen precisely because the correlation is not automatic |
Prefer authorization code + PKCE whenever the client can render a browser and receive a redirect at all; the device authorization grant is the fallback for the narrower set of clients that genuinely cannot, not a general-purpose alternative chosen for convenience.