fix: app login redirect broken after forward_auth challenge
Two bugs: 1. verify() built the login URL with `//login` (double slash) — now `/login` 2. safe_path() rejected absolute https:// next-URLs, so after login the user was silently dropped at `/` instead of their original app URL. Replaced safe_path with safe_redirect(next, domain) which allows relative paths OR absolute URLs whose host is the configured domain (or a subdomain). safe_path is kept as a thin wrapper (domain="") for the admin-UI middleware where next is always a relative path. https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
This commit is contained in:
parent
c261f9d133
commit
4aea0357b6
1 changed files with 22 additions and 9 deletions
|
|
@ -60,13 +60,26 @@ pub async fn current_user_id(state: &crate::AppState, headers: &HeaderMap) -> Op
|
|||
state.sessions.lock().await.get(&token).cloned()
|
||||
}
|
||||
|
||||
/// Reject open-redirect: only allow relative paths.
|
||||
fn safe_path(next: &str) -> String {
|
||||
/// Reject open-redirect: allow relative paths or absolute URLs on the same domain.
|
||||
fn safe_redirect(next: &str, domain: &str) -> String {
|
||||
if next.starts_with('/') && !next.starts_with("//") {
|
||||
next.to_string()
|
||||
} else {
|
||||
"/".to_string()
|
||||
return next.to_string();
|
||||
}
|
||||
// Allow https:// URLs whose host is the domain or a subdomain of it.
|
||||
for prefix in &["https://", "http://"] {
|
||||
if let Some(rest) = next.strip_prefix(prefix) {
|
||||
let host = rest.split('/').next().unwrap_or("");
|
||||
if host == domain || host.ends_with(&format!(".{}", domain)) {
|
||||
return next.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
"/".to_string()
|
||||
}
|
||||
|
||||
/// Reject open-redirect: only allow relative paths (used for admin-UI redirects).
|
||||
fn safe_path(next: &str) -> String {
|
||||
safe_redirect(next, "")
|
||||
}
|
||||
|
||||
// ── Control-plane middleware (admin users only) ───────────────────────────────
|
||||
|
|
@ -138,7 +151,7 @@ pub async fn verify(
|
|||
format!("{}://{}{}", proto, host, uri)
|
||||
};
|
||||
let login_url = format!(
|
||||
"https://{}//login?next={}",
|
||||
"https://{}/login?next={}",
|
||||
state.domain_suffix,
|
||||
urlencoding_simple(&next_url)
|
||||
);
|
||||
|
|
@ -232,10 +245,10 @@ pub async fn login_page(
|
|||
) -> Response {
|
||||
// Already logged in → redirect.
|
||||
if current_user_id(&state, &headers).await.is_some() {
|
||||
let next = params.next.as_deref().map(safe_path).unwrap_or_else(|| "/".into());
|
||||
let next = params.next.as_deref().map(|n| safe_redirect(n, &state.domain_suffix)).unwrap_or_else(|| "/".into());
|
||||
return Redirect::to(&next).into_response();
|
||||
}
|
||||
let next = params.next.map(|s| safe_path(&s)).unwrap_or_else(|| "/".into());
|
||||
let next = params.next.map(|s| safe_redirect(&s, &state.domain_suffix)).unwrap_or_else(|| "/".into());
|
||||
Html(login_html(&next, None)).into_response()
|
||||
}
|
||||
|
||||
|
|
@ -243,7 +256,7 @@ pub async fn handle_login(
|
|||
State(state): State<crate::AppState>,
|
||||
Form(form): Form<LoginForm>,
|
||||
) -> Response {
|
||||
let next = form.next.as_deref().map(safe_path).unwrap_or_else(|| "/".into());
|
||||
let next = form.next.as_deref().map(|n| safe_redirect(n, &state.domain_suffix)).unwrap_or_else(|| "/".into());
|
||||
|
||||
// --- env-var bootstrap path (used before any DB user exists) ---
|
||||
let using_bootstrap = matches!(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue