Skip to content

SNOW-4218366: externalbrowser authentication waits forever if the browser login is never completed #3045

Open

Description

@mdub

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_timeout is passed to AuthByWebBrowser (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 by externalbrowser.

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

  1. changed the title [-]`externalbrowser` authentication waits forever if the browser login is never completed[/-] [+]SNOW-4218366: `externalbrowser` authentication waits forever if the browser login is never completed[/+] on Oct 1, 2026
  2. sfc-gh-dszmolka commented on Oct 1, 2026

    @sfc-gh-dszmolka
    Contributor

    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.

  3. balkashynov commented on Oct 14, 2026

    @balkashynov

    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 blocking select.select() inside the coroutine:

    • With no browser callback, the event loop stops progressing. A heartbeat coroutine never runs, and an outer asyncio.wait_for(..., timeout=1.5) cannot fire. An independent watchdog stopped the test after 2.5 seconds.
    • If a client connects but sends no data, the existing socket-read timeout fires, but execution then returns to blocking 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugstatus-triage_doneInitial triage done, will be further handled by the driver team

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions