hodfa840 commited on
Commit
13477a3
·
1 Parent(s): b833020

Fix login in HF iframe: fresh-session CSRF + SameSite=None cookies

Browse files

Two issues:
1. The Space runs in a cross-site iframe (hf.space inside huggingface.co).
SameSite=Lax cookies are blocked in this context, so the browser never
sends csrftoken to LS -> 403 CSRF error.
Fix: proxy handles CSRF entirely server-side (fresh requests.Session to LS),
so the browser's csrftoken is never needed.

2. Cookies returned after login had SameSite=Lax which is also blocked in the
iframe for subsequent requests.
Fix: set SameSite=None;Secure so cookies work in both direct and iframe access.

Files changed (1) hide show
  1. ls_proxy_hf.py +56 -46
ls_proxy_hf.py CHANGED
@@ -571,61 +571,71 @@ def login_get():
571
  @app.route("/user/login/", methods=["POST"])
572
  @app.route("/user/login", methods=["POST"])
573
  def login_post():
574
- """Forward login POST directly to LS; intercept the success redirect to set long-lived cookies."""
 
 
 
 
 
 
 
575
  try:
576
- # Pass the POST straight through — browser already has the matching csrftoken cookie
577
- # set by login_get(), so LS CSRF validation will pass without any token manipulation.
578
- headers = _fwd_headers(request)
579
- csrf_val = request.form.get("csrfmiddlewaretoken", "")
580
- if csrf_val:
581
- headers["X-CSRFToken"] = csrf_val
582
- # Override Origin and Referer: browser sends the HF Space domain, but Django
583
- # compares Origin against Host (localhost:8080) and rejects cross-origin CSRF.
584
- headers["Origin"] = LS_URL
585
- headers["Referer"] = f"{LS_URL}/user/login/"
586
-
587
- r = requests.post(
 
 
 
 
588
  f"{LS_URL}/user/login/",
589
- data=request.get_data(),
590
- headers=headers,
591
- cookies=_ls_cookies(request),
592
- allow_redirects=False,
593
  timeout=15,
594
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  except requests.exceptions.ConnectionError:
596
  return _ls_unavailable()
597
  except Exception as e:
598
  return Response(f"Login error: {e}", status=502, content_type="text/plain")
599
 
600
- # Success: LS redirects away from /user/login/
601
- if r.status_code in (301, 302, 303, 307, 308):
602
- next_url = request.args.get("next") or request.form.get("next") or "/projects/"
603
- if not next_url.startswith("/") or next_url in ("/", ""):
604
- next_url = "/projects/"
605
- out = Response("", status=302, headers={"Location": next_url})
606
- # 14-day session so a browser restart doesn't drop the cookie
607
- for name, value in r.cookies.items():
608
- out.set_cookie(
609
- name, value,
610
- path="/",
611
- max_age=1209600,
612
- samesite="Lax",
613
- httponly=(name == "sessionid"),
614
- secure=True,
615
- )
616
- return out
617
-
618
- # Failure: wrong password or CSRF error — re-render login page with banner
619
- err_html = r.text if r.text else ""
620
- body_match = _re.search(r'(<body[^>]*>)', err_html, _re.IGNORECASE)
621
- if body_match:
622
- insert_pos = body_match.end()
623
- err_html = err_html[:insert_pos] + _LOGIN_BANNER + err_html[insert_pos:]
624
- else:
625
- err_html = _LOGIN_BANNER + err_html
626
- fwd = _out_headers(r, extra_exclude={"content-encoding", "content-length"})
627
- return Response(err_html, status=200, headers=fwd, content_type="text/html; charset=utf-8")
628
-
629
 
630
  @app.route("/")
631
  def root_redirect():
 
571
  @app.route("/user/login/", methods=["POST"])
572
  @app.route("/user/login", methods=["POST"])
573
  def login_post():
574
+ """Open a fresh server-side LS session to handle CSRF, then forward cookies to browser.
575
+
576
+ We never rely on the browser's csrftoken cookie because the Space runs inside an
577
+ iframe on huggingface.co — cross-site context blocks SameSite=Lax cookies.
578
+ The proxy fetches a fresh CSRF token from LS directly and posts credentials
579
+ server-to-server. Cookies are returned as SameSite=None;Secure so they work
580
+ both in direct-URL access and in the HF iframe.
581
+ """
582
  try:
583
+ sess = requests.Session()
584
+ r0 = sess.get(f"{LS_URL}/user/login/", timeout=10)
585
+ if r0.status_code != 200:
586
+ return _ls_unavailable()
587
+
588
+ csrf_cookie = sess.cookies.get("csrftoken", "")
589
+ # Support any attribute order in the hidden input
590
+ m = (_re.search(r'name="csrfmiddlewaretoken"[^>]*value="([^"]+)"', r0.text) or
591
+ _re.search(r'value="([^"]+)"[^>]*name="csrfmiddlewaretoken"', r0.text))
592
+ csrf_token = m.group(1) if m else csrf_cookie
593
+
594
+ form = request.form.to_dict(flat=True)
595
+ form["csrfmiddlewaretoken"] = csrf_token
596
+
597
+ # allow_redirects=True so we get the final sessionid after Django session rotation
598
+ r1 = sess.post(
599
  f"{LS_URL}/user/login/",
600
+ data=form,
601
+ headers={"Referer": f"{LS_URL}/user/login/",
602
+ "X-CSRFToken": csrf_token},
603
+ allow_redirects=True,
604
  timeout=15,
605
  )
606
+
607
+ if "/user/login" not in r1.url:
608
+ next_url = request.args.get("next") or request.form.get("next") or "/projects/"
609
+ if not next_url.startswith("/") or next_url in ("/", ""):
610
+ next_url = "/projects/"
611
+ out = Response("", status=302, headers={"Location": next_url})
612
+ # SameSite=None;Secure — required for cross-site iframe on huggingface.co
613
+ for name, value in sess.cookies.items():
614
+ out.set_cookie(
615
+ name, value,
616
+ path="/",
617
+ max_age=1209600,
618
+ samesite="None",
619
+ httponly=(name == "sessionid"),
620
+ secure=True,
621
+ )
622
+ return out
623
+
624
+ # Failure: wrong credentials — re-render login page with banner
625
+ err_html = r1.text if r1.text else ""
626
+ body_match = _re.search(r'(<body[^>]*>)', err_html, _re.IGNORECASE)
627
+ if body_match:
628
+ insert_pos = body_match.end()
629
+ err_html = err_html[:insert_pos] + _LOGIN_BANNER + err_html[insert_pos:]
630
+ else:
631
+ err_html = _LOGIN_BANNER + err_html
632
+ return Response(err_html, status=200, content_type="text/html; charset=utf-8")
633
+
634
  except requests.exceptions.ConnectionError:
635
  return _ls_unavailable()
636
  except Exception as e:
637
  return Response(f"Login error: {e}", status=502, content_type="text/plain")
638
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
  @app.route("/")
641
  def root_redirect():