RFC 8628 · DEVICE AUTHORIZATION GRANT

Sign in on screens
with no keyboard

Kiosks. TVs. Cars. Staff terminals. Digital signage. A sensor panel in a plant room. None of them can hold a password, and most of them have no browser to run a login page in.

So the device shows a short code, a human approves it on their phone, and the device ends up holding real OAuth tokens. No password, no passkey on the device, and no secret ever reaches it.

MIT licensed · stdlib Python only · nothing to pip install on the device

reading
/.well-known/
openid-configuration

SIGN IN AT

auth.zapqr.ai/device

BCDF-GHJK

waiting for approval

Approved

access + refresh + id_token

REGISTERED TO

you@example.com

device holds tokens · no secret stored

UNIHIKER

240×320 · no keyboard · no browser

Standards, not a ZapQR protocol

This is the same grant Apple TV, Roku and gh auth login use. Any OAuth SDK on any platform already speaks it — there is nothing ZapQR-specific to learn, and nothing to port when you change hardware.

Nothing to install

Python standard library only. It runs on a microcontroller-class board, and on your laptop it prints the flow to the terminal instead.

No secret on the device

Device clients are public clients. Anyone with the firmware has whatever it holds, so it holds nothing worth taking.

The human is the factor

Approval happens on a phone that already has a passkey. The code on the screen is what ties that approval to this device.

What actually happens

  1. 01

    The device reads /.well-known/openid-configuration. Endpoints are discovered, never hardcoded.

  2. 02

    It POSTs to the device authorization endpoint with its client_id and gets back a user code, a verification URI and a device code.

  3. 03

    It shows the code, plus a QR of the verification URI with the code already embedded — so scanning skips the typing entirely.

  4. 04

    You open it on your phone, sign in with a passkey, and confirm the code matches the one on the device's screen.

  5. 05

    The device — polling the token endpoint the whole time — receives an access token, a refresh token and an ID token.

Run it in ten minutes

The demo ships preconfigured against a public ZapQR client, so you sign in with your own ZapQR account. You do not need the hardware — without the display library it prints to the terminal, which is the fastest way to watch the flow before you touch a board.

git clone https://github.com/dasecure/zapqr-unihiker-demo
cd zapqr-unihiker-demo
python3 zapqr_device_login.py

Optional: a scannable QR

pip install qrcode pillow puts a QR on the screen. Without it the flow is identical — you just type the code.

Booth mode

--loop replaces an expired code immediately so the screen is never dead, holds the welcome for fifteen seconds, then resets for the next person.

Notes for real hardware

Things that only show up once the code is running on something small.

Screen size drives the design

The UNIHIKER is 240×320. User codes use a base-20 charset — no vowels, no lookalike characters — so a code misread off a small screen can't be mistyped into a different valid code. Keep the verification URI short too: every character raises QR density on a display with none to spare.

Poll politely

The server sets interval; slow_down means add to it. The example honours both. Hammering the token endpoint is the fastest way to get a device fleet rate-limited.

The device needn't verify the ID token

It already trusts the token endpoint it just called over TLS. Signature verification matters when a token arrives from somewhere else — worth skipping on a microcontroller where RSA verification is expensive.

Why it's a public client

An ESP32 cannot keep a client secret. So device clients are registered public — no secret — and constrained by their registered grant types plus the human approving the code. That confirmation screen, where you match the code, is what stands in for the missing secret.

Registering your own device fleet

The self-serve dashboard covers web clients; device clients are admin-registered for now. device_flow is what adds the device-code grant to a client's allowed grants — it is off by default, deliberately.

insert into oidc_clients
  (client_id, name, redirect_uris, post_logout_redirect_uris, site_type, device_flow)
values ('my-device-fleet', 'My Device Fleet', '{}', '{}', 'device', true);
Ask us to register one for you →

Put it on your own screen

Clone the demo, sign in with your own ZapQR account, then point it at your hardware. It is MIT licensed — take it apart.