Commit Graph

11 Commits

Author SHA1 Message Date
Brian Krabach cb264bbaaa feat: postMessage auth token relay for cross-origin federation login
- Backend: AuthMiddleware.dispatch() checks X-Muxplex-Token header
  after cookie auth, allowing cross-origin federation requests to
  authenticate using a session token sent as a header instead of a
  SameSite=Strict cookie.

- Backend: GET /api/auth/token endpoint returns the current session
  token for authenticated requests. Used by login popups to relay
  the token back to the opener window via postMessage.

- Frontend (app.js): api() injects X-Muxplex-Token header for
  cross-origin requests when a token is stored in localStorage under
  muxplex.federation_tokens keyed by origin.

- Frontend (app.js): storeFederationToken() helper stores tokens.
  window.addEventListener('message') handler catches muxplex-auth-token
  postMessages and stores the token, then retriggers a poll so the
  auth_required source transitions to authenticated immediately.

- Frontend (app.js): _saveRemoteInstances() prunes stale tokens for
  URLs no longer in the remote instances list.

- Frontend (index.html): Popup relay script fetches /api/auth/token
  and sends the token to window.opener via postMessage when loaded
  as a login popup (window.opener present).

Tests added:
  test_auth.py: X-Muxplex-Token header authenticates / invalid rejected
  test_api.py: GET /api/auth/token returns token / 401 when unauthed
  test_app.mjs: api() header injection, storeFederationToken storage,
                index.html popup script presence
2026-04-01 08:17:28 -07:00
Brian Krabach e61efd141c feat: exempt /api/instance-info from auth 2026-03-30 20:54:21 -07:00
Brian Krabach 575cebbcac fix: resolve_auth file password log prints actual path, not module name
Add `get_password_path` to top-level imports in main.py and fix the file
password resolution branch to log the actual file path instead of
`load_password.__module__` (which printed "muxplex.auth").

Also adds 4 tests covering all _resolve_auth logging paths:
- test_resolve_auth_pam_mode_logs_pam
- test_resolve_auth_env_password_logs_env
- test_resolve_auth_file_password_logs_file
- test_resolve_auth_generates_password_as_last_resort
2026-03-28 23:06:27 -07:00
Brian Krabach 4e590345c3 fix(auth): remove Host-header auth bypass; inject client IP in tests via ASGI middleware
The server_host check (request.url.hostname) read from the user-controlled
HTTP Host header, allowing any attacker to bypass authentication by sending
'Host: 127.0.0.1'. Remove it entirely — request.client.host is the
socket-level IP and cannot be forged.

Fix the localhost test by introducing _InjectClientMiddleware, a thin ASGI
wrapper that writes a real IP into the scope's 'client' field. This lets the
test exercise the actual localhost check with 127.0.0.1 rather than relying
on the URL hostname.

Also:
- Promote _LOCALHOST_ADDRS to module-level constant (no-op per request)
- Add clarifying comment on auth_header[6:] magic slice
- Replace per-request cookies= kwarg with client.cookies.set() (Starlette deprecation)
2026-03-28 21:40:22 -07:00
Brian Krabach 22be933ebf feat(auth): request middleware with localhost bypass and session cookie check
- Add AuthMiddleware class extending BaseHTTPMiddleware
- Localhost bypass: checks both client.host and url.hostname for 127.0.0.1/::1/localhost
- Exempt paths: /login, /auth/mode, /auth/logout bypass auth
- Valid session cookie (muxplex_session) passes through
- Basic auth header: valid password passes, invalid returns 401
- API clients (Accept: application/json) get 401 on auth failure
- Browser clients get 307 redirect to /login on auth failure
- _check_credentials dispatches to PAM or password comparison
- Add 8 middleware tests to test_auth.py

Co-authored-by: Amplifier <amplifier@example.com>
2026-03-28 21:31:48 -07:00
Brian Krabach 7cf2a5882a style(auth): apply code quality suggestions from review
- auth.py: replace 'import os as _os' with plain 'import os' inside
  authenticate_pam() — the alias convention is for module-level private
  names, not function-scoped imports
- test_auth.py: move 'import pwd' from two test functions to module-level
  imports, consistent with existing 'import os' placement
- test_auth.py: make wrong-user test robust against running as root —
  derives 'wrong_user' dynamically so the test holds in Docker CI
  environments where the runner may be root
2026-03-28 21:25:16 -07:00
Brian Krabach 49475ba352 feat(auth): PAM authentication with running-user check 2026-03-28 21:19:53 -07:00
Brian Krabach 22816cd1d5 feat(auth): session cookie signing and verification 2026-03-28 21:11:14 -07:00
Brian Krabach 91e57431e4 feat(auth): signing secret file management 2026-03-28 21:00:56 -07:00
Brian Krabach 750345a763 test(auth): add coverage for config dir 0700 permissions 2026-03-28 20:57:15 -07:00
Brian Krabach 3b248e2a63 feat(auth): password file management
Add muxplex/auth.py with password file management functions:
- get_password_path(): returns ~/.config/muxplex/password
- load_password(): reads password file, returns None if not found
- generate_and_save_password(): generates random password, writes with 0600 permissions

Add muxplex/tests/test_auth.py with 5 tests covering all functions.

Co-authored-by: Amplifier <amplifier@invalid>
2026-03-28 20:49:04 -07:00