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()
|
state.sessions.lock().await.get(&token).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reject open-redirect: only allow relative paths.
|
/// Reject open-redirect: allow relative paths or absolute URLs on the same domain.
|
||||||
fn safe_path(next: &str) -> String {
|
fn safe_redirect(next: &str, domain: &str) -> String {
|
||||||
if next.starts_with('/') && !next.starts_with("//") {
|
if next.starts_with('/') && !next.starts_with("//") {
|
||||||
next.to_string()
|
return next.to_string();
|
||||||
} else {
|
|
||||||
"/".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) ───────────────────────────────
|
// ── Control-plane middleware (admin users only) ───────────────────────────────
|
||||||
|
|
@ -138,7 +151,7 @@ pub async fn verify(
|
||||||
format!("{}://{}{}", proto, host, uri)
|
format!("{}://{}{}", proto, host, uri)
|
||||||
};
|
};
|
||||||
let login_url = format!(
|
let login_url = format!(
|
||||||
"https://{}//login?next={}",
|
"https://{}/login?next={}",
|
||||||
state.domain_suffix,
|
state.domain_suffix,
|
||||||
urlencoding_simple(&next_url)
|
urlencoding_simple(&next_url)
|
||||||
);
|
);
|
||||||
|
|
@ -232,10 +245,10 @@ pub async fn login_page(
|
||||||
) -> Response {
|
) -> Response {
|
||||||
// Already logged in → redirect.
|
// Already logged in → redirect.
|
||||||
if current_user_id(&state, &headers).await.is_some() {
|
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();
|
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()
|
Html(login_html(&next, None)).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,7 +256,7 @@ pub async fn handle_login(
|
||||||
State(state): State<crate::AppState>,
|
State(state): State<crate::AppState>,
|
||||||
Form(form): Form<LoginForm>,
|
Form(form): Form<LoginForm>,
|
||||||
) -> Response {
|
) -> 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) ---
|
// --- env-var bootstrap path (used before any DB user exists) ---
|
||||||
let using_bootstrap = matches!(
|
let using_bootstrap = matches!(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue