Determine this is the right repository
Summary of the issue
Context
On Windows, localhost resolves ::1 before 127.0.0.1. InstalledAppFlow.run_local_server() advertises the name localhost in redirect_uri, but the server it starts is AF_INET only and never listens on ::1.
If any other process is listening on [::]:PORT (the default for Go's net.Listen("tcp", ":PORT")), the browser's redirect goes to that process instead. The authorization code is delivered to it and run_local_server() waits forever.
Expected Behavior:
Either the callback reaches the local server, or the bind fails with a clear error.
Actual Behavior:
The bind succeeds, the browser shows the other process's response, and run_local_server() hangs. Neither side reports an error.
The SO_EXCLUSIVEADDRUSE fix added in #18166 does not prevent this. I measured it - see "Reproduction steps: actual results".
API client name and version
google-auth-oauthlib (vendored in Google Cloud SDK 548.0.0; same code path in current main of packages/google-auth-oauthlib)
Reproduction steps: code
Reproduced through gcloud auth login --launch-browser, which calls run_local_server().
The relevant code, packages/google-auth-oauthlib/google_auth_oauthlib/flow.py:
def run_local_server(
self,
host="localhost",
...
local_server = wsgiref.simple_server.make_server(
bind_addr or host, port, wsgi_app,
server_class=_ExclusiveWSGIServer, handler_class=_WSGIRequestHandler,
)
...
self.redirect_uri = redirect_uri_format.format(host, local_server.server_port)
wsgiref.simple_server.WSGIServer.address_family is socket.AF_INET, and _ExclusiveWSGIServer overrides only server_bind, so the address family is unchanged. host="localhost" resolves to 127.0.0.1 and the server never listens on ::1, while redirect_uri still carries the name localhost.
file: bind_test.py
import socket
PORT = 8085
def try_bind(label, family, addr, exclusive):
s = socket.socket(family, socket.SOCK_STREAM)
try:
if exclusive:
s.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
s.bind((addr, PORT))
s.listen(1)
print(f"[bind OK] {label}")
except OSError as e:
print(f"[bind FAIL] {label}: {e}")
finally:
s.close()
try_bind("127.0.0.1 without SO_EXCLUSIVEADDRUSE", socket.AF_INET, "127.0.0.1", False)
try_bind("127.0.0.1 with SO_EXCLUSIVEADDRUSE", socket.AF_INET, "127.0.0.1", True)
try_bind("::1 without SO_EXCLUSIVEADDRUSE", socket.AF_INET6, "::1", False)
Reproduction steps: supporting files
Any process holding the IPv6 wildcard on the port is enough. A Go program using the default listen address does this:
file: other.go
package main
import "net/http"
func main() {
// net.Listen("tcp", ":8085") creates a dual-stack IPv6 socket bound to [::]
http.ListenAndServe(":8085", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"error":"Unauthorized","success":false}`))
}))
}
Run it first, then run gcloud auth login --launch-browser (or any run_local_server() call) in another shell.
Reproduction steps: actual results
With a Go service holding [::]:8085:
> Get-NetTCPConnection -LocalPort 8nd the browser still reaches the other process.
The third line matters for the fix: `::1` is bindable even while `[::]` is held.085 -State Listen |
ForEach-Object { $p = Get-Process -Id $_.OwningProcess; "{0,-12} PID={1,-6} {2}" -f $_.LocalAddress, $_.OwningProcess, $p.ProcessName }
:: PID=11696 main <- the other service
127.0.0.1 PID=30012 python <- the callback server
localhost resolution on this machine (hosts file has both entries commented out; Windows resolves it internally):
Name Type IPAddress
localhost AAAA ::1 <- tried first
localhost A 127.0.0.1
gcloud printed redirect_uri=http://localhost:8085/. After completing the consent screen the browser landed on http://localhost:8085/?state=...&code=... and displayed the other service's body:
{"error":"Unauthorized","success":false}
run_local_server() never returned.
bind_test.py, run while the Go service holds [::]:8085:
[bind OK] 127.0.0.1 without SO_EXCLUSIVEADDRUSE
[bind OK] 127.0.0.1 with SO_EXCLUSIVEADDRUSE
[bind OK] ::1 without SO_EXCLUSIVEADDRUSE
On Windows a wildcard [::] bind and a specific-address bind coexist, so SO_EXCLUSIVEADDRUSE sees no conflict. The bind succeeds, the server believes it owns the port, a
Reproduction steps: expected results
Either of these would be correct:
- The callback reaches the local server and
run_local_server() returns credentials, or
- The bind fails with a clear error, e.g.
[bind FAIL] 127.0.0.1 with SO_EXCLUSIVEADDRUSE: [WinError 10048] Only one usage of each socket address ... is normally permitted
What must not happen is the current outcome: a successful bind, an authorization code handed to an unrelated local process, and a silent hang.
OS & version + platform
Windows 11 Home 10.0.26200.0
Python environment
Python 3.12.10 (bundled with Google Cloud SDK)
Python dependencies
google-auth-oauthlib as vendored in Google Cloud SDK 548.0.0 (lib/third_party/google_auth_oauthlib), not a pip install.
The same code path is present in current main of packages/google-auth-oauthlib.
Additional context
Suggested fix
Any one of these removes the mismatch:
- Advertise the loopback IP literal instead of the name - build
redirect_uri from 127.0.0.1. RFC 8252 section 7.3 recommends this precisely because name resolution is outside the client's control, and Google's OAuth documentation accepts http://127.0.0.1:<port>.
- When the advertised host is
localhost, listen on both 127.0.0.1 and ::1. The measurement above shows ::1 is bindable in this situation.
- At minimum, make the failure visible: after binding, request the advertised
redirect_uri and verify the response came from this server. Today the failure mode is complete silence on both sides, which is what makes it expensive to diagnose.
Workaround for users hitting this
Re-open the redirect URL with the host replaced by the IP literal, keeping the query string intact:
http://127.0.0.1:8085/?state=...&code=...&scope=...
This reaches the IPv4 bind, the code is received and the login completes.
Note for the Cloud SDK
gcloud 548.0.0 vendors a copy of google_auth_oauthlib that predates #18166 - it has no _ExclusiveWSGIServer and no SO_EXCLUSIVEADDRUSE (zero matches in lib/third_party/google_auth_oauthlib/flow.py). Re-vendoring is worth doing on its own merits, but as measured above it would not fix this particular failure.
Determine this is the right repository
Summary of the issue
Context
On Windows,
localhostresolves::1before127.0.0.1.InstalledAppFlow.run_local_server()advertises the namelocalhostinredirect_uri, but the server it starts isAF_INETonly and never listens on::1.If any other process is listening on
[::]:PORT(the default for Go'snet.Listen("tcp", ":PORT")), the browser's redirect goes to that process instead. The authorization code is delivered to it andrun_local_server()waits forever.Expected Behavior:
Either the callback reaches the local server, or the bind fails with a clear error.
Actual Behavior:
The bind succeeds, the browser shows the other process's response, and
run_local_server()hangs. Neither side reports an error.The
SO_EXCLUSIVEADDRUSEfix added in #18166 does not prevent this. I measured it - see "Reproduction steps: actual results".API client name and version
google-auth-oauthlib (vendored in Google Cloud SDK 548.0.0; same code path in current main of packages/google-auth-oauthlib)
Reproduction steps: code
Reproduced through
gcloud auth login --launch-browser, which callsrun_local_server().The relevant code,
packages/google-auth-oauthlib/google_auth_oauthlib/flow.py:wsgiref.simple_server.WSGIServer.address_familyissocket.AF_INET, and_ExclusiveWSGIServeroverrides onlyserver_bind, so the address family is unchanged.host="localhost"resolves to127.0.0.1and the server never listens on::1, whileredirect_uristill carries the namelocalhost.file: bind_test.py
Reproduction steps: supporting files
Any process holding the IPv6 wildcard on the port is enough. A Go program using the default listen address does this:
file: other.go
Run it first, then run
gcloud auth login --launch-browser(or anyrun_local_server()call) in another shell.Reproduction steps: actual results
With a Go service holding
[::]:8085:localhostresolution on this machine (hosts file has both entries commented out; Windows resolves it internally):gcloud printed
redirect_uri=http://localhost:8085/. After completing the consent screen the browser landed onhttp://localhost:8085/?state=...&code=...and displayed the other service's body:{"error":"Unauthorized","success":false}run_local_server()never returned.bind_test.py, run while the Go service holds
[::]:8085:On Windows a wildcard
[::]bind and a specific-address bind coexist, soSO_EXCLUSIVEADDRUSEsees no conflict. The bind succeeds, the server believes it owns the port, aReproduction steps: expected results
Either of these would be correct:
run_local_server()returns credentials, orWhat must not happen is the current outcome: a successful bind, an authorization code handed to an unrelated local process, and a silent hang.
OS & version + platform
Windows 11 Home 10.0.26200.0
Python environment
Python 3.12.10 (bundled with Google Cloud SDK)
Python dependencies
google-auth-oauthlib as vendored in Google Cloud SDK 548.0.0 (lib/third_party/google_auth_oauthlib), not a pip install.
The same code path is present in current main of packages/google-auth-oauthlib.
Additional context
Suggested fix
Any one of these removes the mismatch:
redirect_urifrom127.0.0.1. RFC 8252 section 7.3 recommends this precisely because name resolution is outside the client's control, and Google's OAuth documentation acceptshttp://127.0.0.1:<port>.localhost, listen on both127.0.0.1and::1. The measurement above shows::1is bindable in this situation.redirect_uriand verify the response came from this server. Today the failure mode is complete silence on both sides, which is what makes it expensive to diagnose.Workaround for users hitting this
Re-open the redirect URL with the host replaced by the IP literal, keeping the query string intact:
This reaches the IPv4 bind, the code is received and the login completes.
Note for the Cloud SDK
gcloud 548.0.0 vendors a copy of
google_auth_oauthlibthat predates #18166 - it has no_ExclusiveWSGIServerand noSO_EXCLUSIVEADDRUSE(zero matches inlib/third_party/google_auth_oauthlib/flow.py). Re-vendoring is worth doing on its own merits, but as measured above it would not fix this particular failure.