feat(web): house-password login on the LAN - #913
Conversation
Session cookie after POST /api/auth/login. Settings turns api.lan_auth on and off. Live status stays visible until a write needs the password.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 22842df2c5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return readError(res).then(function (err) { | ||
| if (err !== LAN_ERROR) return res; | ||
| return openLoginModal().then(function (ok) { | ||
| if (!ok) return res; |
There was a problem hiding this comment.
Reject canceled login requests instead of returning the 401
When a user opens Settings on a locked LAN and selects Cancel, this resolves the original fetch with its 401 response. The caller in web/settings.js parses every response as JSON without checking r.ok, so it treats {"error":"valid LAN password required"} as the configuration and opens a malformed Settings modal. Reject the request on cancellation, or ensure callers check the status before consuming the response.
Useful? React with 👍 / 👎.
| if token, ok := lanSessionCookieValue(r); ok && lanSessionValid(token) { | ||
| return true, r.WithContext(withLANSecret(r.Context(), true)) |
There was a problem hiding this comment.
Invalidate sessions on every LAN-auth transition
If an operator disables and later re-enables api.lan_auth through the supported, hot-applied POST /api/config path, an existing cookie is still accepted here for the remainder of its 12-hour lifetime. Sessions are cleared only by handleAuthPassword, while handlePostConfig can change the same flag without calling dropAllLANSessions, so an old authenticated browser silently regains owner access after the lock is restored. Tie sessions to an auth generation or invalidate them whenever the config transition changes LAN auth.
Useful? React with 👍 / 👎.
miravoss26
left a comment
There was a problem hiding this comment.
Follow-up to #912: browser login form + session cookie for the house password. Read through both the Go session handling and web/lan-auth.js.
Correctness:
- Session token: 32 bytes from
crypto/rand, hex-encoded (256 bits) — no practical brute-force or timing concern from the plain map lookup. - Cookie:
HttpOnly,SameSite=Strict, noSecure(correct for plain-HTTP LAN, explicitly tested), 12hMaxAge.SameSite=Strictis also what closes the CSRF angle on/api/auth/password— a hostile page can't ride the cookie cross-site, even via top-level navigation. - Password change and disable both call
dropAllLANSessions()— old sessions can't outlive a password rotation. Logout only drops the current token, which is right for a multi-device household. - Login reuses the same global guess-limiter (
admitLANSecret) as Bearer auth — one counter for the one secret, as documented. resolveLANSecret: Bearer takes priority over the cookie when both are present, and a malformed/wrong Bearer still denies rather than silently falling back to a valid cookie — fails closed, matches the "Bearer wins" test.web/lan-auth.jsnever toucheslocalStorage/sessionStorage(asserted by a dedicated JS test,lan-auth.test.mjs), and concurrent 401s share one pending-login promise rather than stacking modals.
One thing I can't rule out from the diff alone: the fetch wrapper retries the original input/init on successful login (nativeFetch(input, init)). If any caller elsewhere in the app passes a Request object with a streamed body (rather than a string URL + body: JSON.stringify(...) in init, which is what system.js does here), that retry could resend an already-consumed stream. I didn't find such a call site in this diff, but I also didn't audit every existing fetch caller in the app — worth a quick grep for new Request( before relying on the retry path universally.
CI green, no secrets, no new dependencies.
Safe to merge from my read, once #912 → #911 → #909 land underneath it.
|
Stänger som ersatt av #915. |
Summary
Follow-up to #912. When
api.lan_authis on, the box UI can log in withoutcurl.POST /api/auth/loginchecks the house password (same guess limiter as Bearer) and sets an HttpOnlyftw_lancookie (12 h, SameSite=Strict, not Secure — LAN is HTTP)POST /api/auth/logoutdrops the session/api/auth/password, not a raw config checkbox401 valid LAN password requiredand retries the requestlocalStorageLive status, energy and prices stay visible without the password. Writes and Settings need it.
Stack
Sits on #912 (
agent/lan-auth-seam) → #911 → #909. Merge those first.Test plan
go test ./go/internal/api/node --test web/lan-auth.test.mjs web/javascript-syntax.test.mjscurlwith Bearer still workscurl http://127.0.0.1:8080/api/auth/passwordorapi.lan_auth: false+ restartA human should look at Settings → System and the login modal (CODEOWNERS).
Out of scope