login: honour next on both exits, same-origin only
The console's 401 redirect carries next=/leaders/ and /login dropped it, so a leader arriving cold landed on /account. next now rides the form as a hidden field and is applied after a successful POST and when an already-signed-in person hits /login. identity.safe_next() admits only a relative path with a single leading slash - no scheme, no //, no backslash, no control characters - so the login page cannot become an open redirect. Ten checks added to tests/smoke_identity.py; the suite is 59 + 24 + 53, all green.
This commit is contained in:
+12
-6
@@ -82,10 +82,11 @@ def _shell(heading, intro, inner, error=None):
|
||||
# Login
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _login_form(email="", error=None):
|
||||
def _login_form(email="", error=None, next_url=""):
|
||||
return _shell(
|
||||
"Sign in", "For Pack 73 and Troop 73 leaders and families.",
|
||||
f"""<form method="post" action="/login">
|
||||
<input type="hidden" name="next" value="{_esc(next_url)}">
|
||||
<label for="email">Email</label>
|
||||
<input id="email" name="email" type="email" autocomplete="username" required value="{_esc(email)}">
|
||||
<label for="password">Password</label>
|
||||
@@ -98,21 +99,26 @@ def _login_form(email="", error=None):
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
def login_form(request: Request):
|
||||
# `next` arrives from the leader console's 401 redirect (next=/leaders/).
|
||||
# identity.safe_next() keeps it same-origin; anything odd lands on /account.
|
||||
nxt = identity.safe_next(request.query_params.get("next"))
|
||||
if current_person(request):
|
||||
return RedirectResponse(url="/account", status_code=303)
|
||||
return HTMLResponse(_render("Sign in", _login_form()))
|
||||
return RedirectResponse(url=nxt, status_code=303)
|
||||
return HTMLResponse(_render("Sign in", _login_form(next_url=nxt)))
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
def login(request: Request, email: str = Form(""), password: str = Form("")):
|
||||
def login(request: Request, email: str = Form(""), password: str = Form(""),
|
||||
next: str = Form("")):
|
||||
nxt = identity.safe_next(next)
|
||||
try:
|
||||
person, token = identity.authenticate(
|
||||
email, password, ip=_client_ip(request),
|
||||
user_agent=request.headers.get("user-agent"))
|
||||
except identity.IdentityError as e:
|
||||
return HTMLResponse(_render("Sign in", _login_form(email, e.detail)),
|
||||
return HTMLResponse(_render("Sign in", _login_form(email, e.detail, nxt)),
|
||||
status_code=e.status)
|
||||
resp = RedirectResponse(url="/account", status_code=303)
|
||||
resp = RedirectResponse(url=nxt, status_code=303)
|
||||
resp.set_cookie(COOKIE, token, max_age=identity.SESSION_ABSOLUTE_DAYS * 86400,
|
||||
httponly=True, secure=COOKIE_SECURE, samesite="lax", path="/")
|
||||
return resp
|
||||
|
||||
Reference in New Issue
Block a user