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}
|
OPENAUDIBLE_PASSWORD: ${OPENAUDIBLE_PASSWORD}
|
||||||
PLAYER_USER: ${PLAYER_USER} # default account, created on first start
|
PLAYER_USER: ${PLAYER_USER} # default account, created on first start
|
||||||
PLAYER_PASSWORD: ${PLAYER_PASSWORD}
|
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:
|
volumes:
|
||||||
- player-data:/data # accounts, sessions, listening progress
|
- 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_PROGRESS = os.path.join(DATA, "progress_users.json")
|
||||||
F_LEGACY = os.path.join(DATA, "progress.json") # single-user file from the first version
|
F_LEGACY = os.path.join(DATA, "progress.json") # single-user file from the first version
|
||||||
COOKIE = "oa_session"
|
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
|
SESSION_DAYS = 90
|
||||||
|
|
||||||
# Files the browser fetches without a session (home-screen icon and manifest).
|
# Files the browser fetches without a session (home-screen icon and manifest).
|
||||||
@@ -281,6 +284,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
# ----- routes -----
|
# ----- routes -----
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
path = self.path.split("?")[0]
|
path = self.path.split("?")[0]
|
||||||
|
if path == "/auto":
|
||||||
|
return self.auto_login(self.path.partition("?")[2])
|
||||||
if path == "/login":
|
if path == "/login":
|
||||||
return self.send_body(200, LOGIN, "text/html; charset=utf-8")
|
return self.send_body(200, LOGIN, "text/html; charset=utf-8")
|
||||||
if path == "/logout":
|
if path == "/logout":
|
||||||
@@ -418,13 +423,33 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
if not ok:
|
if not ok:
|
||||||
self.fails[ip] = recent + [now]
|
self.fails[ip] = recent + [now]
|
||||||
return self.json(401, {"error": "wrong"})
|
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)
|
token = secrets.token_urlsafe(32)
|
||||||
|
now = time.time()
|
||||||
with lock:
|
with lock:
|
||||||
sessions = {k: v for k, v in read(F_SESSIONS, {}).items() if v["exp"] > now}
|
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}
|
sessions[sha(token)] = {"user": name, "exp": now + SESSION_DAYS * 86400}
|
||||||
write(F_SESSIONS, sessions, private=True)
|
write(F_SESSIONS, sessions, private=True)
|
||||||
cookie = f"{COOKIE}={token}; Path=/; HttpOnly; SameSite=Lax; Max-Age={SESSION_DAYS * 86400}"
|
# EMBED mode: cookie must survive inside a cross-site iframe (needs HTTPS)
|
||||||
self.json(200, {"user": name}, [("Set-Cookie", cookie)])
|
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):
|
def logout(self):
|
||||||
c = SimpleCookie(self.headers.get("Cookie", ""))
|
c = SimpleCookie(self.headers.get("Cookie", ""))
|
||||||
|
|||||||
Reference in new issue
Block a user