Skip to content

Device pairing

Luis Vervaet edited this page Aug 7, 2026 · 1 revision

Pairing a device with no browser

Copying a long authorize URL onto a TV, a headless Pi or a serial console — and a longer callback URL back off it — is the awkward part of the flow above. So there is a second way in, shaped like RFC 8628: the device shows a short code, you approve it from a computer you already trust, and the session is delivered to the device.

This is additive. /v1/auth/login/start and /v1/auth/login/complete are unchanged and remain the normal way in.

This is not a device grant against DoorDash. DoorDash Identity does not implement RFC 8628. The grant is against ddREST, layered on the same paste-back login: a human still signs in through a real browser. The device never talks to DoorDash at all.

1. The device asks for a code.

curl -sX POST http://localhost:8787/v1/auth/pair/request \
  -H 'content-type: application/json' \
  -d '{"device_label":"Kitchen tablet"}'
{
  "device_code": "ddp1.…",
  "user_code": "BCDF-GHJK",
  "verification_uri": "http://localhost:8787/v1/auth/pair",
  "verification_uri_complete": "http://localhost:8787/v1/auth/pair?user_code=BCDF-GHJK",
  "expires_in": 600,
  "interval": 5
}

The device displays user_code and keeps device_code secret — that is what collects the session. verification_uri_complete prefills the code, so a device with a screen can render it as a QR code and skip the typing entirely.

2. The device polls, no faster than interval:

curl -sX POST http://localhost:8787/v1/auth/pair/token \
  -H 'content-type: application/json' \
  -d '{"device_code":"ddp1.…"}'

Until someone acts, that returns HTTP 400 with an RFC 8628 error code:

error Meaning
authorization_pending Nobody has approved yet. Keep polling.
slow_down You polled too fast. The new minimum is in interval.
access_denied A human refused. Stop.
expired_token The code expired unapproved. Start over.
invalid_grant Unknown device code, or the session was already collected. Stop.

Each body carries both error_description (what an off-the-shelf device-flow client reads) and message (this API's house style).

3. You approve it. Open /v1/auth/pair on a real computer and type the code. The page walks through the same sign-in-and-paste as the normal flow, then confirms. There is a Deny button next to Approve.

The page is plain server-rendered HTML with no JavaScript and no CDN — the browser you walk over to may itself be a console or a TV.

If you would rather script it, /v1/auth/pair/verify, /v1/auth/pair/complete and /v1/auth/pair/deny are the JSON equivalents of the three page steps. Note that /v1/auth/pair/complete does not return the session to you: you are approving access for someone else.

4. The device's next poll returns the session, in the same shape as /v1/auth/login/complete. It is delivered exactly once — the pairing is deleted on collection, so a replayed device code gets invalid_grant. No cookie is set.

Before you expose this

Device flows have one inherent weakness, and it is worth stating plainly: an attacker can start a pairing, then talk you into typing their code in. If you approve it, they get your account. Nothing server-side can fully prevent that, so the approval page says so in as many words and makes Deny as easy to reach as Approve.

Only ever approve a code you read off a device in front of you. If a code arrives by message, email or phone call, deny it.

The other attack — guessing a pending code — is handled by the code itself. They are eight characters from a 20-consonant alphabet (no vowels, so a code can never spell anything; no digits, so O/0 and I/1 cannot be confused), which is about 34.6 bits. Repeated wrong-but-well-formed guesses are throttled on top of that; malformed input is not, so fat-fingering the code will never lock you out.

Turn the whole feature off with PAIRING_ENABLED=false if you do not want it.

Pairing settings

Variable Default Notes
PAIRING_ENABLED true false makes every pairing endpoint 403 and the page unreachable.
PAIRING_CODE_TTL_SECONDS 600 (10m) How long a displayed code stays approvable. Must be at least 60 — it has to survive a whole browser login.
PAIRING_POLL_INTERVAL_SECONDS 5 Minimum seconds between polls. Faster earns a slow_down.
PAIRING_MAX_PENDING 100 Ceiling on unapproved pairings, since anyone can start one.
PAIRING_DB_PATH next to SESSION_DB_PATH e.g. /data/sessions-pairings.db.
PUBLIC_BASE_URL (derived from the request) Set this behind a reverse proxy. It is the address a device puts on screen for a human to walk to, so an internal hostname here is an address nobody can reach.

Clone this wiki locally