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#
/oauth2/device_authorizationPubliccurl -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
}user_codeDisplay it. Large, and easy to copy by hand.
verification_uriDisplay it next to the code.
verification_uri_completeThe same page with the code already filled in. Use it for a QR code, never as the thing you ask someone to type.
device_codeKeep it. Never show it. It is the credential.
expires_inSeconds until the whole attempt dies. Stop polling and start over.
intervalSeconds to wait between polls.
Step 2: poll#
/oauth2/tokenClientcurl -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.
| Status | Code | Means |
|---|---|---|
| 400 | authorization_pending | Not yet. Wait interval seconds and poll again. |
| 400 | slow_down | You are polling too fast. Increase your interval and continue. |
| 400 | expired_token | The attempt timed out. Start again from step 1 with a new code. |
| 400 | access_denied | The 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: stopStep 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.
Rotation, what to store, and what happens if a rotated token is reused.
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#
Show the code and the URL together
Both, always. The complete URI is a convenience for a QR code, not a replacement.
Say how long it is good for
Derived from
expires_in, counting down.Do not make the person press anything to continue
You are already polling. The sign-in should complete on its own.
Offer a way to start over
An expired attempt is common when someone walks away. Getting a new code should be one button.