11import http .server
2+ import json
23import logging
34import socket
45import sys
1314
1415from safety .auth .constants import AUTH_SERVER_URL , CLI_AUTH_SUCCESS , CLI_LOGOUT_SUCCESS , HOST
1516from safety .auth .main import save_auth_config
17+ from authlib .integrations .base_client .errors import OAuthError
18+ from rich .prompt import Prompt
1619
1720LOG = logging .getLogger (__name__ )
1821
@@ -33,40 +36,49 @@ def find_available_port():
3336
3437 return None
3538
39+ def auth_process (code : str , state : str , initial_state : str , code_verifier , client ):
40+ err = None
41+
42+ if initial_state is None or initial_state != state :
43+ err = "The state parameter value provided does not match the expected " \
44+ "value. The state parameter is used to protect against Cross-Site " \
45+ "Request Forgery (CSRF) attacks. For security reasons, the " \
46+ "authorization process cannot proceed with an invalid state " \
47+ "parameter value. Please try again, ensuring that the state " \
48+ "parameter value provided in the authorization request matches " \
49+ "the value returned in the callback."
50+
51+ if err :
52+ click .secho (f'Error: { err } ' , fg = 'red' )
53+ sys .exit (1 )
54+
55+ try :
56+ tokens = client .fetch_token (url = f'{ AUTH_SERVER_URL } /oauth/token' ,
57+ code_verifier = code_verifier ,
58+ client_id = client .client_id ,
59+ grant_type = 'authorization_code' , code = code )
60+
61+ save_auth_config (access_token = tokens ['access_token' ],
62+ id_token = tokens ['id_token' ],
63+ refresh_token = tokens ['refresh_token' ])
64+ return client .fetch_user_info ()
65+
66+ except Exception as e :
67+ LOG .exception (e )
68+ sys .exit (1 )
3669
3770class CallbackHandler (http .server .BaseHTTPRequestHandler ):
3871 def auth (self , code : str , state : str , err , error_description ):
3972 initial_state = self .server .initial_state
4073 ctx = self .server .ctx
4174
42- if initial_state is None or initial_state != state :
43- err = "The state parameter value provided does not match the expected" \
44- "value. The state parameter is used to protect against Cross-Site " \
45- "Request Forgery (CSRF) attacks. For security reasons, the " \
46- "authorization process cannot proceed with an invalid state " \
47- "parameter value. Please try again, ensuring that the state " \
48- "parameter value provided in the authorization request matches " \
49- "the value returned in the callback."
50-
51- if err :
52- click .secho (f'Error: { err } ' , fg = 'red' )
53- sys .exit (1 )
75+ result = auth_process (code = code ,
76+ state = state ,
77+ initial_state = initial_state ,
78+ code_verifier = ctx .obj .auth .code_verifier ,
79+ client = ctx .obj .auth .client )
5480
55- try :
56- tokens = ctx .obj .auth .client .fetch_token (url = f'{ AUTH_SERVER_URL } /oauth/token' ,
57- code_verifier = ctx .obj .auth .code_verifier ,
58- client_id = ctx .obj .auth .client .client_id ,
59- grant_type = 'authorization_code' , code = code )
60-
61- save_auth_config (access_token = tokens ['access_token' ],
62- id_token = tokens ['id_token' ],
63- refresh_token = tokens ['refresh_token' ])
64- self .server .callback = ctx .obj .auth .client .fetch_user_info ()
65-
66- except Exception as e :
67- LOG .exception (e )
68- sys .exit (1 )
69-
81+ self .server .callback = result
7082 self .do_redirect (location = CLI_AUTH_SUCCESS , params = {})
7183
7284 def logout (self ):
@@ -132,27 +144,52 @@ def handle_timeout(self) -> None:
132144 sys .exit (1 )
133145
134146 try :
135- server = ThreadedHTTPServer ((HOST , PORT ), CallbackHandler )
136- server .initial_state = kwargs .get ("initial_state" , None )
137- server .timeout = kwargs .get ("timeout" , 600 )
138- # timeout = kwargs.get("timeout", None)
139- # timeout = float(timeout) if timeout else None
140- server .ctx = kwargs .get ("ctx" , None )
141- server_thread = threading .Thread (target = server .handle_request )
142- server_thread .start ()
143-
144- target = f"{ uri } &port={ PORT } "
145- console .print (f"If the browser does not automatically open in 5 seconds, " \
146- "copy and paste this url into your browser: " \
147- f"[link={ target } ]{ target } [/link]" )
148- click .echo ()
149-
150- wait_msg = "waiting for browser authentication"
151-
152- with console .status (wait_msg , spinner = "bouncingBar" ):
153- time .sleep (2 )
154- click .launch (target )
155- server_thread .join ()
147+ headless = kwargs .get ("headless" , False )
148+ initial_state = kwargs .get ("initial_state" , None )
149+ ctx = kwargs .get ("ctx" , None )
150+
151+ message = "Copy and paste this url into your browser:"
152+
153+
154+ if not headless :
155+ server = ThreadedHTTPServer ((HOST , PORT ), CallbackHandler )
156+ server .initial_state = initial_state
157+ server .timeout = kwargs .get ("timeout" , 600 )
158+ server .ctx = ctx
159+ server_thread = threading .Thread (target = server .handle_request )
160+ server_thread .start ()
161+ message = f"If the browser does not automatically open in 5 seconds, " \
162+ "copy and paste this url into your browser:"
163+
164+ target = uri if headless else f"{ uri } &port={ PORT } "
165+ console .print (f"{ message } [link={ target } ]{ target } [/link]" )
166+ console .print ()
167+
168+ if headless :
169+
170+ exchange_data = None
171+ while not exchange_data :
172+ auth_code_text = Prompt .ask ("Paste the response here" , default = None , console = console )
173+ try :
174+ exchange_data = json .loads (auth_code_text )
175+ state = exchange_data ["state" ]
176+ code = exchange_data ["code" ]
177+ except Exception as e :
178+ code = state = None
179+
180+ return auth_process (code = code ,
181+ state = state ,
182+ initial_state = initial_state ,
183+ code_verifier = ctx .obj .auth .code_verifier ,
184+ client = ctx .obj .auth .client )
185+ else :
186+
187+ wait_msg = "waiting for browser authentication"
188+
189+ with console .status (wait_msg , spinner = "bouncingBar" ):
190+ time .sleep (2 )
191+ click .launch (target )
192+ server_thread .join ()
156193
157194 except OSError as e :
158195 if e .errno == socket .errno .EADDRINUSE :
0 commit comments