---
# === IDENTITY ===
id: software/patterns/oauth2-device-flow/2026
canonical_question: "How do I implement OAuth2 Device Authorization flow?"
aliases:
  - "OAuth2 device flow"
  - "device authorization grant"
  - "RFC 8628 implementation"
  - "device code flow"
  - "OAuth for smart TV"
  - "OAuth for CLI tools"
  - "input-constrained device authentication"
  - "OAuth2 device code grant"
entity_type: software_reference
domain: software > patterns > oauth2_device_flow
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-02-24
confidence: 0.90
version: 1.0
first_published: 2026-02-24

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "RFC 8628 published 2019-08; no breaking changes since — protocol is stable"
  next_review: 2026-08-23
  change_sensitivity: low

# === CONSTRAINTS ===
constraints:
  - "Polling interval MUST be at least the server-provided 'interval' value (default 5 seconds) — polling faster triggers slow_down error and further delays"
  - "device_code has a finite lifetime (expires_in) — typically 15-30 minutes; after expiry the entire flow must restart from Step 1"
  - "user_code MUST be displayed to the user exactly as received — agents must not reformat, lowercase, or strip hyphens from the display value"
  - "Client MUST handle slow_down by adding 5 seconds to current interval — this is cumulative, not a one-time backoff"
  - "grant_type MUST be the full URN: urn:ietf:params:oauth:grant-type:device_code — not a short name"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Application has a browser and can handle redirects (web app, mobile app with webview)"
    use_instead: "Authorization Code with PKCE — more secure, better UX for browser-capable devices"
  - condition: "No user interaction needed — machine-to-machine authentication"
    use_instead: "Client Credentials grant — simpler, no user involvement required"
  - condition: "Device has a keyboard and browser but is a single-page application"
    use_instead: "Authorization Code with PKCE for SPAs"

# === AGENT HINTS ===
inputs_needed:
  - key: "identity_provider"
    question: "Which identity provider are you using?"
    type: choice
    options: ["Auth0", "Microsoft Entra ID", "Google", "Okta", "Keycloak", "Custom"]
  - key: "device_type"
    question: "What type of device is authenticating?"
    type: choice
    options: ["Smart TV", "CLI tool", "IoT device", "Game console", "Set-top box", "Kiosk"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/patterns/oauth2-device-flow/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-24)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/patterns/oauth2-authorization-code-pkce/2026"
      label: "OAuth2 Authorization Code with PKCE"
    - id: "software/patterns/oauth2-client-credentials/2026"
      label: "OAuth2 Client Credentials Grant"
    - id: "software/patterns/jwt-implementation/2026"
      label: "JWT Implementation Guide"
  solves: []
  alternative_to:
    - id: "software/patterns/oauth2-authorization-code-pkce/2026"
      label: "OAuth2 Authorization Code with PKCE (for browser-capable devices)"
  often_confused_with:
    - id: "software/patterns/oauth2-client-credentials/2026"
      label: "OAuth2 Client Credentials (machine-to-machine, no user interaction)"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "RFC 8628 — OAuth 2.0 Device Authorization Grant"
    author: IETF
    url: https://datatracker.ietf.org/doc/html/rfc8628
    type: rfc_spec
    published: 2019-08-01
    reliability: authoritative
  - id: src2
    title: "Device Authorization Flow — Auth0 Documentation"
    author: Auth0
    url: https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow
    type: official_docs
    published: 2025-01-15
    reliability: high
  - id: src3
    title: "OAuth 2.0 Device Authorization Grant — Microsoft Identity Platform"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-device-code
    type: official_docs
    published: 2025-06-15
    reliability: high
  - id: src4
    title: "OAuth 2.0 for TV and Limited-Input Device Applications — Google Identity"
    author: Google
    url: https://developers.google.com/identity/protocols/oauth2/limited-input-device
    type: official_docs
    published: 2025-01-10
    reliability: high
  - id: src5
    title: "OAuth 2.0 Device Authorization Grant — OAuth.net"
    author: OAuth.net
    url: https://oauth.net/2/device-flow/
    type: community_resource
    published: 2024-06-01
    reliability: high
  - id: src6
    title: "OAuth 2.0 Device Flow Explained — Curity"
    author: Curity
    url: https://curity.io/resources/learn/oauth-device-flow/
    type: technical_blog
    published: 2024-03-15
    reliability: moderate_high
  - id: src7
    title: "Device Authorization Grant: Solving OAuth for screens without keyboards — WorkOS"
    author: WorkOS
    url: https://workos.com/blog/oauth-device-authorization-grant
    type: technical_blog
    published: 2024-09-10
    reliability: moderate_high
