Skip to content

Device keys, proofs and the browser sign-in - #20

Merged
AlxcBTW merged 1 commit into
mainfrom
feat/passkeys
Oct 24, 2026
Merged

AlxcBTW merged 1 commit into
mainfrom
feat/passkeys

Conversation

@AlxcBTW

@AlxcBTW AlxcBTW commented Oct 24, 2026

Copy link
Copy Markdown
Member

Accounts are managed on the web, and refresh tokens are bound to a key on the device, so a copied token is useless elsewhere. The cloud half is anyplay-gg/cloud (see the link in the comments); that PR builds against this one, so merge this first.

What changes

  • ADR 0016 records the decision: the launcher only starts a browser flow (browser_sign_in, account_page); e-mail, passkeys, recovery and later Apple, Google and Discord sign-in live on the cloud's web pages. It also holds the design of the token format, the device key, proofs, reuse detection, "sign out everywhere" and passkeys.
  • crates/proof (new): DeviceKey (Ed25519), proofs (AnyPlay-Proof header, 60 seconds, once), PKCE S256 and the RFC 8252 loopback check. Test vectors, one cross-checked with Node's crypto.
  • crates/api: Device.key, RefreshRequest.device_key, BrowserTokenRequest, AccountLink, Info.web, paths auth/browser/token and auth/browser/link.
  • crates/net: makes a device key at each sign-in and keeps it in the same entry as the refresh token; every refresh carries a proof; a saved sign-in from before keys makes a key and binds on its first refresh. The browser flow: loopback listener (random port, Host check, state check), PKCE, token exchange. account_page asks for a one-time link with a proof.
  • crates/control: browser_sign_in {create}, cancel_sign_in, account_page, Pending::Browser. email, code and sign_up stay and are marked deprecated.
  • Spec: cloud-api.md (tokens v2 with test vectors, proofs, devices without a key, browser sign-in), control-protocol.md, CONTEXT.md.

Tokens

Access and relay tokens are v2: kid, aud and iat in the payload, signature over a domain separator. v1 tokens are still read. This is in the cloud PR (crates/ticket); the spec here has the vectors.

Migration for existing devices

A device signed in before keys has 30 days to refresh as before. The net service of this version binds it on its first refresh; after the 30 days it must sign in again (spec: "Devices without a key").

Checked

cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace, cargo deny check, rules.mjs. The cloud PR runs the whole chain (net service, browser, cloud, token exchange, refresh with a proof) in crates/e2e/tests/browser_sign_in.rs.

Not done here

The launcher buttons (someone else). A real browser and a real authenticator were not used; the cloud tests use a cookie-jar client and webauthn-authenticator-rs's software passkey.

Refresh tokens are bound to a key the device makes at sign-in, and refreshing needs a
proof signed by it. Sign-in, account creation and account management move to the cloud's
web pages; the launcher starts the browser flow through the net service (browser_sign_in,
cancel_sign_in, account_page). New crate anyplay-proof: device keys, proofs, PKCE and the
loopback redirect check. API: Device.key, device_key at refresh, browser/token, browser/link,
Info.web. Specification, glossary and ADR 0016.
@AlxcBTW

AlxcBTW commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

The cloud half is anyplaynet/cloud#25. It builds against this PR, so it stays red until this one is merged.

@AlxcBTW
AlxcBTW merged commit 444ff51 into main Oct 24, 2026
7 checks passed
@AlxcBTW
AlxcBTW deleted the feat/passkeys branch October 24, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant