Skip to content

Let a browser on another device sign in to a Windows host with a typed code - #2088

Open
Brian Krabach (bkrabach) wants to merge 6 commits into
mainfrom
windows-ootb/code-sign-in
Open

Brian Krabach (bkrabach) wants to merge 6 commits into
mainfrom
windows-ootb/code-sign-in

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

Change

Problem. On a native Windows host a browser signs in only through a one-use ticket that the local launcher issues and a loopback page redeems. A Windows host has no password sign-in. So a browser on another device (LAN or Tailnet address) could not sign in at all. Optional LAN or Tailnet access must be possible on a Windows host. Certificate renewal for a remote address (#2069) and sign-out of all browsers (#2080) are in place; this is the last piece.

Result. The owner starts the sign-in at the PC:

amplifier-unified browsers code

It prints a code such as ABCDE-FGHJK and the address to open. On the other device the sign-in page for that address shows one field for the code.

  • The code. Ten symbols from 32 (no 0, 1, I, O): 50 bits. It lasts 120 seconds and works once. The host keeps only its SHA-256, at most eight at a time. It is bound to the host instance, the data folder, the owner SID and one address: an exact HTTPS entry of public_origins that is not this computer. Input is upper-cased; spaces and hyphens are removed; nothing else is changed, so a look-alike is a wrong code.
  • Who can make one. The command uses the same pinned request as open: it pins the privately read server certificate before it reads or sends the owner credential. The host accepts that request from this computer only, with the same checks as for a ticket.
  • Where it works. A separate narrow check (browser_entry.remote_origin): HTTPS, an explicit Origin equal to the address the browser used, and that address an exact configured remote one. It does not read the peer address. A Tailscale TCP forward connects from 127.0.0.1 (measured on Windows 11 with Tailscale 1.104.5), so the socket does not say where the browser is. The local ticket keeps its stricter check; its code path and its two test files are unchanged in behavior (tests/test_browser_entry.py and tests/test_windows_auth.py have no edits).
  • Wrong codes. No lockout and no delay. A lockout would let any device that reaches the address keep the owner out. The numbers carry the guess resistance. A wrong code removes nothing, reads no private file, and never touches the owner's command, which uses another endpoint. The host logs only a count.
  • The page. Server-rendered, no script, nothing from the request is shown. The client-owned sign-in page and script are not changed; on Windows the host serves this page only for a non-loopback address.
  • The session. The signed cookie from a code is marked remote. Local cookies do not change. amplifier-unified browsers sign-out (Let the owner sign out all browsers at once #2080) signs out every browser and now also drops pending codes.
  • Other platforms. Linux, macOS and WSL keep the password page for every address and refuse both new endpoints.

Two independent security reviews of this design (typed code versus link, lockout, sign-out) are recorded with the program; their findings are applied.

Also in this PR: a fix to #2080. The running host kept its sign-out time in application keys and changed them after start. aiohttp deprecates that, and a later aiohttp would turn a sign-out into a silent no-op in the running host. The time now lives in a holder object (auth.Sessions). A fault in the watcher is said once in the host log.

Docs. docs/DEPLOYMENT.md has a new section, "Windows host: reach Unified from another device", with the measured Tailnet steps. The local address stays first in public_origins, because the first address is the main one for callbacks of connected services. The README points to it. The design document replaces "remote browsers need a pairing design" with the design as built.

Limits, stated plainly.

  • There is no sign-out for one device.
  • A person on the same network who gets the owner to type the code into a look-alike page within 120 seconds can use it. The command prints the exact address to open.
  • One host process holds pending codes in memory; a restart drops them.
  • The command has no Start-menu entry or in-app button. It needs a terminal. Remote access is outside the default no-terminal journey.
  • The client-owned loopback page still says that remote pairing is not available. That file is authored in the private client repository and is not changed here.

Not touched: components/web-client/**, amplifier_web/static/**, scripts/install.ps1, scripts/windows_bootstrap_native.cs.

Validation

Local, Linux:

  • New tests/test_browser_code.py: 83 passed. Real aiohttp requests through the real authentication middleware and handlers. One case serves the real handlers over real TLS with the product certificate shape and signs in at the remote name from a loopback peer: certificate valid for the remote name and refused for another name; code page without script; owner makes a code over the local address; wrong code; the same form at the local address refused; right code gives a Secure, HttpOnly, SameSite=Strict session that the middleware accepts; the code works once.
  • I broke five security checks one at a time (remote-address check, binding, one use, owner's address check, expiry). The new tests failed for each.
  • Related suites (test_browser_entry, test_windows_auth, test_session_sign_out, test_server_auth, test_cli_deployment, test_device_pairing, test_device_connections, test_windows_user_trial_probe, test_windows_bootstrap_ci, with the new file): 1142 passed, 8 skipped.
  • The real-host sign-out test now also requires that no "Changing state of started application" warning occurs.

Native Windows (this PR's windows-local-connection check): tests/test_browser_code.py and tests/test_session_sign_out.py run there, including the real-TLS case.

Not proven:

  • A real second device, a phone browser, and the documented steps from start to end on a PC that was set up with the Windows setup file. The private store in the new tests is a model on every platform; the native store is covered by the existing native lanes, not by this flow.
  • Whether a phone browser sends Origin on the form post. The local flow already depends on that on desktop browsers.

…d code

On a native Windows host a browser signs in only through a one-use ticket
that the local launcher issues and a loopback page redeems. A browser on
another device (a LAN or Tailnet address) could not sign in at all: Windows
has no password sign-in, and the page told the person to use the launcher.

`amplifier-unified browsers code` now prints a code at the PC. The owner
types it on the sign-in page that the other device shows. The code has ten
symbols from 32 (50 bits), lasts 120 seconds and works once. The host keeps
only its SHA-256, and at most eight codes. The command uses the same pinned
request as `open`: it pins the privately read leaf certificate before it
reads or sends the control credential, and the host accepts that request
from this computer only. The code is bound to the host instance, the data
folder, the owner and one configured HTTPS address that is not this PC.

The redeem check is separate and narrow: HTTPS, an explicit Origin equal to
the address the browser used, and that address an exact configured remote
one. It does not look at the peer address, because a Tailscale TCP forward
connects from 127.0.0.1. There is no lockout and no delay: a wrong code is
counted and never removes a real one, so nobody can keep the owner out by
guessing. A session from a code is marked remote in its signed cookie.
"Sign out all browsers" also drops pending codes.

The local ticket sign-in, and password sign-in on Linux, macOS and WSL, do
not change. `issue`, `redeem` and `owner_browser_url` now share their
checks with the code paths through private helpers; their tests pass
unedited. The remote page is sent with `Referrer-Policy: same-origin`, not
`no-referrer`, because a browser posts a form from a `no-referrer` page
with `Origin: null`.

Not measured: a phone browser, a real second device, and native Windows.
The new tests run on Linux and are added to the focused Windows job.

Co-authored-by: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
… sign-out state in a holder

- One test serves the real handlers over real TLS with the product
  certificate shape and signs in at the remote name from a loopback peer.
- The documented address order keeps the local address first: the first
  configured address is the main one for callbacks of connected services.
- The running host kept its sign-out time in application keys and changed
  them after start, which aiohttp deprecates. It now lives in a holder
  object. A watcher fault is said once in the host log.
…n-in

# Conflicts:
#	.github/workflows/platform-mechanisms.yml
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.

2 participants