---

# OAuth2 Device Authorization Flow: Complete Reference

## TL;DR

- **Bottom line**: The Device Authorization Grant (RFC 8628) lets input-constrained devices (smart TVs, CLI tools, IoT) authenticate users by displaying a short code the user enters on a separate browser-capable device.
- **Key tool/command**: `POST /device/code` to get user_code + verification_uri, then poll `POST /token` with `grant_type=urn:ietf:params:oauth:grant-type:device_code`
- **Watch out for**: Polling the token endpoint too fast — exceeding the `interval` triggers `slow_down` which cumulatively adds 5 seconds per violation.
- **Works with**: Any OAuth 2.0 provider that supports RFC 8628 (Auth0, Microsoft Entra ID, Google, Okta, Keycloak, GitHub).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Polling interval MUST respect the server-returned `interval` value (default: 5 seconds). Each `slow_down` error adds 5 seconds cumulatively. [src1]
- The `device_code` expires after `expires_in` seconds (typically 900-1800s). After expiry, the client MUST restart the entire flow. [src1]
- The `user_code` MUST be displayed exactly as received. Comparison should be case-insensitive and strip non-alphanumeric characters, but display must preserve the original format. [src1]
- The `grant_type` value MUST be the full URN string `urn:ietf:params:oauth:grant-type:device_code` — using any abbreviation will fail. [src1]
- On connection timeout during polling, clients MUST apply exponential backoff (recommended: double the interval). [src1]
- Device flow clients are public clients (cannot store secrets securely) — never embed client_secret in distributed device applications. [src6]

## Quick Reference

| Step | Endpoint | Method | Key Parameters | Response Fields | Notes |
|---|---|---|---|---|---|
| 1. Request device code | `/device/code` (or `/oauth/device/code`) | POST | `client_id`, `scope` | `device_code`, `user_code`, `verification_uri`, `verification_uri_complete`, `expires_in`, `interval` | Content-Type: application/x-www-form-urlencoded |
| 2. Display to user | N/A (device screen) | — | — | — | Show `user_code` + `verification_uri`; optionally render QR code from `verification_uri_complete` |
| 3. User opens browser | `verification_uri` | GET (browser) | — | — | User navigates on phone/laptop |
| 4. User enters code | `verification_uri` | POST (browser) | `user_code` | — | User types the displayed code |
| 5. User authenticates | IdP login page | POST (browser) | credentials | — | Standard login + consent screen |
| 6. Poll for token | `/token` (or `/oauth/token`) | POST | `grant_type=urn:ietf:params:oauth:grant-type:device_code`, `device_code`, `client_id` | `access_token`, `refresh_token`, `token_type`, `expires_in` | Repeat every `interval` seconds |
| 7. Handle poll errors | `/token` | POST | (same) | `error`: `authorization_pending` | Keep polling — user hasn't acted yet |
| 8. Handle slow_down | `/token` | POST | (same) | `error`: `slow_down` | Increase interval by 5 seconds, then continue |
| 9. Handle access_denied | `/token` | POST | (same) | `error`: `access_denied` | User denied — stop polling, show error |
| 10. Handle expired_token | `/token` | POST | (same) | `error`: `expired_token` | Code expired — restart from Step 1 |
| 11. Use access token | Resource API | GET/POST | `Authorization: Bearer {access_token}` | API response | Standard OAuth bearer token usage |
| 12. Refresh token | `/token` | POST | `grant_type=refresh_token`, `refresh_token`, `client_id` | New `access_token` | Use when access_token expires |

## Decision Tree

