From 93c98e0d91182b175bb0a9fad8de4052e9d55f0b Mon Sep 17 00:00:00 2001 From: Gagan Yarramsetty Date: Tue, 26 May 2026 12:46:19 +0530 Subject: [PATCH 1/6] Fix CWE-601 open redirect vulnerability --- src/handlers/http/oidc.rs | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/handlers/http/oidc.rs b/src/handlers/http/oidc.rs index 51d1355cd..bb6d24a25 100644 --- a/src/handlers/http/oidc.rs +++ b/src/handlers/http/oidc.rs @@ -78,11 +78,10 @@ pub async fn login( req: HttpRequest, query: web::Query, ) -> Result { - let conn = req.connection_info().clone(); - let base_url_without_scheme = format!("{}/", conn.host()); - if !is_valid_redirect_url(&base_url_without_scheme, query.redirect.as_str()) { + let canonical_origin = get_canonical_origin(); + if !is_valid_redirect_url_safe(&canonical_origin, &query.redirect) { return Err(OIDCError::BadRequest( - "Bad Request, Invalid Redirect URL!".to_string(), + "Bad Request,Invalid Redirect URL!".to_string(), )); } @@ -169,7 +168,12 @@ pub async fn login( pub async fn logout(req: HttpRequest, query: web::Query) -> HttpResponse { let oidc_client = OIDC_CLIENT.get(); - + let canonical_origin = get_canonical_origin(); + if !is_valid_redirect_url_safe(&canonical_origin, &query.redirect) { + return HttpResponse::BadRequest().json(serde_json::json!({ + "error":"Invalid redirect URL" + })); + } let Some(session) = extract_session_key_from_req(&req).ok() else { return redirect_to_client(query.redirect.as_str(), None); }; @@ -627,10 +631,25 @@ impl actix_web::ResponseError for OIDCError { .body(self.to_string()) } } +// get the canonical origin for this PARSEABLE instance +fn get_canonical_origin() -> Url { + if let Some(origin_url) = &PARSEABLE.options.domain_address { + return origin_url.clone(); + } + let addr = &PARSEABLE.options.address; + // detect http vs https based on TLS cert/key + let scheme = PARSEABLE.options.get_scheme(); + match Url::parse(&format!("{}://{}", scheme, addr)) { + Ok(url) => url, + Err(_) => Url::parse("http://localhost:8000").unwrap(), + } +} -fn is_valid_redirect_url(base_url_without_scheme: &str, redirect_url: &str) -> bool { - let http_scheme_match_regex = Regex::new(r"^(https?://)").unwrap(); - let redirect_url_without_scheme = http_scheme_match_regex.replace(redirect_url, ""); - - base_url_without_scheme == redirect_url_without_scheme +fn is_valid_redirect_url_safe(canonical: &Url, redirect: &Url) -> bool { + // relative paths are always safe + if !redirect.has_host() { + return true; + } + // absolute urls must match canonical origin exactly + redirect.origin() == canonical.origin() } From 1b39db860c7c6befd9a22048546feaad0e2260d6 Mon Sep 17 00:00:00 2001 From: Gagan Yarramsetty Date: Wed, 27 May 2026 15:32:25 +0530 Subject: [PATCH 2/6] prevent open redirect vulnerability in OIDC flow --- src/handlers/http/oidc.rs | 113 ++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/src/handlers/http/oidc.rs b/src/handlers/http/oidc.rs index bb6d24a25..a126a1bf4 100644 --- a/src/handlers/http/oidc.rs +++ b/src/handlers/http/oidc.rs @@ -36,7 +36,6 @@ pub fn set_cookie_cross_site(enabled: bool) { } use chrono::{Duration, TimeDelta}; use openid::Bearer; -use regex::Regex; use serde::Deserialize; use ulid::Ulid; use url::Url; @@ -65,13 +64,12 @@ pub struct Login { pub code: String, pub state: Option, } - /// Struct representing query param when visiting /login /// Caller can set the state for code auth flow and this is /// at the end used as target for redirect #[derive(Deserialize, Debug)] pub struct RedirectAfterLogin { - pub redirect: Url, + pub redirect: String, } pub async fn login( @@ -79,22 +77,21 @@ pub async fn login( query: web::Query, ) -> Result { let canonical_origin = get_canonical_origin(); - if !is_valid_redirect_url_safe(&canonical_origin, &query.redirect) { - return Err(OIDCError::BadRequest( - "Bad Request,Invalid Redirect URL!".to_string(), - )); - } + let redirect = is_valid_redirect_url_safe(&canonical_origin, &query.redirect) + .map_err(|_| OIDCError::BadRequest("Bad Request,Invalid Redirect URL!".to_string()))?; let oidc_client = OIDC_CLIENT.get(); let session_key = extract_session_key_from_req(&req).ok(); let (session_key, oidc_client) = match (session_key, oidc_client) { - (None, None) => return Ok(redirect_no_oauth_setup(query.redirect.clone())), + (None, None) => return Ok(redirect_no_oauth_setup(&canonical_origin)), (None, Some(client)) => { - let redirect = query.into_inner().redirect.to_string(); - let scope = PARSEABLE.options.scope.to_string(); - let mut auth_url: String = client.read().await.auth_url(&scope, Some(redirect)).into(); + let mut auth_url: String = client + .read() + .await + .auth_url(&scope, Some(redirect.clone())) + .into(); auth_url.push_str("&access_type=offline&prompt=consent"); return Ok(HttpResponse::TemporaryRedirect() @@ -133,7 +130,7 @@ pub async fn login( ); Ok(redirect_to_client( - query.redirect.as_str(), + &redirect, [user_cookie, user_id_cookie, session_cookie], )) } @@ -142,23 +139,22 @@ pub async fn login( // if it's a valid active session, just redirect back key @ SessionKey::SessionId(_) => { let resp = if Users.session_exists(&key) { - redirect_to_client(query.redirect.as_str(), None) + redirect_to_client(&redirect, None) } else { Users.remove_session(&key); if let Some(oidc_client) = oidc_client { - let redirect = query.into_inner().redirect.to_string(); let scope = PARSEABLE.options.scope.to_string(); let mut auth_url: String = oidc_client .read() .await - .auth_url(&scope, Some(redirect)) + .auth_url(&scope, Some(redirect.clone())) .into(); auth_url.push_str("&access_type=offline&prompt=consent"); HttpResponse::TemporaryRedirect() .insert_header((actix_web::http::header::LOCATION, auth_url)) .finish() } else { - redirect_to_client(query.redirect.as_str(), None) + redirect_to_client(&redirect, None) } }; Ok(resp) @@ -169,13 +165,16 @@ pub async fn login( pub async fn logout(req: HttpRequest, query: web::Query) -> HttpResponse { let oidc_client = OIDC_CLIENT.get(); let canonical_origin = get_canonical_origin(); - if !is_valid_redirect_url_safe(&canonical_origin, &query.redirect) { - return HttpResponse::BadRequest().json(serde_json::json!({ - "error":"Invalid redirect URL" - })); - } + let redirect = match is_valid_redirect_url_safe(&canonical_origin, &query.redirect) { + Ok(redirect) => redirect, + Err(_) => { + return HttpResponse::BadRequest().json(serde_json::json!({ + "error":"Invalid redirect URL" + })); + } + }; let Some(session) = extract_session_key_from_req(&req).ok() else { - return redirect_to_client(query.redirect.as_str(), None); + return redirect_to_client(&redirect, None); }; let tenant_id = get_tenant_id_from_key(&session); let user = Users.remove_session(&session); @@ -189,9 +188,9 @@ pub async fn logout(req: HttpRequest, query: web::Query) -> (Some(username), Some(logout_endpoint)) if Users.is_oauth(&username, &tenant_id).unwrap_or_default() => { - redirect_to_oidc_logout(logout_endpoint, &query.redirect) + redirect_to_oidc_logout(logout_endpoint, &redirect, &canonical_origin) } - _ => redirect_to_client(query.redirect.as_str(), None), + _ => redirect_to_client(&redirect, None), } } @@ -368,11 +367,18 @@ fn build_login_response( "user_id": user_id, })) } else { - let redirect_url = login_query - .state - .clone() - .unwrap_or_else(|| PARSEABLE.options.address.to_string()); - + let canonical_origin = get_canonical_origin(); + let redirect_url = match login_query.state.as_deref() { + Some(state) => match is_valid_redirect_url_safe(&canonical_origin, state) { + Ok(redirect_url) => redirect_url, + Err(_) => { + return HttpResponse::BadRequest().json(serde_json::json!({ + "error":"Invalid redirect URL" + })); + } + }, + None => canonical_origin.to_string(), + }; redirect_to_client(&redirect_url, cookies) } } @@ -413,8 +419,17 @@ fn exchange_basic_for_cookie( cookie_session(id) } -fn redirect_to_oidc_logout(mut logout_endpoint: Url, redirect: &Url) -> HttpResponse { - logout_endpoint.set_query(Some(&format!("post_logout_redirect_uri={redirect}"))); +fn redirect_to_oidc_logout( + mut logout_endpoint: Url, + redirect: &str, + canonical: &Url, +) -> HttpResponse { + let post_logout_redirect = + absolute_redirect_url(canonical, redirect).unwrap_or_else(|| canonical.to_string()); + logout_endpoint.set_query(None); + logout_endpoint + .query_pairs_mut() + .append_pair("post_logout_redirect_uri", &post_logout_redirect); HttpResponse::TemporaryRedirect() .insert_header((actix_web::http::header::CACHE_CONTROL, "no-store")) .insert_header(( @@ -438,8 +453,10 @@ pub fn redirect_to_client( response.finish() } -fn redirect_no_oauth_setup(mut url: Url) -> HttpResponse { - url.set_path("oidc-not-configured"); +fn redirect_no_oauth_setup(canonical: &Url) -> HttpResponse { + let url = canonical + .join("/oidc-not-configured") + .unwrap_or_else(|_| canonical.clone()); let mut response = HttpResponse::MovedPermanently(); response.insert_header((actix_web::http::header::LOCATION, url.as_str())); response.insert_header((actix_web::http::header::CACHE_CONTROL, "no-store")); @@ -645,11 +662,27 @@ fn get_canonical_origin() -> Url { } } -fn is_valid_redirect_url_safe(canonical: &Url, redirect: &Url) -> bool { - // relative paths are always safe - if !redirect.has_host() { - return true; +fn is_valid_redirect_url_safe(canonical: &Url, redirect: &str) -> Result { + if redirect.is_empty() { + return Err(()); + } + if redirect.starts_with("/") && !redirect.starts_with("//") { + return Ok(redirect.to_string()); + } + let url = Url::parse(redirect).map_err(|_| ())?; + if !matches!(url.scheme(), "http" | "https") { + return Err(()); + } + if url.origin() != canonical.origin() { + return Err(()); + } + Ok(url.to_string()) +} + +fn absolute_redirect_url(canonical: &Url, redirect: &str) -> Option { + if redirect.starts_with("/") && !redirect.starts_with("//") { + canonical.join(redirect).ok().map(|url| url.to_string()) + } else { + Url::parse(redirect).ok().map(|url| url.to_string()) } - // absolute urls must match canonical origin exactly - redirect.origin() == canonical.origin() } From 0ec18bcf9266a5dd8d65748b3cbd5be5658337e3 Mon Sep 17 00:00:00 2001 From: Gagan Yarramsetty Date: Wed, 27 May 2026 19:21:34 +0530 Subject: [PATCH 3/6] Added Comments --- helm/values.yaml | 3 +++ src/cli.rs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/helm/values.yaml b/helm/values.yaml index 1f757abfc..595f855e3 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -50,6 +50,9 @@ parseable: ## Add environment variables to the Parseable Deployment env: RUST_LOG: warn + # Set this when Parseable is accessed through a reverse proxy/TLS terminator. + # Must match the public URL users access. + # P_ORIGIN_URI: "https://parseable.example.com" ## Enable to create a log stream and then add retention configuration ## for that log stream # logstream: diff --git a/src/cli.rs b/src/cli.rs index 50f1202c9..d848af737 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -198,7 +198,7 @@ pub struct Options { long = "origin", env = "P_ORIGIN_URI", value_parser = validation::url, - help = "Parseable server global domain address" + help = "Public canonical origin for Parseable, used for OIDC redirects. Set this when running behind a reverse proxy or TLS terminator" )] pub domain_address: Option, From 71c94d8682d294a399e0e1f03c09d55ff2585485 Mon Sep 17 00:00:00 2001 From: Gagan Yarramsetty Date: Thu, 28 May 2026 07:51:51 +0530 Subject: [PATCH 4/6] fix OIDC redirect deserialization and error handling --- src/handlers/http/oidc.rs | 59 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/handlers/http/oidc.rs b/src/handlers/http/oidc.rs index a126a1bf4..44f982617 100644 --- a/src/handlers/http/oidc.rs +++ b/src/handlers/http/oidc.rs @@ -69,6 +69,7 @@ pub struct Login { /// at the end used as target for redirect #[derive(Deserialize, Debug)] pub struct RedirectAfterLogin { + #[serde(default)] pub redirect: String, } @@ -78,7 +79,10 @@ pub async fn login( ) -> Result { let canonical_origin = get_canonical_origin(); let redirect = is_valid_redirect_url_safe(&canonical_origin, &query.redirect) - .map_err(|_| OIDCError::BadRequest("Bad Request,Invalid Redirect URL!".to_string()))?; + .map_err(|_| { + tracing::warn!("Invalid redirect URL provided: '{}'", query.redirect); + OIDCError::BadRequest("Invalid Redirect URL".to_string()) + })?; let oidc_client = OIDC_CLIENT.get(); @@ -162,36 +166,33 @@ pub async fn login( } } -pub async fn logout(req: HttpRequest, query: web::Query) -> HttpResponse { +pub async fn logout(req: HttpRequest, query: web::Query) -> Result { let oidc_client = OIDC_CLIENT.get(); let canonical_origin = get_canonical_origin(); - let redirect = match is_valid_redirect_url_safe(&canonical_origin, &query.redirect) { - Ok(redirect) => redirect, - Err(_) => { - return HttpResponse::BadRequest().json(serde_json::json!({ - "error":"Invalid redirect URL" - })); - } - }; - let Some(session) = extract_session_key_from_req(&req).ok() else { - return redirect_to_client(&redirect, None); - }; - let tenant_id = get_tenant_id_from_key(&session); - let user = Users.remove_session(&session); - let logout_endpoint = if let Some(client) = oidc_client { - client.read().await.logout_url() - } else { - None - }; - - match (user, logout_endpoint) { - (Some(username), Some(logout_endpoint)) - if Users.is_oauth(&username, &tenant_id).unwrap_or_default() => - { - redirect_to_oidc_logout(logout_endpoint, &redirect, &canonical_origin) - } - _ => redirect_to_client(&redirect, None), - } + let redirect = is_valid_redirect_url_safe(&canonical_origin, &query.redirect) + .map_err(|_| { + tracing::warn!("Invalid redirect URL provided in logout: '{}'", query.redirect); + OIDCError::BadRequest("Invalid Redirect URL".to_string()) + })?; + let Some(session) = extract_session_key_from_req(&req).ok() else { + return Ok(redirect_to_client(&redirect, None)); + }; + let tenant_id = get_tenant_id_from_key(&session); + let user = Users.remove_session(&session); + let logout_endpoint = if let Some(client) = oidc_client { + client.read().await.logout_url() + } else { + None + }; + + Ok(match (user, logout_endpoint) { + (Some(username), Some(logout_endpoint)) + if Users.is_oauth(&username, &tenant_id).unwrap_or_default() => + { + redirect_to_oidc_logout(logout_endpoint, &redirect, &canonical_origin) + } + _ => redirect_to_client(&redirect, None), + }) } /// Handler for code callback From 3301320103b71fba99dc58c0155ac8128dbf0bb5 Mon Sep 17 00:00:00 2001 From: Gagan Yarramsetty Date: Thu, 28 May 2026 08:33:40 +0530 Subject: [PATCH 5/6] Fixed few errors --- helm/values.yaml | 4 +- src/handlers/http/oidc.rs | 106 ++++++++++++++++++++++++++++---------- 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/helm/values.yaml b/helm/values.yaml index 595f855e3..d427c6821 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -51,8 +51,8 @@ parseable: env: RUST_LOG: warn # Set this when Parseable is accessed through a reverse proxy/TLS terminator. - # Must match the public URL users access. - # P_ORIGIN_URI: "https://parseable.example.com" + # Must match the public URL users access. + # P_ORIGIN_URI: "https://parseable.example.com" ## Enable to create a log stream and then add retention configuration ## for that log stream # logstream: diff --git a/src/handlers/http/oidc.rs b/src/handlers/http/oidc.rs index 44f982617..1d21c4b65 100644 --- a/src/handlers/http/oidc.rs +++ b/src/handlers/http/oidc.rs @@ -78,8 +78,8 @@ pub async fn login( query: web::Query, ) -> Result { let canonical_origin = get_canonical_origin(); - let redirect = is_valid_redirect_url_safe(&canonical_origin, &query.redirect) - .map_err(|_| { + let redirect = + is_valid_redirect_url_safe(&canonical_origin, &query.redirect).map_err(|_| { tracing::warn!("Invalid redirect URL provided: '{}'", query.redirect); OIDCError::BadRequest("Invalid Redirect URL".to_string()) })?; @@ -166,33 +166,39 @@ pub async fn login( } } -pub async fn logout(req: HttpRequest, query: web::Query) -> Result { +pub async fn logout( + req: HttpRequest, + query: web::Query, +) -> Result { let oidc_client = OIDC_CLIENT.get(); let canonical_origin = get_canonical_origin(); - let redirect = is_valid_redirect_url_safe(&canonical_origin, &query.redirect) - .map_err(|_| { - tracing::warn!("Invalid redirect URL provided in logout: '{}'", query.redirect); + let redirect = + is_valid_redirect_url_safe(&canonical_origin, &query.redirect).map_err(|_| { + tracing::warn!( + "Invalid redirect URL provided in logout: '{}'", + query.redirect + ); OIDCError::BadRequest("Invalid Redirect URL".to_string()) })?; - let Some(session) = extract_session_key_from_req(&req).ok() else { - return Ok(redirect_to_client(&redirect, None)); - }; - let tenant_id = get_tenant_id_from_key(&session); - let user = Users.remove_session(&session); - let logout_endpoint = if let Some(client) = oidc_client { - client.read().await.logout_url() - } else { - None - }; - - Ok(match (user, logout_endpoint) { - (Some(username), Some(logout_endpoint)) - if Users.is_oauth(&username, &tenant_id).unwrap_or_default() => - { - redirect_to_oidc_logout(logout_endpoint, &redirect, &canonical_origin) - } - _ => redirect_to_client(&redirect, None), - }) + let Some(session) = extract_session_key_from_req(&req).ok() else { + return Ok(redirect_to_client(&redirect, None)); + }; + let tenant_id = get_tenant_id_from_key(&session); + let user = Users.remove_session(&session); + let logout_endpoint = if let Some(client) = oidc_client { + client.read().await.logout_url() + } else { + None + }; + + Ok(match (user, logout_endpoint) { + (Some(username), Some(logout_endpoint)) + if Users.is_oauth(&username, &tenant_id).unwrap_or_default() => + { + redirect_to_oidc_logout(logout_endpoint, &redirect, &canonical_origin) + } + _ => redirect_to_client(&redirect, None), + }) } /// Handler for code callback @@ -665,9 +671,9 @@ fn get_canonical_origin() -> Url { fn is_valid_redirect_url_safe(canonical: &Url, redirect: &str) -> Result { if redirect.is_empty() { - return Err(()); + return Ok(canonical.to_string()); } - if redirect.starts_with("/") && !redirect.starts_with("//") { + if redirect.starts_with('/') && !redirect.starts_with("//") { return Ok(redirect.to_string()); } let url = Url::parse(redirect).map_err(|_| ())?; @@ -681,9 +687,53 @@ fn is_valid_redirect_url_safe(canonical: &Url, redirect: &str) -> Result Option { - if redirect.starts_with("/") && !redirect.starts_with("//") { + if redirect.is_empty() { + return Some(canonical.to_string()); + } + if redirect.starts_with('/') && !redirect.starts_with("//") { canonical.join(redirect).ok().map(|url| url.to_string()) } else { Url::parse(redirect).ok().map(|url| url.to_string()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_redirect_uses_canonical_origin() { + let canonical = Url::parse("https://parseable.example.com/").unwrap(); + + assert_eq!( + is_valid_redirect_url_safe(&canonical, "").unwrap(), + canonical.to_string() + ); + assert_eq!( + absolute_redirect_url(&canonical, "").unwrap(), + canonical.to_string() + ); + } + + #[test] + fn relative_redirect_is_allowed_and_can_be_made_absolute() { + let canonical = Url::parse("https://parseable.example.com/").unwrap(); + + assert_eq!( + is_valid_redirect_url_safe(&canonical, "/dashboard").unwrap(), + "/dashboard" + ); + assert_eq!( + absolute_redirect_url(&canonical, "/dashboard").unwrap(), + "https://parseable.example.com/dashboard" + ); + } + + #[test] + fn cross_origin_redirect_is_rejected() { + let canonical = Url::parse("https://parseable.example.com/").unwrap(); + + assert!(is_valid_redirect_url_safe(&canonical, "https://evil.example.com/").is_err()); + assert!(is_valid_redirect_url_safe(&canonical, "//evil.example.com/").is_err()); + } +} From dd150d505231c213e4b7a70729307cbcce61be7d Mon Sep 17 00:00:00 2001 From: Gagan Yarramsetty Date: Thu, 28 May 2026 19:04:45 +0530 Subject: [PATCH 6/6] redirect url error fixed --- src/handlers/http/oidc.rs | 11 ++++------- src/oidc.rs | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/handlers/http/oidc.rs b/src/handlers/http/oidc.rs index 1d21c4b65..944a37641 100644 --- a/src/handlers/http/oidc.rs +++ b/src/handlers/http/oidc.rs @@ -660,13 +660,10 @@ fn get_canonical_origin() -> Url { if let Some(origin_url) = &PARSEABLE.options.domain_address { return origin_url.clone(); } - let addr = &PARSEABLE.options.address; - // detect http vs https based on TLS cert/key - let scheme = PARSEABLE.options.get_scheme(); - match Url::parse(&format!("{}://{}", scheme, addr)) { - Ok(url) => url, - Err(_) => Url::parse("http://localhost:8000").unwrap(), - } + crate::oidc::canonical_local_origin( + &PARSEABLE.options.address, + PARSEABLE.options.tls_cert_path.is_some() && PARSEABLE.options.tls_key_path.is_some(), + ) } fn is_valid_redirect_url_safe(canonical: &Url, redirect: &str) -> Result { diff --git a/src/oidc.rs b/src/oidc.rs index 9071cb455..17d0d4483 100644 --- a/src/oidc.rs +++ b/src/oidc.rs @@ -54,10 +54,7 @@ impl OpenidConfig { redirect_to: &str, ) -> Result { let redirect_uri = match self.origin { - Origin::Local { socket_addr, https } => { - let protocol = if https { "https" } else { "http" }; - url::Url::parse(&format!("{protocol}://{socket_addr}")).expect("valid url") - } + Origin::Local { socket_addr, https } => canonical_local_origin(&socket_addr, https), Origin::Production(url) => url, }; @@ -67,6 +64,37 @@ impl OpenidConfig { } } +pub fn canonical_local_origin(socket_addr: &str, https: bool) -> Url { + let scheme = if https { "https" } else { "http" }; + let mut url = Url::parse(&format!("{scheme}://{socket_addr}")) + .unwrap_or_else(|_| Url::parse(&format!("{scheme}://localhost:8000")).unwrap()); + if matches!(url.host_str(), Some("0.0.0.0") | Some("::")) { + url.set_host(Some("localhost")).expect("localhost is valid"); + } + url +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn wildcard_ipv4_origin_uses_localhost_with_configured_port() { + assert_eq!( + canonical_local_origin("0.0.0.0:8000", false).as_str(), + "http://localhost:8000/" + ); + } + + #[test] + fn loopback_ipv4_origin_is_preserved() { + assert_eq!( + canonical_local_origin("127.0.0.1:9000", false).as_str(), + "http://127.0.0.1:9000/" + ); + } +} + #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] pub struct Claims { #[serde(flatten)]