@@ -312,9 +312,7 @@ async def test_auth_webbrowser_fail_webbrowser(
312312 )
313313 captured = capsys .readouterr ()
314314 assert captured .out == (
315- "Initiating login request with your identity provider. A browser window "
316- "should have opened for you to complete the login. If you can't see it, "
317- "check existing browser windows, or your OS settings. Press CTRL+C to "
315+ "Initiating login request with your identity provider. Press CTRL+C to "
318316 f"abort and try again...\n Going to open: { REF_SSO_URL if disable_console_login else REF_CONSOLE_LOGIN_SSO_URL } to authenticate...\n We were unable to open a browser window for "
319317 "you, please open the url above manually then paste the URL you "
320318 "are redirected to into the terminal.\n "
@@ -381,10 +379,10 @@ async def test_auth_webbrowser_fail_webserver(_, capsys, disable_console_login):
381379 )
382380 captured = capsys .readouterr ()
383381 assert captured .out == (
384- "Initiating login request with your identity provider. A browser window "
382+ "Initiating login request with your identity provider. Press CTRL+C to "
383+ f"abort and try again...\n Going to open: { REF_SSO_URL if disable_console_login else REF_CONSOLE_LOGIN_SSO_URL } to authenticate...\n A browser window "
385384 "should have opened for you to complete the login. If you can't see it, "
386- "check existing browser windows, or your OS settings. Press CTRL+C to "
387- f"abort and try again...\n Going to open: { REF_SSO_URL if disable_console_login else REF_CONSOLE_LOGIN_SSO_URL } to authenticate...\n "
385+ "check existing browser windows, or your OS settings.\n "
388386 )
389387 assert rest ._connection .errorhandler .called # an error
390388 assert auth .assertion_content is None
@@ -873,6 +871,98 @@ async def test_auth_webbrowser_socket_reuseport_option_not_set_with_no_flag(
873871 assert auth .assertion_content == ref_token
874872
875873
874+ @pytest .mark .parametrize ("force_auth_server" , [True , False ])
875+ @patch ("secrets.token_bytes" , return_value = PROOF_KEY )
876+ async def test_auth_webbrowser_force_auth_server (_ , monkeypatch , force_auth_server ):
877+ """Authentication by WebBrowser with SNOWFLAKE_AUTH_FORCE_SERVER environment variable."""
878+ ref_token = "MOCK_TOKEN"
879+ rest = _init_rest (REF_SSO_URL , REF_PROOF_KEY , disable_console_login = True )
880+
881+ # Set environment variable
882+ if force_auth_server :
883+ monkeypatch .setenv ("SNOWFLAKE_AUTH_FORCE_SERVER" , "true" )
884+ else :
885+ monkeypatch .delenv ("SNOWFLAKE_AUTH_FORCE_SERVER" , raising = False )
886+
887+ # mock socket
888+ recv_func = recv_setup ([successful_web_callback (ref_token )])
889+ mock_socket_pkg = _init_socket ()
890+
891+ # mock webbrowser - simulate browser failing to open
892+ mock_webbrowser = MagicMock ()
893+ mock_webbrowser .open_new .return_value = False
894+
895+ # Mock select.select to return socket client
896+ with mock .patch (
897+ "select.select" , return_value = ([mock_socket_pkg .return_value ], [], [])
898+ ):
899+ auth = AuthByWebBrowser (
900+ application = APPLICATION ,
901+ webbrowser_pkg = mock_webbrowser ,
902+ socket_pkg = mock_socket_pkg ,
903+ )
904+
905+ if force_auth_server :
906+ # When SNOWFLAKE_AUTH_FORCE_SERVER is true, should continue with server flow even if browser fails
907+ with mock .patch .object (
908+ auth ._event_loop ,
909+ "sock_accept" ,
910+ side_effect = _mock_event_loop_sock_accept (),
911+ ), mock .patch .object (
912+ auth ._event_loop , "sock_sendall" , return_value = None
913+ ), mock .patch .object (
914+ auth ._event_loop ,
915+ "sock_recv" ,
916+ side_effect = _mock_event_loop_sock_recv (recv_func ),
917+ ):
918+ await auth .prepare (
919+ conn = rest ._connection ,
920+ authenticator = AUTHENTICATOR ,
921+ service_name = SERVICE_NAME ,
922+ account = ACCOUNT ,
923+ user = USER ,
924+ password = PASSWORD ,
925+ )
926+ assert not rest ._connection .errorhandler .called # no error
927+ assert auth .assertion_content == ref_token
928+ body = {"data" : {}}
929+ await auth .update_body (body )
930+ assert body ["data" ]["TOKEN" ] == ref_token
931+ assert body ["data" ]["AUTHENTICATOR" ] == EXTERNAL_BROWSER_AUTHENTICATOR
932+ assert body ["data" ]["PROOF_KEY" ] == REF_PROOF_KEY
933+ else :
934+ # When SNOWFLAKE_AUTH_FORCE_SERVER is false/unset, should fall back to manual URL input
935+ with patch (
936+ "builtins.input" ,
937+ return_value = f"http://example.com/sso?token={ ref_token } " ,
938+ ), mock .patch .object (
939+ auth ._event_loop ,
940+ "sock_accept" ,
941+ side_effect = _mock_event_loop_sock_accept (),
942+ ), mock .patch .object (
943+ auth ._event_loop , "sock_sendall" , return_value = None
944+ ), mock .patch .object (
945+ auth ._event_loop ,
946+ "sock_recv" ,
947+ side_effect = _mock_event_loop_sock_recv (recv_func ),
948+ ):
949+ await auth .prepare (
950+ conn = rest ._connection ,
951+ authenticator = AUTHENTICATOR ,
952+ service_name = SERVICE_NAME ,
953+ account = ACCOUNT ,
954+ user = USER ,
955+ password = PASSWORD ,
956+ )
957+ assert not rest ._connection .errorhandler .called # no error
958+ assert auth .assertion_content == ref_token
959+ body = {"data" : {}}
960+ await auth .update_body (body )
961+ assert body ["data" ]["TOKEN" ] == ref_token
962+ assert body ["data" ]["AUTHENTICATOR" ] == EXTERNAL_BROWSER_AUTHENTICATOR
963+ assert body ["data" ]["PROOF_KEY" ] == REF_PROOF_KEY
964+
965+
876966@pytest .mark .parametrize ("authenticator" , ["EXTERNALBROWSER" , "externalbrowser" ])
877967async def test_externalbrowser_authenticator_is_case_insensitive (
878968 monkeypatch , authenticator
0 commit comments