```
START — Does the device have a browser and keyboard?
├── YES → Use Authorization Code with PKCE (not device flow)
│
└── NO → Is there a user who needs to authenticate?
    ├── NO → Use Client Credentials grant (machine-to-machine)
    │
    └── YES → Can the device display a URL and short code?
        ├── NO → Consider out-of-band setup (pre-provisioned tokens)
        │
        └── YES → Use Device Authorization Flow (this unit)
            ├── Does the IdP support verification_uri_complete?
            │   ├── YES → Display QR code (better UX) + fallback text code
            │   └── NO → Display verification_uri + user_code as text
            │
            └── Device type?
                ├── Smart TV / Game Console → Full device flow with on-screen code
                ├── CLI Tool → Print code to terminal, open browser with xdg-open/open
                └── IoT / Headless → Send code via display, LED, or companion app
```

## Step-by-Step Guide

### 1. Register your application with the identity provider

Register a public client application with your OAuth2 provider. Enable the Device Authorization Grant. Note your `client_id` — you will not have a `client_secret` for public clients. [src2]

```bash
# Auth0: Enable Device Code grant in Application Settings > Advanced > Grant Types
# Microsoft: Register app in Azure Portal > App registrations > Authentication > Allow public client flows = Yes
# Google: Create OAuth credentials at console.cloud.google.com > APIs & Services > Credentials
```

**Verify**: Check your IdP dashboard confirms Device Code grant is enabled for the application.

### 2. Request a device code from the authorization server

Send a POST request to the device authorization endpoint with your `client_id` and desired `scope`. [src1]

```bash
curl -X POST "https://YOUR_IDP/device/code" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "scope=openid profile email"
```

**Verify**: Response includes `device_code`, `user_code`, `verification_uri`, `expires_in`, and `interval`:

```json
{
  "device_code": "GmRhm...long_opaque_string",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://YOUR_IDP/activate",
  "verification_uri_complete": "https://YOUR_IDP/activate?user_code=WDJB-MJHT",
  "expires_in": 1800,
  "interval": 5
}
```

### 3. Display the user code and verification URI

Show the `user_code` and `verification_uri` on the device screen. If `verification_uri_complete` is available, render a QR code for it. [src1] [src6]

```
===========================================
  To sign in, visit: https://idp.example/activate
  Enter code: WDJB-MJHT

  [QR CODE pointing to verification_uri_complete]

  This code expires in 30 minutes.
===========================================
```

**Verify**: User can visually read the code and URL from the device screen.

### 4. Poll the token endpoint for authorization

Start polling the token endpoint at the `interval` specified in the device code response. Handle all error codes correctly. [src1] [src3]

```python
import time
import requests

def poll_for_token(token_url, device_code, client_id, interval=5):
    """Poll token endpoint until user authorizes or code expires."""
    while True:
        time.sleep(interval)

        resp = requests.post(token_url, data={
            "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
            "device_code": device_code,
            "client_id": client_id,
        })

        data = resp.json()

        if resp.status_code == 200:
            return data  # Success: contains access_token, refresh_token, etc.

        error = data.get("error")
        if error == "authorization_pending":
            continue  # User hasn't acted yet — keep polling
        elif error == "slow_down":
            interval += 5  # MUST increase by 5 seconds
            continue
        elif error == "access_denied":
            raise Exception("User denied authorization")
        elif error == "expired_token":
            raise Exception("Device code expired — restart flow")
        else:
            raise Exception(f"Unexpected error: {error}")
```

**Verify**: `poll_for_token()` returns a dict with `access_token` after user completes browser authorization.

### 5. Handle the successful token response

Store the `access_token` and `refresh_token` securely. Use the access token for API calls. [src3]

```python
tokens = poll_for_token(token_url, device_code, client_id, interval)

access_token = tokens["access_token"]
refresh_token = tokens.get("refresh_token")
expires_in = tokens["expires_in"]

# Use the access token
headers = {"Authorization": f"Bearer {access_token}"}
api_response = requests.get("https://api.example.com/resource", headers=headers)
```

**Verify**: API call returns 200 OK with the protected resource data.

### 6. Implement token refresh

When the access token expires, use the refresh token to obtain a new one without re-running the device flow. [src3]

```python
def refresh_access_token(token_url, refresh_token, client_id):
    resp = requests.post(token_url, data={
        "grant_type": "refresh_token",
        "refresh_token": refresh_token,
        "client_id": client_id,
    })
    if resp.status_code == 200:
        return resp.json()
    raise Exception(f"Token refresh failed: {resp.json()}")
```

**Verify**: New `access_token` and optionally new `refresh_token` returned.

## Code Examples

### Python: Complete Device Flow Client

