Repository navigation
SNOW-4218366: externalbrowser authentication waits forever if the browser login is never completed #3045
Description
Python version
Reproduced on both of these:
- Python 3.14.7 (the Python bundled with Snowflake CLI 3.28.0 from Homebrew)
- Python 3.12.12, with the connector installed from
main
Operating system and processor architecture
macOS-26.6.2-arm64-arm-64bit-Mach-O
Installed packages
Reproduced with both of these:
snowflake-cli==3.28.0
snowflake-connector-python==4.7.5
snowflake-snowpark-python==1.53.0
snowflake.core==1.10.0
snowflake-connector-python @ git+https://github.com/snowflakedb/snowflake-connector-python@b4a555400dd8af2bb37069ce7210b5e7db6cfcc5 # main, 4.8.0
What did you do?
Connected with authenticator="externalbrowser" when no cached ID token was available, and did not complete the login in the browser. In my case the connection was made by snow sql (Snowflake CLI) run from an unattended script, so nobody was there to finish the login.
This script reproduces it offline, without an account or a real browser. It stubs the SSO URL lookup, and makes "opening the browser" succeed without opening anything, as if the user never finishes the login:
Reproduction script (runs offline, no account needed)
import sys
import threading
import time
import webbrowser
import snowflake.connector
from snowflake.connector.auth.webbrowser import AuthByWebBrowser
TIMEOUT = 5 # seconds, for both login_timeout and external_browser_timeout
WAIT = 30 # seconds to wait before declaring a hang
AuthByWebBrowser._get_sso_url = lambda self, *args, **kwargs: "https://sso.example.com/login"
webbrowser.open_new = lambda url, *args, **kwargs: True
outcome = {}
def attempt_connect():
try:
snowflake.connector.connect(
account="testaccount",
user="testuser",
authenticator="externalbrowser",
login_timeout=TIMEOUT,
external_browser_timeout=TIMEOUT,
client_store_temporary_credential=False,
)
outcome["result"] = "connected (unexpected)"
except Exception as e:
outcome["result"] = f"{type(e).__name__}: {e}"
start = time.monotonic()
thread = threading.Thread(target=attempt_connect, daemon=True)
thread.start()
thread.join(WAIT)
elapsed = time.monotonic() - start
if thread.is_alive():
print(f"BUG: connect() still waiting after {elapsed:.0f}s, despite login_timeout={TIMEOUT} and external_browser_timeout={TIMEOUT}")
sys.exit(1)
print(f"OK: connect() returned after {elapsed:.1f}s: {outcome['result']}")Output, the same on 4.7.5 and main:
Initiating login request with your identity provider. Press CTRL+C to abort and try again...
Going to open: https://sso.example.com/login to authenticate...
A browser window should have opened for you to complete the login. If you can't see it, check existing browser windows, or your OS settings.
BUG: connect() still waiting after 30s, despite login_timeout=5 and external_browser_timeout=5
What did you expect to see?
The connection attempt to fail after a timeout, with an error saying the browser login wasn't completed.
Instead, it waits forever. AuthByWebBrowser._receive_saml_token waits for the callback with a select.select() call that has no timeout (webbrowser.py L244-L246):
read_sockets, _write_sockets, _exception_sockets = select.select(
[socket_connection], [], []
)Neither login_timeout nor external_browser_timeout limits this wait:
login_timeoutis passed toAuthByWebBrowser(connection.py L1611-L1618), but it governs the login requests, not the wait for the browser callback.external_browser_timeout(default 120s, connection.py L266) limits the equivalent wait in the OAuth authorization-code flow (oauth_code.py L346-L348), but it isn't used byexternalbrowser.
Suggested fix: pass external_browser_timeout to AuthByWebBrowser, use it as the select.select() timeout, and fail the authentication with a clear error when it expires. That would match the OAuth flow's behaviour and use the same setting. The async AuthByWebBrowser (aio/auth/_webbrowser.py) has the same unbounded select.select() and needs the same change.
I've tried this locally against main. With it, the script above prints:
OK: connect() returned after 5.1s: DatabaseError: 251022 (08001): Failed to connect to DB: testaccount.snowflakecomputing.com:443, Browser authentication was not completed within 5 seconds (external_browser_timeout).
The existing sync and async test_auth_webbrowser unit tests still pass. One thing to decide: external_browser_timeout defaults to 120s, so this would change the default from waiting forever to failing after 2 minutes. That seems right to me, but you might prefer a different default for externalbrowser. I'm happy to open a PR.
Can you set logging to DEBUG and collect the logs?
Not captured. The script above reproduces the problem without an account, so the logs shouldn't be needed. From the code, the last DEBUG message before the wait should be "step 3: accept SAML token".
Activity
[-]`externalbrowser` authentication waits forever if the browser login is never completed[/-][+]SNOW-4218366: `externalbrowser` authentication waits forever if the browser login is never completed[/+]sfc-gh-dszmolka commentedon Oct 1, 2026
hi @mdub and thanks for filing this issue. for the record, this already is fixed (== was never an issue) in v5 but i guess you need it work in v4 too ;)
anyhow, the team will take a look.
balkashynov commentedon Oct 14, 2026
Hi maintainers, I investigated the async path left out of #3046 and reproduced a few remaining cases on current main (
e58434c) and that PR’s head (0078cb6), using Python 3.12.11 on macOS arm64.The tests exercised
AuthByWebBrowser.prepare()with real localhost sockets, stubbing the SSO lookup, browser launch, and failure reporting. No Snowflake account was needed.The main async issue is that
_receive_saml_token()calls blockingselect.select()inside the coroutine:asyncio.wait_for(..., timeout=1.5)cannot fire. An independent watchdog stopped the test after 2.5 seconds.select(), again preventing cancellation.Separately, #3046’s synchronous path still blocks in
recv()when a client connects without sending data, exceeding the configured browser timeout. Both paths also discard a valid POST when its headers and body arrive in separate writes. Complete GET and POST callbacks succeeded.Would you welcome a focused v4 async fix that forwards
external_browser_timeout, uses nonblocking accept/read operations under one overall deadline, and ensures cancellation closes the sockets? I’d include regression tests for event-loop responsiveness, timeout, cancellation, and successful callbacks.I’d coordinate the synchronous read-timeout case with #3046’s author and keep fragmented-request handling separate. I understand v5 already addresses the original issue, so I wanted to confirm interest in this v4 work before proceeding.