fix(oauth): use localhost for redirect URI when bound to 0.0.0.0 (#2247)

* fix(oauth): use localhost for redirect URI when bound to 0.0.0.0

Google rejects redirect_uri=http://0.0.0.0:<port>/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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* fix: use is_ok_and instead of map_or(false, ..) to satisfy clippy

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Illia Polosukhin
2026-04-10 23:19:00 +09:00
committed by GitHub
parent 494636d64a
commit a8e6533a1f

View File

@@ -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::<std::net::IpAddr>()
.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");
}
}