Skip to content

feat(web): house-password login on the LAN - #913

Closed
frahlg wants to merge 1 commit into
agent/lan-auth-seamfrom
agent/lan-auth-ui
Closed

feat(web): house-password login on the LAN#913
frahlg wants to merge 1 commit into
agent/lan-auth-seamfrom
agent/lan-auth-ui

Conversation

@frahlg

@frahlg frahlg commented Aug 14, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to #912. When api.lan_auth is on, the box UI can log in without curl.

  • POST /api/auth/login checks the house password (same guess limiter as Bearer) and sets an HttpOnly ftw_lan cookie (12 h, SameSite=Strict, not Secure — LAN is HTTP)
  • POST /api/auth/logout drops the session
  • A valid cookie is owner, same as Bearer. Bearer still wins if both are sent
  • Sessions live in process memory. A restart logs everyone out
  • Settings → System turns the lock on and off through /api/auth/password, not a raw config checkbox
  • A small fetch wrapper opens a password modal on 401 valid LAN password required and retries the request
  • The password is never written to localStorage

Live 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.mjs
  • Default: no modal, Settings works
  • Enable from Settings → System with a ≥10 character password
  • From another browser / after logout: opening Settings shows the password form
  • After login, Save and support dump work
  • curl with Bearer still works
  • Recovery: curl http://127.0.0.1:8080/api/auth/password or api.lan_auth: false + restart

A human should look at Settings → System and the login modal (CODEOWNERS).

Out of scope

  • User accounts
  • WebAuthn
  • Locking live dashboard reads

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread web/lan-auth.js
return readError(res).then(function (err) {
if (err !== LAN_ERROR) return res;
return openLoginModal().then(function (ok) {
if (!ok) return res;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +156 to +157
if token, ok := lanSessionCookieValue(r); ok && lanSessionValid(token) {
return true, r.WithContext(withLANSecret(r.Context(), true))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 miravoss26 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, no Secure (correct for plain-HTTP LAN, explicitly tested), 12h MaxAge. SameSite=Strict is 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.js never touches localStorage/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.

@frahlg

frahlg commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

Ersatt av #915. Restackad ovanpå #914 (den rebasade sömmen).

@frahlg

frahlg commented Aug 16, 2026

Copy link
Copy Markdown
Member Author

Stänger som ersatt av #915.

@frahlg frahlg closed this Aug 16, 2026
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