# Passkeys, sessions and device approval

Status: implemented platform-independent account policy under the user's request for multiple passkeys, device management and TV/new-device approval. Worker persistence, HTTP routes, cookie delivery and UI verification are separate integration evidence recorded in DEVLOG.md. This is an issuer-local login flow, not a claim of OAuth device-grant interoperability.

An owner can enroll up to five verified passkeys, inspect safe credential metadata, rename labels and remove a credential. Enrollment is a fresh WebAuthn registration ceremony bound to the exact RP/origin, account, expiring challenge and initiating owner session. Enrollment, removal and session revocation require a session authenticated within 15 minutes and exact-origin/session CSRF checks. Removing a passkey requires a remaining working password or passkey and a configured recovery-file hash. Removing a credential revokes login sessions created using that credential. A verified email remains contact-only unless its owner explicitly enables email login. Email login is disabled by default; disabling it invalidates outstanding links, while file recovery remains unchanged. Passkey removal does not treat contact verification as a replacement working credential.

Sessions have opaque management IDs distinct from their authentication tokens. Device lists contain label, creation, last-use and expiry times plus a current-session marker. Session-token hashes and other sessions' CSRF tokens never leave this view. Labels are bounded, untrusted plain text rather than verified hardware descriptions. Existing sessions receive stable derived public IDs without exposing their token hashes. Owners may revoke one selected session, all others or their current session. Last-use persistence is throttled to once per minute; concurrent telemetry conflicts do not fail reads.

The requesting device explicitly starts a login, passes Turnstile/rate-limit checks and receives an unpredictable polling secret plus an eight-character human code. Both expire after five minutes. The private polling secret is stored hashed and stays on the requesting device. If the HTTP adapter supplies a separate random request cookie, polling and cancellation also require its binding. The public QR URL contains only the request ID and human code in a fragment, never the polling secret or an owner token.

The user opens the provider's own HTTPS account site on a device where they can authenticate. Scanning or visiting the QR URL only loads a review; it cannot approve or authenticate. The review identifies the selected account, displays the requesting device's unverified label and matching human code, and explains that approval grants a full owner login session. The user must be signing in the device physically in front of them. The explicit approval POST carries the matching code, current account handle and acknowledgement, with recent owner authentication and CSRF protection. Denial remains a separate action. Do not approve codes received from strangers, support messages or an unexpected sign-in prompt.

The requesting device waits at least five seconds between polls; polling too early increases the required interval by five seconds. Pending, denied, expired and slow-poll outcomes carry no account session. Once approved, an atomic request compare-and-swap consumes the handoff before a new owner session is created. Only one concurrent poll can win. The service rechecks that the account remains active and that the approving session is still live; recovery, logout or session revocation prevents handoff. File recovery resumes the same pending setup after a lost response or timeout using a hashed original-file resume authority; it does not create a device or owner session until replacement-file proof and saving confirmation complete activation. A separate account-store conflict fails closed and requires a new request rather than replaying an authentication result. This deliberately sacrifices transport retry convenience until an atomic cross-resource handoff is available.

The injected device-request repository must atomically reserve unique request IDs and human codes and implement revision compare-and-swap. Records require bounded retention and expiry cleanup; callers cannot reuse consumed or expired records. Public inspection validates the code and is rate limited. An owner session is delivered only through the requesting device's secure cookie adapter, with its secret removed from JSON. The approving browser receives no requesting-session secret.

Remote phishing remains a limitation of device approval: a malicious person can create a real request and persuade a victim to approve its code. Matching the visible code, naming the account, requiring an explicit review and showing full-session authority reduce accidental approval but cannot establish physical possession cryptographically. Device labels and a QR image do not prove device ownership. Stronger device attestation and narrower application grants require separate designs.

The flow follows the cross-device review pattern, distinct public/private codes and polling/backoff principles in [RFC 8628](https://www.rfc-editor.org/rfc/rfc8628.html). Its security discussion covers guessing, untrusted devices and remote phishing. This implementation's full owner sessions, JSON action APIs, fragment URLs, five-minute limits and repository interfaces are project policy rather than a conforming OAuth token endpoint.

| Review checkpoint | Reviewer/date/outcome |
| --- | --- |
| Passkey lifecycle and concurrent enrollment/removal evidence | Pending; real maintained-library tests implemented |
| Session-management authorization and immediate revocation | Pending; platform-independent tests implemented |
| Persistent device-request races and requesting-cookie binding | Pending; platform adapter verification required |
| Explicit account/code review and phishing warning usability | Pending; browser and human review required |

Reviewer fields record actual outcomes when available. No approval is fabricated, and implementation continues under the user's existing authorization.
