feat(auth): return to the original view after an auth-expiry redirect
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m18s
CI / publish (push) Successful in 45s

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:
2026-06-17 13:05:37 -04:00
parent 2594ca517d
commit 71715c38d8
4 changed files with 102 additions and 8 deletions
+2 -1
View File
@@ -11,7 +11,7 @@
</div>
{% endif %}
{% if oidc_enabled %}
<a href="/login/oidc" class="btn" style="width:100%;text-align:center;margin-bottom:1.25rem;display:block;">
<a href="/login/oidc{% if next_url %}?next={{ next_url | urlencode }}{% endif %}" class="btn" style="width:100%;text-align:center;margin-bottom:1.25rem;display:block;">
Sign in with SSO
</a>
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1.25rem;">
@@ -21,6 +21,7 @@
</div>
{% endif %}
<form method="post" action="/login">
{% if next_url %}<input type="hidden" name="next" value="{{ next_url }}">{% endif %}
<div class="form-group">
<input type="text" name="username" placeholder="Username" autofocus required>
</div>