> Full script: [python-complete-device-flow-client.py](scripts/python-complete-device-flow-client.py) (48 lines)

```python
# Input:  client_id, scopes, IdP endpoints
# Output: access_token + refresh_token
import time
import requests
import sys
# ... (see full script)
```

### Node.js: Complete Device Flow Client

> Full script: [node-js-complete-device-flow-client.js](scripts/node-js-complete-device-flow-client.js) (39 lines)

```javascript
// Input:  clientId, scopes, IdP endpoints
// Output: { access_token, refresh_token, expires_in }
async function deviceFlowAuth(clientId, deviceCodeUrl, tokenUrl, scope = "openid profile") {
  // Step 1: Request device code
  const codeResp = await fetch(deviceCodeUrl, {
# ... (see full script)
```

### Go: Complete Device Flow Client

> Full script: [go-complete-device-flow-client.go](scripts/go-complete-device-flow-client.go) (80 lines)

```go
// Input:  clientID, scopes, IdP endpoints
// Output: TokenResponse with AccessToken, RefreshToken
package main
import (
    "encoding/json"
# ... (see full script)
```

## Anti-Patterns

### Wrong: Polling without respecting the interval

```python
# BAD — polls as fast as possible, will trigger slow_down and get rate-limited
while True:
    resp = requests.post(token_url, data=token_data)
    if resp.status_code == 200:
        break
```

### Correct: Always sleep for the specified interval before each poll

```python
# GOOD — respects server-specified interval and handles slow_down
interval = auth.get("interval", 5)
while True:
    time.sleep(interval)  # Always wait BEFORE polling
    resp = requests.post(token_url, data=token_data)
    data = resp.json()
    if resp.status_code == 200:
        break
    if data.get("error") == "slow_down":
        interval += 5  # Cumulative increase per RFC 8628
```

### Wrong: Ignoring the slow_down error

```javascript
// BAD — treats slow_down same as authorization_pending
if (data.error === "authorization_pending" || data.error === "slow_down") {
  continue; // interval stays the same — server will keep returning slow_down
}
```

### Correct: Incrementing interval on slow_down

```javascript
// GOOD — adds 5 seconds on slow_down per RFC 8628 Section 3.5
if (data.error === "slow_down") {
  interval += 5000; // cumulative +5s in milliseconds
  continue;
}
```

### Wrong: Hardcoding the verification URI

```python
# BAD — verification_uri may change; different tenants have different URIs
VERIFY_URL = "https://microsoft.com/devicelogin"
print(f"Go to {VERIFY_URL}")
```

### Correct: Using the URI from the device code response

```python
# GOOD — always use the URI returned by the authorization server
auth = request_device_code(client_id, scope)
print(f"Go to {auth['verification_uri']}")  # Dynamic from server response
```

### Wrong: Embedding client_secret in distributed device apps

```python
# BAD — device apps are public clients; secrets can be extracted from binaries
resp = requests.post(device_code_url, data={
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET,  # NEVER do this on a distributed device
    "scope": "openid",
})
```

### Correct: Using public client flow without secrets

```python
# GOOD — device flow is designed for public clients (no secret)
resp = requests.post(device_code_url, data={
    "client_id": CLIENT_ID,  # Only client_id, no secret
    "scope": "openid",
})
```

## Common Pitfalls

- **Polling before the first interval elapses**: The RFC requires waiting `interval` seconds before the first poll, not just between polls. Polling immediately after receiving the device code will waste a request. Fix: `time.sleep(interval)` before the first token request. [src1]
- **Not handling `verification_uri_complete`**: Many IdPs return this field which includes the user_code in the URL. Displaying a QR code for it dramatically improves UX on TVs. Fix: check for `verification_uri_complete` and render a QR code when available. [src4]
- **Forgetting to check `expires_in`**: If the user never completes authorization, your polling loop runs forever. Fix: track `expires_in` and break the loop with a clear error when the deadline passes. [src1]
- **Using wrong grant_type string**: The grant type is the full URN `urn:ietf:params:oauth:grant-type:device_code`, not `device_code` or `device_authorization`. Fix: use the exact string from the RFC. [src1]
- **Not requesting offline_access scope**: Without this scope (or equivalent), many providers won't issue a refresh_token, forcing the user to re-authenticate when the access_token expires. Fix: include `offline_access` in the scope parameter. [src3]
- **Displaying user_code in lowercase**: Some providers generate case-sensitive codes. Even if comparison is case-insensitive, display must match the server response. Fix: display `user_code` exactly as received. [src1]
- **Not implementing exponential backoff on network errors**: Connection timeouts during polling should trigger backoff, not just retry at the same interval. Fix: double the interval on each consecutive network error. [src1]

