Web Mouse: your phone as a trackpad, and the auth model it needed
Where this came from
I saw an Apple trackpad and wanted one. Then I looked at the price and wanted to know, first, whether I’d actually use it.
That question is harder than it sounds, because what you’re buying isn’t really the hardware — it’s the gestures. Two-finger scroll, the inertia when you flick, the way a large surface changes how much you move your hand. You can’t evaluate any of that in a shop in thirty seconds, and you certainly can’t evaluate it against your own daily work.
Then the obvious thing occurred to me: a trackpad is a slab of multi-touch glass that reports finger positions over a wire. I already owned a slab of multi-touch glass that reports finger positions over WiFi. It was in my pocket.
So instead of buying one to find out, I spent a weekend building an approximation and used that. Cheaper, reversible, and I’d learn something either way — which is roughly my justification for most side projects, though usually less literally.
Where it got interesting
Getting a cursor to move is the easy part — Flask takes the touch deltas, pynput applies them. I had that working quickly. Then I stopped, because I’d built something that accepts input from the network and converts it directly into control of my machine, and I hadn’t thought about who was allowed to send it.
A tool that moves your cursor is one step from a tool that clicks things. On a shared or public network, an unauthenticated endpoint at 0.0.0.0:5000 that takes movement commands is not a convenience feature — it’s a way for anyone on the network to drive your laptop while you’re looking at it.
So the design question stopped being “how do I send deltas” and became how does the laptop know this phone, and only this phone, is allowed to control it?
The session model
The host page generates a session and shows it as a QR code:
def generate_session(host_url):
global current_session
token = uuid.uuid4().hex
control_url = f"{host_url.rstrip('/')}/control?token={token}"
current_session = {
'token': token,
'authorized': None,
'created_at': datetime.datetime.now(datetime.UTC).isoformat()
}
return current_session
The flow that follows has four properties I wanted:
- Possession, not passwords. The token only exists on the laptop screen. Scanning the QR proves you’re in the room and looking at it — which for a device sitting in front of you is a better proof than any credential I’d have made the user type on a phone keyboard.
- Claim, then control. The controller hits
POST /claimwith the token and aclient_id. Presenting the token is not enough on its own; the claim is what binds the session to one device. - One controller at a time. The first valid claim sets
authorized. Every later device with the same token gets a409 Conflict—session occupied. Two phones fighting over one cursor is a bug I’d rather make impossible than debug. - Revocation is one click.
POST /new_sessionmints a fresh token and orphans the old one. Handing control to someone else, or taking it back, is the same action.
if current_session['authorized'] is None or current_session['authorized'] == client_id:
current_session['authorized'] = client_id
return jsonify(success=True, token=token)
return jsonify(success=False, error='session occupied'), 409
The re-claim branch matters more than it looks. authorized == client_id lets the same phone reconnect after its screen sleeps or the WiFi blips, without the user rescanning. Without it, the correct security behaviour and the correct usability behaviour are in direct conflict.
What it does not defend against
Being explicit about this is the point, not a disclaimer:
- No HTTPS. Traffic is plaintext on the LAN, token included. Anyone already sniffing your local network can lift it.
- No session expiry. A token stays valid until a new session replaces it. Leave it running for a week and it’s still live.
- No CSRF protection. Another page in the controller’s browser could, in principle, post to the endpoints.
Every one of these is fine under the assumption the tool is built on — a trusted home network, a session you start deliberately and end when you’re done — and none of them would be acceptable if this were exposed beyond the LAN. Writing the assumption down is what makes it a design decision rather than an oversight.