fix(auth): honor ADMIN_EMAILS with robust email merge and JWT bootstrap for admin checks

Made-with: Cursor
This commit is contained in:
dosangyoon
2026-03-23 10:27:59 +09:00
parent aff4b7d961
commit ea104aef6e
2 changed files with 48 additions and 9 deletions

View File

@@ -43,6 +43,17 @@ def parse_email_csv(s: str) -> list[str]:
return [x.lower() for x in parse_csv(s)]
def email_from_id_token_payload(payload: dict) -> Optional[str]:
"""Prefer standard `email`; also accept Auth0-style namespaced claims ending with `/email`."""
v = payload.get("email")
if isinstance(v, str) and v.strip():
return v.strip().lower()
for k, val in payload.items():
if isinstance(k, str) and k.endswith("/email") and isinstance(val, str) and val.strip():
return val.strip().lower()
return None
PORT = int(env("PORT", "8023") or "8023")
DB_HOST = env("DB_HOST", "").strip()
@@ -193,7 +204,8 @@ def verify_user_from_request() -> Tuple[Optional[str], str]:
payload = verify_id_token(id_token, issuer=issuer, audience=audience)
sub = str(payload.get("sub") or "").strip()
email = (str(payload.get("email")).strip().lower() if payload.get("email") else "")
e = email_from_id_token_payload(payload)
email = e or ""
return (sub or None, email)
@@ -205,6 +217,8 @@ def is_request_admin() -> Tuple[bool, str]:
return (False, "")
ensure_user_table()
sub, email = verify_user_from_request()
if email and is_bootstrap_admin(email):
return (True, email)
if not sub:
return (False, email)
row = db_one(f"select is_admin from public.{TABLE} where sub = %s", (sub,))
@@ -404,7 +418,7 @@ def api_auth_sync() -> Response:
payload = verify_id_token(id_token, issuer=issuer, audience=audience)
sub = str(payload.get("sub") or "").strip()
email = (str(payload.get("email")).strip().lower() if payload.get("email") else None)
jwt_email = email_from_id_token_payload(payload)
name = (str(payload.get("name")).strip() if payload.get("name") else None)
picture = (str(payload.get("picture")).strip() if payload.get("picture") else None)
provider = sub.split("|", 1)[0] if "|" in sub else None
@@ -412,7 +426,13 @@ def api_auth_sync() -> Response:
if not sub:
return jsonify({"ok": False, "error": "missing_sub"}), 400
bootstrap_admin = bool(email and is_bootstrap_admin(email))
prev_row = db_one(f"select email from public.{TABLE} where sub = %s", (sub,))
prev_email: Optional[str] = None
if prev_row and prev_row[0]:
pe = str(prev_row[0]).strip().lower()
prev_email = pe or None
effective_email = jwt_email or prev_email
bootstrap_admin = bool(effective_email and is_bootstrap_admin(effective_email))
sql = f"""
insert into public.{TABLE}
@@ -420,7 +440,7 @@ def api_auth_sync() -> Response:
values
(%s, %s, %s, %s, %s, now(), now(), %s, %s, now())
on conflict (sub) do update set
email = excluded.email,
email = coalesce(nullif(btrim(excluded.email::text), ''), public.{TABLE}.email),
name = excluded.name,
picture = excluded.picture,
provider = excluded.provider,
@@ -432,7 +452,7 @@ def api_auth_sync() -> Response:
returning is_admin, can_manage, first_login_at, last_login_at, last_logout_at
"""
row = db_one(sql, (sub, email, name, picture, provider, bootstrap_admin, bootstrap_admin, bootstrap_admin, bootstrap_admin))
row = db_one(sql, (sub, jwt_email, name, picture, provider, bootstrap_admin, bootstrap_admin, bootstrap_admin, bootstrap_admin))
is_admin = bool(row[0]) if row else False
can_manage = bool(row[1]) if row else False
user = (