Skip to content

Commit 54906d6

Browse files
Environment variable to force browser-based auth (#2538)
Co-authored-by: Patryk Czajka <[email protected]>
1 parent f1ea979 commit 54906d6

File tree

3 files changed

+94
-14
lines changed

3 files changed

+94
-14
lines changed

DESCRIPTION.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Source code is also available at: https://github.com/snowflakedb/snowflake-conne
88

99
# Release Notes
1010
- v4.1.0(TBD)
11+
- Added the `SNOWFLAKE_AUTH_FORCE_SERVER` environment variable to force the use of the local-listening server when using the `externalbrowser` auth method
12+
- This allows headless environments (like Docker or Airflow) running locally to auth via a browser URL
1113

1214
- v4.0.0(October 09,2025)
1315
- Added support for checking certificates revocation using revocation lists (CRLs)

src/snowflake/connector/auth/webbrowser.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,26 @@ def prepare(
165165
return
166166

167167
print(
168-
"Initiating login request with your identity provider. A "
169-
"browser window should have opened for you to complete the "
170-
"login. If you can't see it, check existing browser windows, "
171-
"or your OS settings. Press CTRL+C to abort and try again..."
168+
"Initiating login request with your identity provider. Press CTRL+C to abort and try again..."
172169
)
173170

174171
logger.debug("step 2: open a browser")
175172
print(f"Going to open: {sso_url} to authenticate...")
176-
if not self._webbrowser.open_new(sso_url):
173+
browser_opened = self._webbrowser.open_new(sso_url)
174+
if browser_opened:
175+
print(
176+
"A browser window should have opened for you to complete the "
177+
"login. If you can't see it, check existing browser windows, "
178+
"or your OS settings."
179+
)
180+
181+
if (
182+
browser_opened
183+
or os.getenv("SNOWFLAKE_AUTH_FORCE_SERVER", "False").lower() == "true"
184+
):
185+
logger.debug("step 3: accept SAML token")
186+
self._receive_saml_token(conn, socket_connection)
187+
else:
177188
print(
178189
"We were unable to open a browser window for you, "
179190
"please open the url above manually then paste the "
@@ -195,9 +206,6 @@ def prepare(
195206
},
196207
)
197208
return
198-
else:
199-
logger.debug("step 3: accept SAML token")
200-
self._receive_saml_token(conn, socket_connection)
201209
finally:
202210
socket_connection.close()
203211

test/unit/test_auth_webbrowser.py

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,7 @@ def test_auth_webbrowser_fail_webbrowser(
278278
)
279279
captured = capsys.readouterr()
280280
assert captured.out == (
281-
"Initiating login request with your identity provider. A browser window "
282-
"should have opened for you to complete the login. If you can't see it, "
283-
"check existing browser windows, or your OS settings. Press CTRL+C to "
281+
"Initiating login request with your identity provider. Press CTRL+C to "
284282
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 "
285283
"you, please open the url above manually then paste the URL you "
286284
"are redirected to into the terminal.\n"
@@ -337,10 +335,10 @@ def test_auth_webbrowser_fail_webserver(_, capsys, disable_console_login):
337335
)
338336
captured = capsys.readouterr()
339337
assert captured.out == (
340-
"Initiating login request with your identity provider. A browser window "
338+
"Initiating login request with your identity provider. Press CTRL+C to "
339+
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 "
341340
"should have opened for you to complete the login. If you can't see it, "
342-
"check existing browser windows, or your OS settings. Press CTRL+C to "
343-
f"abort and try again...\nGoing to open: {REF_SSO_URL if disable_console_login else REF_CONSOLE_LOGIN_SSO_URL} to authenticate...\n"
341+
"check existing browser windows, or your OS settings.\n"
344342
)
345343
assert rest._connection.errorhandler.called # an error
346344
assert auth.assertion_content is None
@@ -753,6 +751,78 @@ def test_auth_webbrowser_socket_reuseport_option_not_set_with_no_flag(monkeypatc
753751
assert auth.assertion_content == ref_token
754752

755753

754+
@pytest.mark.parametrize("force_auth_server", [True, False])
755+
@patch("secrets.token_bytes", return_value=PROOF_KEY)
756+
def test_auth_webbrowser_force_auth_server(_, monkeypatch, force_auth_server):
757+
"""Authentication by WebBrowser with SNOWFLAKE_AUTH_FORCE_SERVER environment variable."""
758+
ref_token = "MOCK_TOKEN"
759+
rest = _init_rest(REF_SSO_URL, REF_PROOF_KEY, disable_console_login=True)
760+
761+
# Set environment variable
762+
if force_auth_server:
763+
monkeypatch.setenv("SNOWFLAKE_AUTH_FORCE_SERVER", "true")
764+
else:
765+
monkeypatch.delenv("SNOWFLAKE_AUTH_FORCE_SERVER", raising=False)
766+
767+
# mock socket
768+
mock_socket_pkg = _init_socket(
769+
recv_side_effect_func=recv_setup([successful_web_callback(ref_token)])
770+
)
771+
772+
# mock webbrowser - simulate browser failing to open
773+
mock_webbrowser = MagicMock()
774+
mock_webbrowser.open_new.return_value = False
775+
776+
# Mock select.select to return socket client
777+
with mock.patch(
778+
"select.select", return_value=([mock_socket_pkg.return_value], [], [])
779+
):
780+
auth = AuthByWebBrowser(
781+
application=APPLICATION,
782+
webbrowser_pkg=mock_webbrowser,
783+
socket_pkg=mock_socket_pkg,
784+
)
785+
786+
if force_auth_server:
787+
# When SNOWFLAKE_AUTH_FORCE_SERVER is true, should continue with server flow even if browser fails
788+
auth.prepare(
789+
conn=rest._connection,
790+
authenticator=AUTHENTICATOR,
791+
service_name=SERVICE_NAME,
792+
account=ACCOUNT,
793+
user=USER,
794+
password=PASSWORD,
795+
)
796+
assert not rest._connection.errorhandler.called # no error
797+
assert auth.assertion_content == ref_token
798+
body = {"data": {}}
799+
auth.update_body(body)
800+
assert body["data"]["TOKEN"] == ref_token
801+
assert body["data"]["AUTHENTICATOR"] == EXTERNAL_BROWSER_AUTHENTICATOR
802+
assert body["data"]["PROOF_KEY"] == REF_PROOF_KEY
803+
else:
804+
# When SNOWFLAKE_AUTH_FORCE_SERVER is false/unset, should fall back to manual URL input
805+
with patch(
806+
"builtins.input",
807+
return_value=f"http://example.com/sso?token={ref_token}",
808+
):
809+
auth.prepare(
810+
conn=rest._connection,
811+
authenticator=AUTHENTICATOR,
812+
service_name=SERVICE_NAME,
813+
account=ACCOUNT,
814+
user=USER,
815+
password=PASSWORD,
816+
)
817+
assert not rest._connection.errorhandler.called # no error
818+
assert auth.assertion_content == ref_token
819+
body = {"data": {}}
820+
auth.update_body(body)
821+
assert body["data"]["TOKEN"] == ref_token
822+
assert body["data"]["AUTHENTICATOR"] == EXTERNAL_BROWSER_AUTHENTICATOR
823+
assert body["data"]["PROOF_KEY"] == REF_PROOF_KEY
824+
825+
756826
@pytest.mark.parametrize("authenticator", ["EXTERNALBROWSER", "externalbrowser"])
757827
def test_externalbrowser_authenticator_is_case_insensitive(monkeypatch, authenticator):
758828
"""Test that external browser authenticator is case insensitive."""

0 commit comments

Comments
 (0)