fix(web): exchange the launch token for a short-lived session (F-04) - #51
Merged
Merged
Conversation
The launch token is persistent, lives in ~/.config/replicant/web-token, and was ALSO the value written into the session cookie. So the cookie was the master credential: no expiry, no rotation, and no way to revoke one browser without regenerating the token file and breaking every script and every other client at the same time. Anything that read that cookie held permanent access. The same token was additionally appended to EventSource and WebSocket URLs, which is the least private part of a request: URLs reach server logs, browser history and the Referer header. Both halves are gone. **SessionStore** issues short-lived random ids (12h), validates, expires, revokes one or all, and sweeps on issue so a reconnect loop cannot grow it without bound. The cookie now holds one of those. It is per app rather than module-global, so two apps in one process cannot authenticate each other's browsers. `Secure` is set only over https, because setting it on the loopback http this tool serves by default would stop the cookie being sent at all. **POST /api/session/logout** revokes this browser and nothing else. Deliberately unauthenticated: the worst anyone can do is end a session they already hold. **The query parameter is gone from both stream URLs.** EventSource and WebSocket cannot set headers, which is why the token was there, but both are same-origin and the cookie is already set by the page load that precedes them. The launch token still works as a header for non-browser clients, because a script cannot run a cookie jar. Five tests in test_web_access.py wrote the launch token straight into the cookie, which is exactly what this removes. They now establish a real session through the server, so they still test cookie auth and the Origin rules, but against a credential the server minted. Verified in a browser, not only in tests: address bar carries no token, `document.cookie` is empty (httpOnly), and the HTTP API, the terminal WebSocket and the run SSE stream all authenticate on the cookie alone, with CEF lines arriving over SSE. 853 py + 136 fe. black, ruff, mypy clean. All 14 new guards observed to fail first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The launch token is persistent, lives in ~/.config/replicant/web-token, and was
ALSO the value written into the session cookie. So the cookie was the master
credential: no expiry, no rotation, and no way to revoke one browser without
regenerating the token file and breaking every script and every other client at
the same time. Anything that read that cookie held permanent access.
The same token was additionally appended to EventSource and WebSocket URLs,
which is the least private part of a request: URLs reach server logs, browser
history and the Referer header.
Both halves are gone.
SessionStore issues short-lived random ids (12h), validates, expires,
revokes one or all, and sweeps on issue so a reconnect loop cannot grow it
without bound. The cookie now holds one of those. It is per app rather than
module-global, so two apps in one process cannot authenticate each other's
browsers.
Secureis set only over https, because setting it on the loopbackhttp this tool serves by default would stop the cookie being sent at all.
POST /api/session/logout revokes this browser and nothing else. Deliberately
unauthenticated: the worst anyone can do is end a session they already hold.
The query parameter is gone from both stream URLs. EventSource and WebSocket
cannot set headers, which is why the token was there, but both are same-origin
and the cookie is already set by the page load that precedes them.
The launch token still works as a header for non-browser clients, because a
script cannot run a cookie jar.
Five tests in test_web_access.py wrote the launch token straight into the cookie,
which is exactly what this removes. They now establish a real session through the
server, so they still test cookie auth and the Origin rules, but against a
credential the server minted.
Verified in a browser, not only in tests: address bar carries no token,
document.cookieis empty (httpOnly), and the HTTP API, the terminal WebSocketand the run SSE stream all authenticate on the cookie alone, with CEF lines
arriving over SSE.
853 py + 136 fe. black, ruff, mypy clean. All 14 new guards observed to fail first.