@@ -52,11 +52,12 @@ def _save_cache(cache):
5252 # Decide how to handle this, maybe clear cache or log extensively
5353 # session.pop("token_cache", None) # Option: Clear on serialization failure
5454
55- def _build_msal_app (cache = None ):
55+ def _build_msal_app (cache = None , authority_override = None ):
5656 """Builds the MSAL ConfidentialClientApplication, optionally initializing with a cache."""
57+ authority = authority_override or AUTHORITY
5758 return ConfidentialClientApplication (
5859 CLIENT_ID ,
59- authority = AUTHORITY ,
60+ authority = authority ,
6061 client_credential = CLIENT_SECRET ,
6162 token_cache = cache # Pass the cache instance here
6263 )
@@ -88,7 +89,7 @@ def get_valid_access_token(scopes=None):
8889
8990 required_scopes = scopes or SCOPE # Use default SCOPE if none provided
9091
91- msal_app = _build_msal_app (cache = _load_cache ())
92+ msal_app = _build_msal_app (cache = _load_cache (), authority_override = get_graph_authority () )
9293 user_info = session .get ("user" , {})
9394 # MSAL uses home_account_id which combines oid and tid
9495 # Construct it carefully based on your id_token_claims structure
@@ -160,7 +161,7 @@ def get_valid_access_token_for_plugins(scopes=None):
160161
161162 required_scopes = scopes or SCOPE # Use default SCOPE if none provided
162163
163- msal_app = _build_msal_app (cache = _load_cache ())
164+ msal_app = _build_msal_app (cache = _load_cache (), authority_override = get_graph_authority () )
164165 user_info = session .get ("user" , {})
165166 # MSAL uses home_account_id which combines oid and tid
166167 # Construct it carefully based on your id_token_claims structure
@@ -844,6 +845,103 @@ def get_current_user_info():
844845 "displayName" : user .get ("name" )
845846 }
846847
848+
849+ def _normalize_authority (authority_base , tenant_id ):
850+ """Normalize an authority URL and append tenant when appropriate."""
851+ base = (authority_base or "" ).strip ().rstrip ("/" )
852+ tenant = (tenant_id or "" ).strip ()
853+
854+ if not base or not tenant :
855+ return base
856+
857+ lowered = base .lower ()
858+ tenant_lower = tenant .lower ()
859+
860+ if lowered .endswith (f"/{ tenant_lower } " ):
861+ return base
862+
863+ if lowered .endswith ("/common" ) or lowered .endswith ("/organizations" ) or lowered .endswith ("/consumers" ):
864+ return base
865+
866+ return f"{ base } /{ tenant } "
867+
868+
869+ def get_graph_authority ():
870+ """
871+ Resolve authority for Graph token acquisition, independent of general Azure environment defaults.
872+
873+ Precedence:
874+ 1. CUSTOM_GRAPH_AUTHORITY_URL_VALUE if provided
875+ 2. Custom cloud identity authority for AZURE_ENVIRONMENT=custom
876+ 3. Gov/Public cloud authority based on AZURE_ENVIRONMENT
877+ """
878+ custom_graph_authority = (CUSTOM_GRAPH_AUTHORITY_URL_VALUE or "" ).strip ()
879+ if custom_graph_authority :
880+ return _normalize_authority (custom_graph_authority , TENANT_ID )
881+
882+ if AZURE_ENVIRONMENT == "custom" :
883+ return _normalize_authority (CUSTOM_IDENTITY_URL_VALUE , TENANT_ID )
884+
885+ if AZURE_ENVIRONMENT == "usgovernment" :
886+ return f"https://login.microsoftonline.us/{ TENANT_ID } "
887+
888+ return f"https://login.microsoftonline.com/{ TENANT_ID } "
889+
890+
891+ def get_graph_base_url ():
892+ """
893+ Resolve the Microsoft Graph base URL for this deployment.
894+
895+ Precedence:
896+ 1. CUSTOM_GRAPH_URL_VALUE if provided (works in any AZURE_ENVIRONMENT mode)
897+ 2. Azure Gov Graph for usgovernment
898+ 3. Public Graph by default
899+
900+ Returns:
901+ str: Normalized Graph base URL ending with /v1.0
902+ """
903+ custom_graph_url = (CUSTOM_GRAPH_URL_VALUE or "" ).strip ().rstrip ("/" )
904+ if custom_graph_url :
905+ normalized = custom_graph_url
906+ lowered = normalized .lower ()
907+
908+ # Allow legacy values such as https://.../v1.0/users
909+ if lowered .endswith ("/users" ):
910+ normalized = normalized [:- 6 ].rstrip ("/" )
911+ lowered = normalized .lower ()
912+
913+ if "/v1.0" not in lowered :
914+ normalized = f"{ normalized } /v1.0"
915+
916+ return normalized
917+
918+ if AZURE_ENVIRONMENT == "usgovernment" :
919+ return "https://graph.microsoft.us/v1.0"
920+
921+ return "https://graph.microsoft.com/v1.0"
922+
923+
924+ def get_graph_endpoint (path = "" ):
925+ """
926+ Build a full Graph endpoint from a relative path.
927+
928+ Args:
929+ path (str): Relative Graph path (for example: "/users" or "users/{id}")
930+
931+ Returns:
932+ str: Fully qualified Microsoft Graph URL
933+ """
934+ base_url = get_graph_base_url ().rstrip ("/" )
935+ path = (path or "" ).strip ()
936+
937+ if not path :
938+ return base_url
939+
940+ if not path .startswith ("/" ):
941+ path = f"/{ path } "
942+
943+ return f"{ base_url } { path } "
944+
847945def get_user_profile_image ():
848946 """
849947 Fetches the user's profile image from Microsoft Graph and returns it as base64.
@@ -854,13 +952,7 @@ def get_user_profile_image():
854952 debug_print ("get_user_profile_image: Could not acquire access token" )
855953 return None
856954
857- # Determine the correct Graph endpoint based on Azure environment
858- if AZURE_ENVIRONMENT == "usgovernment" :
859- profile_image_endpoint = "https://graph.microsoft.us/v1.0/me/photo/$value"
860- elif AZURE_ENVIRONMENT == "custom" :
861- profile_image_endpoint = f"{ CUSTOM_GRAPH_URL_VALUE } /me/photo/$value"
862- else :
863- profile_image_endpoint = "https://graph.microsoft.com/v1.0/me/photo/$value"
955+ profile_image_endpoint = get_graph_endpoint ("/me/photo/$value" )
864956
865957 headers = {
866958 "Authorization" : f"Bearer { token } " ,
0 commit comments