From a8e6533a1f76502e65ff75b19914af516407a815 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Fri, 10 Apr 2026 23:19:00 +0900 Subject: [PATCH] fix(oauth): use localhost for redirect URI when bound to 0.0.0.0 (#2247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(oauth): use localhost for redirect URI when bound to 0.0.0.0 Google rejects redirect_uri=http://0.0.0.0:/oauth/callback as invalid. When no tunnel public_url is configured, the gateway base URL was built directly from GATEWAY_HOST, which is a bind address — not a routable hostname. Map unspecified addresses (0.0.0.0, ::, [::]) to localhost, matching the social-login flow's existing fallback. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(oauth): use IpAddr::is_unspecified for robust wildcard detection Address PR feedback: replace string matching with std::net::IpAddr parsing so all representations of unspecified addresses (including 0:0:0:0:0:0:0:0) are caught. Add regression tests. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: use is_ok_and instead of map_or(false, ..) to satisfy clippy Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/main.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index f8cbdc3c16..76c4b07c69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -770,7 +770,7 @@ async fn async_main() -> anyhow::Result<()> { .tunnel .public_url .clone() - .unwrap_or_else(|| format!("http://{}:{}", gw_config.host, gw_config.port)); + .unwrap_or_else(|| oauth_base_url(&gw_config.host, gw_config.port)); ext_mgr.enable_gateway_mode(gw_base).await; gw = gw.with_extension_manager(Arc::clone(ext_mgr)); } @@ -1441,3 +1441,45 @@ async fn async_main() -> anyhow::Result<()> { Ok(()) } + +/// Build the OAuth base URL from the gateway bind address and port. +/// +/// Unspecified addresses (`0.0.0.0`, `::`, `[::]`) are mapped to `localhost` +/// because they are valid bind addresses but not valid OAuth redirect hosts. +fn oauth_base_url(host: &str, port: u16) -> String { + let trimmed = host.trim_start_matches('[').trim_end_matches(']'); + let is_unspecified = trimmed + .parse::() + .is_ok_and(|ip| ip.is_unspecified()); + if is_unspecified { + format!("http://localhost:{}", port) + } else { + format!("http://{}:{}", host, port) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn oauth_base_url_maps_unspecified_to_localhost() { + assert_eq!(oauth_base_url("0.0.0.0", 3033), "http://localhost:3033"); + assert_eq!(oauth_base_url("::", 3033), "http://localhost:3033"); + assert_eq!(oauth_base_url("[::]", 3033), "http://localhost:3033"); + assert_eq!( + oauth_base_url("0:0:0:0:0:0:0:0", 3033), + "http://localhost:3033" + ); + } + + #[test] + fn oauth_base_url_preserves_explicit_host() { + assert_eq!(oauth_base_url("127.0.0.1", 3000), "http://127.0.0.1:3000"); + assert_eq!( + oauth_base_url("my-server.example.com", 8080), + "http://my-server.example.com:8080" + ); + assert_eq!(oauth_base_url("::1", 3000), "http://::1:3000"); + } +}