Skip to content

Commit 1a2af3d

Browse files
HeroCCsfc-gh-turbaszek
authored andcommitted
Environment variable to force browser-based auth (#2538)
Co-authored-by: Patryk Czajka <[email protected]> (cherry picked from commit 54906d6)
1 parent b3e598f commit 1a2af3d

File tree

2 files changed

+92
-14
lines changed

2 files changed

+92
-14
lines changed

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
@@ -277,9 +277,7 @@ def test_auth_webbrowser_fail_webbrowser(
277277
)
278278
captured = capsys.readouterr()
279279
assert captured.out == (
280-
"Initiating login request with your identity provider. A browser window "
281-
"should have opened for you to complete the login. If you can't see it, "
282-
"check existing browser windows, or your OS settings. Press CTRL+C to "
280+
"Initiating login request with your identity provider. Press CTRL+C to "
283281
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 "
284282
"you, please open the url above manually then paste the URL you "
285283
"are redirected to into the terminal.\n"
@@ -336,10 +334,10 @@ def test_auth_webbrowser_fail_webserver(_, capsys, disable_console_login):
336334
)
337335
captured = capsys.readouterr()
338336
assert captured.out == (
339-
"Initiating login request with your identity provider. A browser window "
337+
"Initiating login request with your identity provider. Press CTRL+C to "
338+
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 "
340339
"should have opened for you to complete the login. If you can't see it, "
341-
"check existing browser windows, or your OS settings. Press CTRL+C to "
342-
f"abort and try again...\nGoing to open: {REF_SSO_URL if disable_console_login else REF_CONSOLE_LOGIN_SSO_URL} to authenticate...\n"
340+
"check existing browser windows, or your OS settings.\n"
343341
)
344342
assert rest._connection.errorhandler.called # an error
345343
assert auth.assertion_content is None
@@ -751,6 +749,78 @@ def test_auth_webbrowser_socket_reuseport_option_not_set_with_no_flag(monkeypatc
751749
assert auth.assertion_content == ref_token
752750

753751

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

0 commit comments

Comments
 (0)