Spaces:
Running
Running
hodfa840 commited on
Commit ·
13477a3
1
Parent(s): b833020
Fix login in HF iframe: fresh-session CSRF + SameSite=None cookies
Browse filesTwo 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.
- 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 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
try:
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
#
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
f"{LS_URL}/user/login/",
|
| 589 |
-
data=
|
| 590 |
-
headers=
|
| 591 |
-
|
| 592 |
-
allow_redirects=
|
| 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():
|