From 77bd8389a2a7dbfb748a1ff681d806065425ac7c Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:19:04 -0400 Subject: [PATCH] Add opt-in EMBED_KEY passwordless sign-in and iframe-friendly session cookie Co-Authored-By: Claude Sonnet 5.5 --- docker-compose.yml | 2 ++ server.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e8cd35f..5468fef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: OPENAUDIBLE_PASSWORD: ${OPENAUDIBLE_PASSWORD} PLAYER_USER: ${PLAYER_USER} # default account, created on first start PLAYER_PASSWORD: ${PLAYER_PASSWORD} + EMBED_KEY: ${EMBED_KEY:-} # optional: enables /auto?key=... passwordless sign-in (serve over HTTPS) + EMBED_USER: ${EMBED_USER:-} # which account it signs in as (default: first account) volumes: - player-data:/data # accounts, sessions, listening progress diff --git a/server.py b/server.py index 6c74cb2..0ce5feb 100644 --- a/server.py +++ b/server.py @@ -39,6 +39,9 @@ F_SESSIONS = os.path.join(DATA, "sessions.json") # token hash -> {user, exp} F_PROGRESS = os.path.join(DATA, "progress_users.json") F_LEGACY = os.path.join(DATA, "progress.json") # single-user file from the first version COOKIE = "oa_session" +# Optional passwordless entry for embedding in another app. Unset = disabled. +EMBED_KEY = os.environ.get("EMBED_KEY", "") +EMBED_USER = os.environ.get("EMBED_USER", "").strip().lower() # defaults to the first account SESSION_DAYS = 90 # Files the browser fetches without a session (home-screen icon and manifest). @@ -281,6 +284,8 @@ class Handler(BaseHTTPRequestHandler): # ----- routes ----- def do_GET(self): path = self.path.split("?")[0] + if path == "/auto": + return self.auto_login(self.path.partition("?")[2]) if path == "/login": return self.send_body(200, LOGIN, "text/html; charset=utf-8") if path == "/logout": @@ -418,13 +423,33 @@ class Handler(BaseHTTPRequestHandler): if not ok: self.fails[ip] = recent + [now] return self.json(401, {"error": "wrong"}) + self.start_session(name, lambda cookie: self.json(200, {"user": name}, [("Set-Cookie", cookie)])) + + def start_session(self, name, respond): token = secrets.token_urlsafe(32) + now = time.time() with lock: sessions = {k: v for k, v in read(F_SESSIONS, {}).items() if v["exp"] > now} sessions[sha(token)] = {"user": name, "exp": now + SESSION_DAYS * 86400} write(F_SESSIONS, sessions, private=True) - cookie = f"{COOKIE}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_DAYS * 86400}" - self.json(200, {"user": name}, [("Set-Cookie", cookie)]) + # EMBED mode: cookie must survive inside a cross-site iframe (needs HTTPS) + same = "SameSite=None; Secure" if EMBED_KEY else "SameSite=Lax" + respond(f"{COOKIE}={token}; Path=/; HttpOnly; {same}; Max-Age={SESSION_DAYS * 86400}") + + def auto_login(self, query): + """GET /auto?key=SECRET -> signed in as EMBED_USER, no password. Off unless EMBED_KEY is set.""" + key = (urllib.parse.parse_qs(query).get("key") or [""])[0] + users = read(F_USERS, {}) + name = EMBED_USER or next(iter(users), "") + if not EMBED_KEY or name not in users or not secrets.compare_digest(key.encode(), EMBED_KEY.encode()): + time.sleep(1) + return self.send_error(404) + def go(cookie): + self.send_response(302) + self.send_header("Set-Cookie", cookie) + self.send_header("Location", "/") + self.end_headers() + self.start_session(name, go) def logout(self): c = SimpleCookie(self.headers.get("Cookie", ""))