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
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.
The device reads /.well-known/openid-configuration. Endpoints are discovered, never hardcoded.
It POSTs to the device authorization endpoint with its client_id and gets back a user code, a verification URI and a device code.
It shows the code, plus a QR of the verification URI with the code already embedded — so scanning skips the typing entirely.
You open it on your phone, sign in with a passkey, and confirm the code matches the one on the device's screen.
The device — polling the token endpoint the whole time — receives an access token, a refresh token and an ID token.
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.
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.
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);
Clone the demo, sign in with your own ZapQR account, then point it at your hardware. It is MIT licensed — take it apart.