Device Authorization Flow

The Device Authorization Grant (RFC 8628) lets CLI tools, IoT devices, and headless environments authenticate a user without opening a browser on the device itself. The user completes the browser step on any other device (phone, laptop, etc.).

When to Use This Flow

  • You are building a CLI tool (my-tool login)
  • Your environment is headless — remote server over SSH, Docker container, CI runner
  • Opening a browser programmatically is impossible or inconvenient

The client is always public (no client_secret). Use code_challenge only if you’re doing the (rarer) PKCE-for-device variant — Signet does not require it here.

How It Works

sequenceDiagram participant CLI participant Signet participant Browser CLI->>Signet: POST /oauth/device/code (client_id, scope) Signet-->>CLI: device_code, user_code, verification_uri, interval note over CLI: Display verification_uri + user_code to user loop Poll every `interval` seconds CLI->>Signet: POST /oauth/token (device_code) Signet-->>CLI: authorization_pending end Browser->>Signet: Visit verification_uri Browser->>Signet: Enter user_code + approve CLI->>Signet: POST /oauth/token (device_code) Signet-->>CLI: access_token + refresh_token

Step 1: Request a Device Code

curl -X POST https://your-signet/oauth/device/code \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "scope=openid profile email offline_access"
Parameter Required Notes
client_id yes Public client with Device Flow enabled
scope no Space-separated; must be a subset of the client’s registered scopes. Omitted ⇒ defaults to the client’s full registered scope set. Include openid for an ID token
resource no RFC 8707 Resource Indicator. Absolute http(s) URI, no fragment, ≤ 1024 chars; repeat for multiple resources, max 10. When supplied, the issued access token’s aud is bound to these values — but each must be on your client’s allowed-resources allowlist (deny-all by default). Malformed or not-allowlisted values return invalid_target — see Errors

Example with resource indicators (MCP / multi-RS):

curl -X POST https://your-signet/oauth/device/code \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "scope=read" \
  -d "resource=https://api.example.com" \
  -d "resource=https://mcp.example.com"

When resource is present, the user sees a dedicated device confirm consent page listing both resources and must click Confirm and Authorize before the device code is marked authorized. The resulting access token’s aud is bound to the requested resources.

The consent recorded at /device/verify is stored per resource set: approving the same app for a different resource combination creates an independent grant instead of overwriting the earlier one, each grant is revocable on its own under Account → Authorizations, and revoking one only invalidates the tokens issued under it. (Earlier releases kept a single grant per app.)

Response:

{
  "device_code": "abc123...",
  "user_code": "WXYZ-1234",
  "verification_uri": "https://your-signet/device",
  "expires_in": 1800,
  "interval": 5
}

interval is the minimum poll interval. Respect slow_down (see below) to back off further.

Step 2: Display Instructions to the User

To sign in, visit:
    https://your-signet/device

And enter the code:
    WXYZ-1234

Waiting for authorization…

If a browser is available locally, open verification_uri automatically (but still print the URL and code in case auto-open fails):

// Go
_ = exec.Command("open", verificationURI).Start()      // macOS
_ = exec.Command("xdg-open", verificationURI).Start()  // Linux
_ = exec.Command("cmd", "/c", "start", verificationURI).Start() // Windows

A QR code of verification_uri (plus displaying the code) is a friendly touch for mobile users.

Step 3: Poll for the Token

curl -X POST https://your-signet/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
  -d "device_code=abc123..." \
  -d "client_id=YOUR_CLIENT_ID"

Narrowing resource (RFC 8707 §2.2): optionally pass resource=... here to bind the access token to a subset of what was granted at /oauth/device/code. Widening (passing a resource that wasn’t in the original grant) returns 400 invalid_target — the device code is not consumed, so the CLI may retry with a corrected list.

Success (user approved):

{
  "access_token": "eyJhbG...",
  "refresh_token": "def502...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile email offline_access"
}

Errors while polling (HTTP 400, shape {"error": "...", "error_description": "..."}):

error HTTP Meaning / Action
authorization_pending 400 User hasn’t approved yet — keep polling at interval
slow_down 400 Polling too fast — increase interval by ≥ 5 seconds
access_denied 400 User rejected the request — stop polling
expired_token 400 device_code past expires_in — restart from Step 1
invalid_grant 400 device_code unknown or already used — restart from Step 1
invalid_target 400 resource= on this token request is not a subset of the original device-code grant, or fails RFC 8707 shape validation. Device code is not consumed — retry with a corrected list. See Errors §Resource Indicator Errors

429 Too Many Requests is also possible — see Tokens & Revocation §Rate Limits. The full error catalog lives in Errors.

Step 4: Use the Access Token

curl -H "Authorization: Bearer ACCESS_TOKEN" https://api.example.com/resource

Step 5: Refresh the Access Token

When the access token nears expiry, trade the refresh token — see Tokens & Revocation §Refreshing Tokens. Read the rotation-mode reuse-detection gotcha before implementing retries.

Step 6: Sign Out

On my-tool logout, revoke the refresh token — deleting the local token file alone leaves a stolen token valid until expiry. See Tokens & Revocation §Sign Out.

Storing Tokens Locally

CLI conventions:

  • macOS: Keychain (e.g., security add-generic-password)
  • Linux: Secret Service (libsecret) or file in $XDG_CONFIG_HOME/<app>/token.json with 0600
  • Windows: Credential Manager

Never write refresh tokens to log output or debug traces.

Integration Checklist

  • [ ] client_id with Device Flow enabled by the admin
  • [ ] Respect interval and back off on slow_down
  • [ ] Restart flow on expired_token / access_denied
  • [ ] Store access & refresh tokens in OS-level secure storage
  • [ ] Revoke the refresh token on logout
  • [ ] Handle 429 rate-limit responses with backoff

Example CLI Client

github.com/go-signet/device-cli — complete Device Flow in Go.