feat: device-auth backend for codeless sign-in (app #12) #3

Merged
richiexec merged 1 commit from feat/device-auth into main 2026-07-19 17:34:36 +00:00
Collaborator

Backend half of codeless device sign-in (amber-app issue #12). Pairs with amber-app PR feature/codeless-signin.

What's here

  • device_auth collection (migration) — code (unique short), deviceName, status, user→users, expiresAt, + hidden authToken / keyCiphertext / pollSecret / devicePubKey. Collection REST rules are owner-scoped list/view/delete (= the device list + revoke); create/update are locked to the hooks.
  • pb_hooks/device_auth.pb.js — the handshake:
    • POST /api/device-auth/request (public) → {id, code, pollSecret, expiresAt, ttl} (5-min TTL).
    • GET /api/device-auth/info?code= (public, non-secret) → deviceName + the TV's ephemeral pubkey + status (what the approve page needs).
    • GET /api/device-auth/poll?id=&secret= (pollSecret-gated) → status; on approved, delivers {token, keyCiphertext, record} — the only place secrets leave the server, and only to the pollSecret holder (the TV). 120s pickup window.
    • POST /api/device-auth/approve (auth required) → mints e.auth.newAuthToken() for the caller, stores the E2E-sealed vault key.
    • POST /api/device-auth/decline (auth) → denies + scrubs.
    • cleanup cron scrubs delivered secrets + drops stale rows; approved rows remain as the device list.
  • pb_hooks/device_page.pb.js — self-contained GET /device?code= approve page: signs the user in, shows "Approve ?", and seals the addon-config vault key to the TV's pubkey.
  • docs/device-auth-contract.md — full contract + crypto spec.

Crypto (the load-bearing part)

The approve page (where the user does type their password) derives the #20 vault key — PBKDF2-HMAC-SHA256, 210000 iters, 256-bit, account salt — and transfers it to the TV end-to-end so the server only ever sees opaque ciphertext: X25519 → HKDF-SHA256 → AES-256-GCM. Proven to match the app byte-for-byte with committed interop fixtures (a Node run of the page's exact inlined crypto seals vectors the Dart tests open).

Deploy

Merge + redeploy on Coolify (Dockerfile now ships pb_hooks via --hooksDir; the migration auto-applies on boot). python scripts/verify.py against the live URL includes new device_auth checks; https://pb.petruzalekr.cz/device?code=TEST should render the approve page. On-device E2E happens with the app build after this deploys.

Known limitation

PocketBase JWTs are stateless — revoking a device drops it from the list + stops polling but can't invalidate an already-minted token before expiry. Documented as a follow-up.

🤖 Generated with Claude Code

Backend half of **codeless device sign-in** (amber-app issue #12). Pairs with amber-app PR `feature/codeless-signin`. ## What's here - **`device_auth` collection** (migration) — `code` (unique short), `deviceName`, `status`, `user`→users, `expiresAt`, + **hidden** `authToken` / `keyCiphertext` / `pollSecret` / `devicePubKey`. Collection REST rules are owner-scoped list/view/delete (= the device list + revoke); create/update are locked to the hooks. - **`pb_hooks/device_auth.pb.js`** — the handshake: - `POST /api/device-auth/request` (public) → `{id, code, pollSecret, expiresAt, ttl}` (5-min TTL). - `GET /api/device-auth/info?code=` (public, non-secret) → deviceName + the TV's ephemeral pubkey + status (what the approve page needs). - `GET /api/device-auth/poll?id=&secret=` (**pollSecret-gated**) → status; on approved, delivers `{token, keyCiphertext, record}` — the **only** place secrets leave the server, and only to the `pollSecret` holder (the TV). 120s pickup window. - `POST /api/device-auth/approve` (**auth required**) → mints `e.auth.newAuthToken()` for the caller, stores the E2E-sealed vault key. - `POST /api/device-auth/decline` (auth) → denies + scrubs. - cleanup cron scrubs delivered secrets + drops stale rows; approved rows remain as the device list. - **`pb_hooks/device_page.pb.js`** — self-contained `GET /device?code=` approve page: signs the user in, shows "Approve <deviceName>?", and seals the addon-config vault key to the TV's pubkey. - **`docs/device-auth-contract.md`** — full contract + crypto spec. ## Crypto (the load-bearing part) The approve page (where the user *does* type their password) derives the #20 vault key — **PBKDF2-HMAC-SHA256, 210000 iters, 256-bit**, account salt — and transfers it to the TV **end-to-end** so the server only ever sees opaque ciphertext: **X25519 → HKDF-SHA256 → AES-256-GCM**. Proven to match the app byte-for-byte with committed interop fixtures (a Node run of the page's exact inlined crypto seals vectors the Dart tests open). ## Deploy Merge + redeploy on Coolify (Dockerfile now ships `pb_hooks` via `--hooksDir`; the migration auto-applies on boot). `python scripts/verify.py` against the live URL includes new device_auth checks; `https://pb.petruzalekr.cz/device?code=TEST` should render the approve page. On-device E2E happens with the app build after this deploys. ## Known limitation PocketBase JWTs are stateless — revoking a device drops it from the list + stops polling but can't invalidate an already-minted token before expiry. Documented as a follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude added 1 commit 2026-07-19 12:39:29 +00:00
A new device (TV) signs into an account with no password typed on it and
no pairing code, approved from an already-logged-in phone/PC.

- device_auth collection (migration): code (unique), deviceName, status,
  user, expiresAt + hidden authToken/keyCiphertext/pollSecret/devicePubKey.
  Owner-scoped list/view/delete only (the device list + revoke); create/
  update locked to the hooks.
- pb_hooks/device_auth.pb.js: public request + info + pollSecret-gated
  poll routes (the TV is unauthenticated), auth-gated approve (mints
  e.auth.newAuthToken(), stores the E2E vault-key ciphertext) + decline,
  and a cleanup cron that scrubs delivered tokens and stale rows.
- pb_hooks/device_page.pb.js: self-contained approve page at GET /device.
  Signs in, derives the addon-config vault key (#20) with WebCrypto PBKDF2,
  seals {salt,keyB64} to the TV's X25519 pubkey (vendored TweetNaCl +
  WebCrypto HKDF/AES-GCM), and approves. The server never sees the key.
- Dockerfile ships pb_hooks (--hooksDir); verify.py covers the schema +
  that the routes are served.

Crypto matches the app byte-for-byte (proven by the app's fixed-vector
tests). Full contract: docs/device-auth-contract.md. JWT-revocation
limitation documented (v1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
richiexec merged commit 4cc5279842 into main 2026-07-19 17:34:36 +00:00
richiexec deleted branch feat/device-auth 2026-07-19 17:34:36 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: richiexec/amber-backend#3
No description provided.