11from __future__ import annotations
22
33import base64
4+ import contextlib
45import hashlib
56import http .server
67import json
@@ -27,6 +28,8 @@ def __init__(self) -> None:
2728 self .theme : str | None = None
2829 self .is_complete = False
2930 self .token_error : str | None = None
31+ self .manual_code : str | None = None
32+ self .lock = threading .Lock ()
3033
3134 def create_callback_handler (self ) -> type [http .server .BaseHTTPRequestHandler ]:
3235 """Create HTTP handler for OAuth callback."""
@@ -57,10 +60,14 @@ def do_GET(self) -> None:
5760 return
5861
5962 params = urllib .parse .parse_qs (parsed .query )
60- oauth_handler .code = params .get ("code" , [None ])[0 ]
61- oauth_handler .state = params .get ("state" , [None ])[0 ]
62- oauth_handler .error = params .get ("error" , [None ])[0 ]
63- oauth_handler .theme = params .get ("theme" , ["light" ])[0 ]
63+
64+ with oauth_handler .lock :
65+ if not oauth_handler .is_complete :
66+ oauth_handler .code = params .get ("code" , [None ])[0 ]
67+ oauth_handler .state = params .get ("state" , [None ])[0 ]
68+ oauth_handler .error = params .get ("error" , [None ])[0 ]
69+ oauth_handler .theme = params .get ("theme" , ["light" ])[0 ]
70+ oauth_handler .is_complete = True
6471
6572 # Send HTML response
6673 self .send_response (200 )
@@ -70,8 +77,6 @@ def do_GET(self) -> None:
7077 html_content = self ._get_html_response ()
7178 self .wfile .write (html_content .encode ())
7279
73- oauth_handler .is_complete = True
74-
7580 def _get_html_response (self ) -> str :
7681 """Return simple HTML response."""
7782 theme = oauth_handler .theme or "light"
@@ -613,15 +618,6 @@ def serve_forever_wrapper() -> None:
613618
614619 return httpd
615620
616- def wait_for_callback (self , httpd : http .server .HTTPServer , timeout : int = 120 ) -> bool : # noqa: ARG002
617- """Wait for OAuth callback with timeout."""
618- waited = 0
619- while not self .is_complete and waited < timeout :
620- time .sleep (0.5 )
621- waited += 0.5
622-
623- return self .is_complete
624-
625621 def exchange_code_for_token (self , code : str , code_verifier : str , redirect_uri : str ) -> str | None :
626622 """Exchange authorization code for API token."""
627623 token_url = f"{ get_cfapi_base_urls ().cfwebapp_base_url } /codeflash/auth/oauth/token"
@@ -648,73 +644,26 @@ def exchange_code_for_token(self, code: str, code_verifier: str, redirect_uri: s
648644 try :
649645 error_data = e .response .json ()
650646 error_msg = error_data .get ("error_description" , error_data .get ("error" , error_msg ))
651- except Exception : # noqa: S110
652- pass
653- self .token_error = "Unauthorized" # noqa: S105
654- click .echo (f"❌ { self .token_error } " )
647+ except Exception :
648+ self .token_error = "Unauthorized" # noqa: S105
655649 return None
656650 except Exception :
657651 self .token_error = "Unauthorized" # noqa: S105
658- click .echo (f"❌ { self .token_error } " )
659652 return None
660653 else :
661654 return api_key
662655
663656
664- def _handle_local_oauth_flow (
665- oauth : OAuthHandler ,
666- httpd : http .server .HTTPServer ,
667- state : str ,
668- code_verifier : str ,
669- local_redirect_uri : str ,
670- local_auth_url : str ,
671- ) -> str | None :
672- """Handle local OAuth flow with browser and server."""
673- click .echo (f"\n 📋 If your browser didn't open, visit: { local_auth_url } \n " )
674- click .echo ("⏳ Waiting for authentication..." )
675-
676- success = oauth .wait_for_callback (httpd , timeout = 180 )
677-
678- if not success :
679- httpd .shutdown ()
680- click .echo ("❌ Authentication timed out. Please try again." )
681- return None
682-
683- if oauth .error or not oauth .code or not oauth .state or oauth .state != state :
684- httpd .shutdown ()
685- click .echo ("❌ Unauthorized." )
686- return None
687-
688- api_key = oauth .exchange_code_for_token (oauth .code , code_verifier , local_redirect_uri )
689-
690- # Wait for browser to poll status
691- time .sleep (3 )
692- httpd .shutdown ()
693-
694- return api_key
695-
696-
697- def _handle_remote_oauth_flow (code_verifier : str , remote_redirect_uri : str , remote_auth_url : str ) -> str | None :
698- """Handle remote OAuth flow with manual code entry."""
699- oauth = OAuthHandler ()
700- click .echo ("⚠️ Browser could not be opened automatically." )
701- click .echo ("\n 📋 Please visit this URL to authenticate:" )
702- click .echo (f"\n { remote_auth_url } \n " )
703-
704- # Prompt user to paste the code
705- code = click .prompt ("Paste the authorization code here" , type = str ).strip ()
706-
707- if not code :
708- click .echo ("❌ No code provided." )
709- return None
710-
711- # Exchange code for token
712- api_key = oauth .exchange_code_for_token (code , code_verifier , remote_redirect_uri )
713-
714- if api_key :
715- click .echo ("✅ Authentication successful!" )
716-
717- return api_key
657+ def _wait_for_manual_code_input (oauth : OAuthHandler ) -> None :
658+ """Thread function to wait for manual code input."""
659+ try :
660+ code = input ()
661+ with oauth .lock :
662+ if not oauth .is_complete :
663+ oauth .manual_code = code .strip ()
664+ oauth .is_complete = True
665+ except Exception : # noqa: S110
666+ pass
718667
719668
720669def perform_oauth_signin () -> str | None :
@@ -733,37 +682,69 @@ def perform_oauth_signin() -> str | None:
733682 local_redirect_uri = f"http://localhost:{ port } /callback"
734683 remote_redirect_uri = f"{ get_cfapi_base_urls ().cfwebapp_base_url } /codeflash/auth/callback"
735684
736- local_auth_url = (
737- f" { get_cfapi_base_urls (). cfwebapp_base_url } /codeflash/auth?"
685+ base_url = f" { get_cfapi_base_urls (). cfwebapp_base_url } /codeflash/auth"
686+ params = (
738687 f"response_type=code"
739688 f"&client_id=cf-cli-app"
740- f"&redirect_uri={ urllib .parse .quote (local_redirect_uri )} "
741689 f"&code_challenge={ code_challenge } "
742690 f"&code_challenge_method=sha256"
743691 f"&state={ state } "
744692 )
693+ local_auth_url = f"{ base_url } ?{ params } &redirect_uri={ urllib .parse .quote (local_redirect_uri )} "
694+ remote_auth_url = f"{ base_url } ?{ params } &redirect_uri={ urllib .parse .quote (remote_redirect_uri )} "
745695
746- remote_auth_url = (
747- f"{ get_cfapi_base_urls ().cfwebapp_base_url } /codeflash/auth?"
748- f"response_type=code"
749- f"&client_id=cf-cli-app"
750- f"&redirect_uri={ urllib .parse .quote (remote_redirect_uri )} "
751- f"&code_challenge={ code_challenge } "
752- f"&code_challenge_method=sha256"
753- f"&state={ state } "
754- )
696+ # Start local server
697+ try :
698+ httpd = oauth .start_local_server (port )
699+ except Exception :
700+ click .echo ("❌ Failed to start local server." )
701+ return None
755702
756703 # Try to open browser
757704 click .echo ("🌐 Opening browser to sign in to CodeFlash…" )
705+ with contextlib .suppress (Exception ):
706+ webbrowser .open (local_auth_url )
758707
759- try :
760- # Start local server first
761- httpd = oauth .start_local_server (port )
762- browser_opened = webbrowser .open (local_auth_url )
763- except Exception :
764- browser_opened = False
708+ # Show remote URL and start input thread
709+ click .echo ("\n 📋 If browser didn't open, visit this URL:" )
710+ click .echo (f"\n { remote_auth_url } \n " )
711+ click .echo ("Paste code here if prompted > " , nl = False )
712+
713+ # Start thread to wait for manual input
714+ input_thread = threading .Thread (target = _wait_for_manual_code_input , args = (oauth ,))
715+ input_thread .daemon = True
716+ input_thread .start ()
717+
718+ waited = 0
719+ while not oauth .is_complete and waited < 180 :
720+ time .sleep (0.5 )
721+ waited += 0.5
722+
723+ if not oauth .is_complete :
724+ httpd .shutdown ()
725+ click .echo ("\n ❌ Authentication timed out." )
726+ return None
765727
766- if browser_opened :
767- return _handle_local_oauth_flow (oauth , httpd , state , code_verifier , local_redirect_uri , local_auth_url )
728+ # Check which method completed
729+ api_key = None
730+
731+ if oauth .manual_code :
732+ # Manual code was entered
733+ api_key = oauth .exchange_code_for_token (oauth .manual_code , code_verifier , remote_redirect_uri )
734+ elif oauth .code :
735+ # Browser callback received
736+ if oauth .error or not oauth .state or oauth .state != state :
737+ httpd .shutdown ()
738+ click .echo ("\n ❌ Unauthorized." )
739+ return None
740+
741+ api_key = oauth .exchange_code_for_token (oauth .code , code_verifier , local_redirect_uri )
742+
743+ # Cleanup
744+ time .sleep (3 )
768745 httpd .shutdown ()
769- return _handle_remote_oauth_flow (code_verifier , remote_redirect_uri , remote_auth_url )
746+
747+ if not api_key :
748+ click .echo ("❌ Authentication failed." )
749+
750+ return api_key
0 commit comments