Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
- Fixed `get_results_from_sfqid` when using `DictCursor` and executing multiple statements at once
- Added the `oauth_credentials_in_body` parameter supporting an option to send the oauth client credentials in the request body
- Added support for intermediate certificates as roots when they are stored in the trust store
- Added the `SNOWFLAKE_AUTH_FORCE_SERVER` environment variable to force the use of the local-listening server when using the `externalbrowser` auth method
- This allows headless environments (like Docker or Airflow) running locally to auth via a browser URL
- Bumped up vendored `urllib3` to `2.5.0` and `requests` to `v2.32.5`

- v3.17.3(September 02,2025)
Expand Down
24 changes: 16 additions & 8 deletions src/snowflake/connector/auth/webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,26 @@ def prepare(
return

print(
"Initiating login request with your identity provider. 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. Press CTRL+C to abort and try again..."
"Initiating login request with your identity provider. Press CTRL+C to abort and try again..."
)

logger.debug("step 2: open a browser")
print(f"Going to open: {sso_url} to authenticate...")
if not self._webbrowser.open_new(sso_url):
browser_opened = self._webbrowser.open_new(sso_url)
if browser_opened:
print(
"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."
)

if (
browser_opened
or os.getenv("SNOWFLAKE_AUTH_FORCE_SERVER", "False").lower() == "true"
):
logger.debug("step 3: accept SAML token")
self._receive_saml_token(conn, socket_connection)
else:
print(
"We were unable to open a browser window for you, "
"please open the url above manually then paste the "
Expand All @@ -195,9 +206,6 @@ def prepare(
},
)
return
else:
logger.debug("step 3: accept SAML token")
self._receive_saml_token(conn, socket_connection)
finally:
socket_connection.close()

Expand Down
79 changes: 73 additions & 6 deletions test/unit/test_auth_webbrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,7 @@ def test_auth_webbrowser_fail_webbrowser(
)
captured = capsys.readouterr()
assert captured.out == (
"Initiating login request with your identity provider. 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. Press CTRL+C to "
"Initiating login request with your identity provider. Press CTRL+C to "
f"abort and try again...\nGoing to open: {REF_SSO_URL if disable_console_login else REF_CONSOLE_LOGIN_SSO_URL} to authenticate...\nWe were unable to open a browser window for "
"you, please open the url above manually then paste the URL you "
"are redirected to into the terminal.\n"
Expand Down Expand Up @@ -337,10 +335,10 @@ def test_auth_webbrowser_fail_webserver(_, capsys, disable_console_login):
)
captured = capsys.readouterr()
assert captured.out == (
"Initiating login request with your identity provider. A browser window "
"Initiating login request with your identity provider. Press CTRL+C to "
f"abort and try again...\nGoing to open: {REF_SSO_URL if disable_console_login else REF_CONSOLE_LOGIN_SSO_URL} to authenticate...\nA 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. Press CTRL+C to "
f"abort and try again...\nGoing to open: {REF_SSO_URL if disable_console_login else REF_CONSOLE_LOGIN_SSO_URL} to authenticate...\n"
"check existing browser windows, or your OS settings.\n"
)
assert rest._connection.errorhandler.called # an error
assert auth.assertion_content is None
Expand Down Expand Up @@ -752,6 +750,75 @@ def test_auth_webbrowser_socket_reuseport_option_not_set_with_no_flag(monkeypatc
assert auth.assertion_content == ref_token


@pytest.mark.parametrize("force_auth_server", [True, False])
@patch("secrets.token_bytes", return_value=PROOF_KEY)
def test_auth_webbrowser_force_auth_server(_, monkeypatch, force_auth_server):
"""Authentication by WebBrowser with SNOWFLAKE_AUTH_FORCE_SERVER environment variable."""
ref_token = "MOCK_TOKEN"
rest = _init_rest(REF_SSO_URL, REF_PROOF_KEY, disable_console_login=True)

# Set environment variable
if force_auth_server:
monkeypatch.setenv("SNOWFLAKE_AUTH_FORCE_SERVER", "true")
else:
monkeypatch.delenv("SNOWFLAKE_AUTH_FORCE_SERVER", raising=False)

# mock socket
mock_socket_pkg = _init_socket(
recv_side_effect_func=recv_setup([successful_web_callback(ref_token)])
)

# mock webbrowser - simulate browser failing to open
mock_webbrowser = MagicMock()
mock_webbrowser.open_new.return_value = False

# Mock select.select to return socket client
with mock.patch(
"select.select", return_value=([mock_socket_pkg.return_value], [], [])
):
auth = AuthByWebBrowser(
application=APPLICATION,
webbrowser_pkg=mock_webbrowser,
socket_pkg=mock_socket_pkg,
)

if force_auth_server:
# When SNOWFLAKE_AUTH_FORCE_SERVER is true, should continue with server flow even if browser fails
auth.prepare(
conn=rest._connection,
authenticator=AUTHENTICATOR,
service_name=SERVICE_NAME,
account=ACCOUNT,
user=USER,
password=PASSWORD,
)
assert not rest._connection.errorhandler.called # no error
assert auth.assertion_content == ref_token
body = {"data": {}}
auth.update_body(body)
assert body["data"]["TOKEN"] == ref_token
assert body["data"]["AUTHENTICATOR"] == EXTERNAL_BROWSER_AUTHENTICATOR
assert body["data"]["PROOF_KEY"] == REF_PROOF_KEY
else:
# When SNOWFLAKE_AUTH_FORCE_SERVER is false/unset, should fall back to manual URL input
with patch("builtins.input", return_value=f"http://example.com/sso?token={ref_token}"):
auth.prepare(
conn=rest._connection,
authenticator=AUTHENTICATOR,
service_name=SERVICE_NAME,
account=ACCOUNT,
user=USER,
password=PASSWORD,
)
assert not rest._connection.errorhandler.called # no error
assert auth.assertion_content == ref_token
body = {"data": {}}
auth.update_body(body)
assert body["data"]["TOKEN"] == ref_token
assert body["data"]["AUTHENTICATOR"] == EXTERNAL_BROWSER_AUTHENTICATOR
assert body["data"]["PROOF_KEY"] == REF_PROOF_KEY


@pytest.mark.parametrize("authenticator", ["EXTERNALBROWSER", "externalbrowser"])
def test_externalbrowser_authenticator_is_case_insensitive(monkeypatch, authenticator):
"""Test that external browser authenticator is case insensitive."""
Expand Down