## Diagnostic Commands

```bash
# Test device code endpoint (replace with your IdP URL and client_id)
curl -s -X POST "https://YOUR_IDP/device/code" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID&scope=openid" | jq .

# Test token polling (will return authorization_pending before user acts)
curl -s -X POST "https://YOUR_IDP/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code=YOUR_DEVICE_CODE&client_id=YOUR_CLIENT_ID" | jq .

# Verify access token (decode JWT without verification — for debugging only)
echo "YOUR_ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# Check token expiry from JWT claims
echo "YOUR_ACCESS_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.exp | todate'

# Test IdP discovery endpoint (OpenID Connect)
curl -s "https://YOUR_IDP/.well-known/openid-configuration" | jq '.device_authorization_endpoint'
```

## Version History & Compatibility

| Version / Standard | Status | Key Changes | Notes |
|---|---|---|---|
| RFC 8628 (Aug 2019) | Current, Stable | Finalized the Device Authorization Grant | Replaced earlier draft-ietf-oauth-device-flow |
| Draft -15 (2018) | Superseded | Last draft before RFC publication | Minor editorial changes from draft to RFC |
| Google Device Flow (2016+) | Proprietary predecessor | Pre-RFC implementation; now aligned with RFC 8628 | Uses `https://oauth2.googleapis.com/device/code` |
| Microsoft Device Code Flow | RFC 8628 aligned | Does not support `verification_uri_complete` | Uses `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode` |
| Auth0 Device Flow | RFC 8628 aligned | Full RFC support including `verification_uri_complete` | Uses `https://{domain}/oauth/device/code` |
| GitHub Device Flow | RFC 8628 aligned | Used for CLI authentication (`gh auth login`) | Uses `https://github.com/login/device/code` |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Smart TV, streaming device, game console needs user auth | Device has a full browser and keyboard | Authorization Code with PKCE |
| CLI tool needs to authenticate a human user | Service-to-service (no human user) | Client Credentials grant |
| IoT device with a screen but no keyboard | Mobile app with WebView support | Authorization Code with PKCE (mobile) |
| Digital signage or kiosk in restricted input mode | Single-page web application | Authorization Code with PKCE (SPA) |
| Set-top box or media player needs user login | Server-side web application | Authorization Code (standard) |
| Headless device that can display/communicate a code | Device with no display and no way to show a code to user | Pre-provisioned credentials or bootstrap token |

## Important Caveats

- **Provider endpoint differences**: Each IdP uses slightly different endpoint paths. Always check the `/.well-known/openid-configuration` discovery document for the `device_authorization_endpoint` field rather than hardcoding paths. [src1]
- **Scope limitations**: Google's device flow supports only a limited set of scopes. Not all OAuth scopes available in other flows may work with device flow. Check your IdP's documentation for device-flow-specific scope restrictions. [src4]
- **No `verification_uri_complete` on Microsoft**: Microsoft Entra ID does not support the optional `verification_uri_complete` field, so QR code flows require manual construction. [src3]
- **User code entropy**: RFC 8628 recommends 8-character base-20 codes (using consonants BCDFGHJKLMNPQRSTVWXZ) for ~34.5 bits of entropy. Shorter or numeric-only codes reduce security against brute-force attacks. [src1]
- **Phishing risk**: An attacker could trick a user into entering a device code on a legitimate verification page, authorizing the attacker's device. Mitigation: display device/location context during the authorization step. [src1]
- **No PKCE integration**: Device flow does not use PKCE (Proof Key for Code Exchange). Security relies on the short lifetime of the device code and the entropy of both device_code and user_code. [src1]

## Related Units

- [OAuth2 Authorization Code with PKCE](/software/patterns/oauth2-authorization-code-pkce/2026)
- [OAuth2 Client Credentials Grant](/software/patterns/oauth2-client-credentials/2026)
- [JWT Implementation Guide](/software/patterns/jwt-implementation/2026)
