Add opt-in EMBED_KEY passwordless sign-in and iframe-friendly session cookie
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
429a72f2c3
commit
77bd8389a2
2 files changed
+29
-2
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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", ""))
|
||||
|
||||
Reference in new issue
Block a user