11import asyncio
2- import json
32import os
43import webbrowser
5- from socket import AF_INET , SOCK_STREAM , error , socket
64from typing import Optional
7- from urllib .parse import urlparse
85
96from uipath ._cli ._auth ._auth_server import HTTPServer
10- from uipath ._cli ._auth ._client_credentials import ClientCredentialsService
7+ from uipath ._cli ._auth ._models import TokenData
118from uipath ._cli ._auth ._oidc_utils import OidcUtils
12- from uipath ._cli ._auth ._portal_service import (
13- PortalService ,
14- get_tenant_id ,
15- select_tenant ,
16- )
17- from uipath ._cli ._auth ._url_utils import set_force_flag
18- from uipath ._cli ._auth ._utils import update_auth_file , update_env_file
9+ from uipath ._cli ._auth ._portal_service import PortalService
10+ from uipath ._cli ._auth ._url_utils import extract_org_tenant , resolve_domain
11+ from uipath ._cli ._auth ._utils import update_env_file
1912from uipath ._cli ._utils ._console import ConsoleLogger
13+ from uipath ._services import ExternalApplicationService
2014
2115
2216class AuthService :
@@ -25,35 +19,20 @@ def __init__(
2519 environment : str ,
2620 * ,
2721 force : bool ,
28- client_id : Optional [str ],
29- client_secret : Optional [str ],
30- base_url : Optional [str ],
31- tenant : Optional [str ],
32- scope : Optional [str ],
22+ client_id : Optional [str ] = None ,
23+ client_secret : Optional [str ] = None ,
24+ base_url : Optional [str ] = None ,
25+ tenant : Optional [str ] = None ,
26+ scope : Optional [str ] = None ,
3327 ):
3428 self ._force = force
3529 self ._console = ConsoleLogger ()
36- self ._domain = self ._get_domain (environment )
3730 self ._client_id = client_id
3831 self ._client_secret = client_secret
3932 self ._base_url = base_url
4033 self ._tenant = tenant
34+ self ._domain = resolve_domain (self ._base_url , environment , self ._force )
4135 self ._scope = scope
42- set_force_flag (self ._force )
43-
44- def _get_domain (self , environment : str ) -> str :
45- # only search env var if not force authentication
46- if not self ._force :
47- uipath_url = os .getenv ("UIPATH_URL" )
48- if uipath_url and environment == "cloud" : # "cloud" is the default
49- parsed_url = urlparse (uipath_url )
50- if parsed_url .scheme and parsed_url .netloc :
51- environment = f"{ parsed_url .scheme } ://{ parsed_url .netloc } "
52- else :
53- self ._console .error (
54- f"Malformed UIPATH_URL: '{ uipath_url } '. Please ensure it includes both scheme and netloc (e.g., 'https://cloud.uipath.com')."
55- )
56- return environment
5736
5837 def authenticate (self ) -> None :
5938 if self ._client_id and self ._client_secret :
@@ -62,103 +41,103 @@ def authenticate(self) -> None:
6241
6342 self ._authenticate_authorization_code ()
6443
65- def _authenticate_client_credentials (self ) -> None :
44+ def _authenticate_client_credentials (self ):
6645 if not self ._base_url :
6746 self ._console .error (
6847 "--base-url is required when using client credentials authentication."
6948 )
7049 return
71- self ._console .hint ("Using client credentials authentication." )
72- credentials_service = ClientCredentialsService (self ._base_url )
73- credentials_service .authenticate (
50+
51+ organization_name , tenant_name = extract_org_tenant (self ._base_url )
52+ if not (organization_name and tenant_name ):
53+ self ._console .error (
54+ "--base-url must include both organization and tenant, "
55+ "e.g., 'https://cloud.uipath.com/{organization}/{tenant}'."
56+ )
57+ return
58+
59+ app_service = ExternalApplicationService (self ._domain )
60+ token_data = app_service .get_token_data (
7461 self ._client_id , # type: ignore
7562 self ._client_secret , # type: ignore
7663 self ._scope ,
7764 )
7865
66+ self ._tenant = tenant_name
67+ with PortalService (
68+ self ._domain , access_token = token_data ["access_token" ]
69+ ) as portal_service :
70+ self ._finalize_auth (portal_service , token_data )
71+
7972 def _authenticate_authorization_code (self ) -> None :
8073 with PortalService (self ._domain ) as portal_service :
81- if not self ._force :
82- # use existing env vars
83- if (
84- os .getenv ("UIPATH_URL" )
85- and os .getenv ("UIPATH_TENANT_ID" )
86- and os .getenv ("UIPATH_ORGANIZATION_ID" )
87- ):
88- try :
89- portal_service .ensure_valid_token ()
90- return
91- except Exception :
92- self ._console .error (
93- "Authentication token is invalid. Please reauthenticate using the '--force' flag." ,
94- )
74+ if not self ._force and self ._can_reuse_existing_token (portal_service ):
75+ return
76+
9577 auth_url , code_verifier , state = OidcUtils .get_auth_url (self ._domain )
96- webbrowser .open (auth_url , 1 )
97- auth_config = OidcUtils .get_auth_config ()
78+ self ._open_browser (auth_url )
9879
99- self ._console .link (
100- "If a browser window did not open, please open the following URL in your browser:" ,
101- auth_url ,
102- )
80+ auth_config = OidcUtils .get_auth_config ()
10381 server = HTTPServer (port = auth_config ["port" ])
10482 token_data = asyncio .run (server .start (state , code_verifier , self ._domain ))
105-
10683 if not token_data :
10784 self ._console .error (
10885 "Authentication failed. Please try again." ,
10986 )
87+ return
11088
111- portal_service .update_token_data (token_data )
112- update_auth_file (token_data )
113- access_token = token_data ["access_token" ]
114- update_env_file ({"UIPATH_ACCESS_TOKEN" : access_token })
89+ self ._finalize_auth (portal_service , token_data )
11590
116- tenants_and_organizations = portal_service .get_tenants_and_organizations ()
91+ def _finalize_auth (
92+ self ,
93+ portal_service : PortalService ,
94+ token_data : TokenData ,
95+ ) -> None :
96+ portal_service .update_token_data (token_data )
97+
98+ tenant_info = (
99+ portal_service .retrieve_tenant (self ._tenant )
100+ if self ._tenant
101+ else portal_service .select_tenant ()
102+ )
117103
118- if self ._tenant :
119- base_url = get_tenant_id (
120- self ._domain , self ._tenant , tenants_and_organizations
121- )
122- else :
123- base_url = select_tenant (self ._domain , tenants_and_organizations )
104+ tenant_id = tenant_info ["tenant_id" ]
105+ organization_id = tenant_info ["organization_id" ]
106+ uipath_url = portal_service .build_tenant_url ()
107+
108+ update_env_file (
109+ {
110+ "UIPATH_ACCESS_TOKEN" : token_data ["access_token" ],
111+ "UIPATH_URL" : uipath_url ,
112+ "UIPATH_TENANT_ID" : tenant_id ,
113+ "UIPATH_ORGANIZATION_ID" : organization_id ,
114+ }
115+ )
124116
117+ try :
118+ portal_service .enable_studio_web (uipath_url )
119+ except Exception :
120+ self ._console .error ("Could not prepare the environment. Please try again." )
121+
122+ def _can_reuse_existing_token (self , portal_service : PortalService ) -> bool :
123+ if (
124+ os .getenv ("UIPATH_URL" )
125+ and os .getenv ("UIPATH_TENANT_ID" )
126+ and os .getenv ("UIPATH_ORGANIZATION_ID" )
127+ ):
125128 try :
126- portal_service .post_auth (base_url )
129+ portal_service .ensure_valid_token ()
130+ return True
127131 except Exception :
128132 self ._console .error (
129- "Could not prepare the environment . Please try again." ,
133+ "Authentication token is invalid . Please reauthenticate using the '--force' flag."
130134 )
131-
132- def set_port (self ):
133- def is_port_in_use (target_port : int ) -> bool :
134- with socket (AF_INET , SOCK_STREAM ) as s :
135- try :
136- s .bind (("localhost" , target_port ))
137- s .close ()
138- return False
139- except error :
140- return True
141-
142- auth_config = OidcUtils .get_auth_config ()
143- port = int (auth_config .get ("port" , 8104 ))
144- port_option_one = int (auth_config .get ("portOptionOne" , 8104 )) # type: ignore
145- port_option_two = int (auth_config .get ("portOptionTwo" , 8055 )) # type: ignore
146- port_option_three = int (auth_config .get ("portOptionThree" , 42042 )) # type: ignore
147- if is_port_in_use (port ):
148- if is_port_in_use (port_option_one ):
149- if is_port_in_use (port_option_two ):
150- if is_port_in_use (port_option_three ):
151- self ._console .error (
152- "All configured ports are in use. Please close applications using ports or configure different ports."
153- )
154- else :
155- port = port_option_three
156- else :
157- port = port_option_two
158- else :
159- port = port_option_one
160- auth_config ["port" ] = port
161- with open (
162- os .path .join (os .path .dirname (__file__ ), ".." , "auth_config.json" ), "w"
163- ) as f :
164- json .dump (auth_config , f )
135+ return False
136+
137+ def _open_browser (self , url : str ) -> None :
138+ # Try to open browser. Always print the fallback link.
139+ webbrowser .open (url , new = 1 )
140+ self ._console .link (
141+ "If a browser window did not open, please open the following URL in your browser:" ,
142+ url ,
143+ )
0 commit comments