Skip to content

The device flow

For anything with no browser. Show a short code, let the person approve it elsewhere, and poll until they do.

The device authorization grant is for a client that cannot open a browser: a calculator, a terminal tool, a set-top box. It is how Swoop signs in.

The idea is simple. The device shows a short code. The person types it on a machine that does have a browser. The device polls until that happens.

Step 1: ask for a code#

POST/oauth2/device_authorizationPublic
curl -s -X POST https://account.ceagle.cc/oauth2/device_authorization \
  -d client_id="$CLIENT_ID" \
  -d scope="openid profile offline_access"
{
  "device_code": "…",
  "user_code": "BQDF-JHNP",
  "verification_uri": "https://account.ceagle.cc/device",
  "verification_uri_complete": "https://account.ceagle.cc/device?user_code=BQDF-JHNP",
  "expires_in": 900,
  "interval": 5
}
What to do with each field
user_code

Display it. Large, and easy to copy by hand.

verification_uri

Display it next to the code.

verification_uri_complete

The same page with the code already filled in. Use it for a QR code, never as the thing you ask someone to type.

device_code

Keep it. Never show it. It is the credential.

expires_in

Seconds until the whole attempt dies. Stop polling and start over.

interval

Seconds to wait between polls.

Step 2: poll#

POST/oauth2/tokenClient
curl -s -X POST https://account.ceagle.cc/oauth2/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
  -d device_code="$DEVICE_CODE" \
  -d client_id="$CLIENT_ID"

Until the person approves, that answers:

{ "error": "authorization_pending" }

Which is not a failure. Keep going.

StatusCodeMeans
400authorization_pendingNot yet. Wait interval seconds and poll again.
400slow_downYou are polling too fast. Increase your interval and continue.
400expired_tokenThe attempt timed out. Start again from step 1 with a new code.
400access_deniedThe person refused. Do not retry.

Anything else is a real error.

Polling correctly#

delay = interval
loop:
    sleep(delay)
    answer = POST /oauth2/token
    if answer has access_token:      done
    if answer.error == "slow_down":  delay = delay + 5; continue
    if answer.error == "authorization_pending": continue
    otherwise: stop

Step 3: the tokens#

The success response is the ordinary token response:

{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 600,
  "refresh_token": "…",
  "id_token": "…",
  "scope": "openid profile offline_access"
}

Ask for offline_access, because a device that has to run the whole flow again every ten minutes is a device nobody signs in to twice.

Refresh tokens

Rotation, what to store, and what happens if a rotated token is reused.

Read on

Not gated by human verification#

The device endpoints are deliberately outside the Turnstile gate. There is no browser on the device side, so there would be nobody to solve a challenge.

They keep the rate limiter and client authentication.

Designing the screen#

  1. Show the code and the URL together

    Both, always. The complete URI is a convenience for a QR code, not a replacement.

  2. Say how long it is good for

    Derived from expires_in, counting down.

  3. Do not make the person press anything to continue

    You are already polling. The sign-in should complete on its own.

  4. Offer a way to start over

    An expired attempt is common when someone walks away. Getting a new code should be one button.

CEagle documentation