feat(auth): return to the original view after an auth-expiry redirect
When a session has expired, require_role now bounces to /login?next=<path> and sends the user back there after re-login. - middleware: safe_next_url() (same-site relative path/query only; rejects off-site, protocol-relative, javascript:, and the auth pages — no open redirect). _login_redirect() builds the next param; for HTMX requests it uses HX-Current-URL (the page, not the fragment) and HX-Redirect so the whole browser navigates instead of swapping the login page into a fragment. - login GET/POST carry `next` (hidden field), validated, used on success for local + LDAP; OIDC stashes it in session across the IdP round-trip. - login.html: hidden next field + next on the SSO link. - tests for safe_next_url. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
"""Unit tests for safe_next_url — the auth post-login return-path guard."""
|
||||
from steward.auth.middleware import safe_next_url
|
||||
|
||||
|
||||
def test_relative_path_passes():
|
||||
assert safe_next_url("/hosts/") == "/hosts/"
|
||||
assert safe_next_url("/hosts/abc?range=24h") == "/hosts/abc?range=24h"
|
||||
|
||||
|
||||
def test_full_url_reduced_to_path():
|
||||
# HTMX HX-Current-URL is absolute; we keep only the same-site path+query.
|
||||
assert safe_next_url("http://steward.local/d/1") == "/d/1"
|
||||
assert safe_next_url("https://steward.local/hosts/x?a=1") == "/hosts/x?a=1"
|
||||
|
||||
|
||||
def test_offsite_host_is_dropped_not_redirected():
|
||||
# A crafted absolute URL can't become an open redirect — netloc is discarded.
|
||||
assert safe_next_url("http://evil.com/phish") == "/phish"
|
||||
|
||||
|
||||
def test_protocol_relative_rejected():
|
||||
assert safe_next_url("//evil.com/x") is None
|
||||
assert safe_next_url("/\\evil.com") is None
|
||||
|
||||
|
||||
def test_scheme_only_rejected():
|
||||
assert safe_next_url("javascript:alert(1)") is None
|
||||
|
||||
|
||||
def test_auth_pages_not_returned_to():
|
||||
assert safe_next_url("/login") is None
|
||||
assert safe_next_url("/login?next=/x") is None
|
||||
assert safe_next_url("/logout") is None
|
||||
assert safe_next_url("/setup") is None
|
||||
|
||||
|
||||
def test_empty_and_none():
|
||||
assert safe_next_url(None) is None
|
||||
assert safe_next_url("") is None
|
||||
Reference in New Issue
Block a user