Skip to content

Commit dcde439

Browse files
upd for cross-cloud calls and unification of OIDC endpoints/calls (#771)
Co-authored-by: Bionic711 <nadoyle@microsoft.com>
1 parent 079f8e4 commit dcde439

6 files changed

Lines changed: 130 additions & 43 deletions

File tree

application/single_app/config.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
EXECUTOR_TYPE = 'thread'
9595
EXECUTOR_MAX_WORKERS = 30
9696
SESSION_TYPE = 'filesystem'
97-
VERSION = "0.239.002"
97+
VERSION = "0.239.004"
9898

9999
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production')
100100

@@ -176,12 +176,14 @@ def get_allowed_extensions(enable_video=False, enable_audio=False):
176176

177177
# Add Support for Custom Azure Environments
178178
CUSTOM_GRAPH_URL_VALUE = os.getenv("CUSTOM_GRAPH_URL_VALUE", "")
179+
CUSTOM_GRAPH_AUTHORITY_URL_VALUE = os.getenv("CUSTOM_GRAPH_AUTHORITY_URL_VALUE", "")
179180
CUSTOM_IDENTITY_URL_VALUE = os.getenv("CUSTOM_IDENTITY_URL_VALUE", "")
180181
CUSTOM_RESOURCE_MANAGER_URL_VALUE = os.getenv("CUSTOM_RESOURCE_MANAGER_URL_VALUE", "")
181182
CUSTOM_BLOB_STORAGE_URL_VALUE = os.getenv("CUSTOM_BLOB_STORAGE_URL_VALUE", "")
182183
CUSTOM_COGNITIVE_SERVICES_URL_VALUE = os.getenv("CUSTOM_COGNITIVE_SERVICES_URL_VALUE", "")
183184
CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE = os.getenv("CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE", "")
184185
CUSTOM_REDIS_CACHE_INFRASTRUCTURE_URL_VALUE = os.getenv("CUSTOM_REDIS_CACHE_INFRASTRUCTURE_URL_VALUE", "")
186+
CUSTOM_OIDC_METADATA_URL_VALUE = os.getenv("CUSTOM_OIDC_METADATA_URL_VALUE", "")
185187

186188

187189
# Azure AD Configuration
@@ -193,41 +195,39 @@ def get_allowed_extensions(enable_video=False, enable_audio=False):
193195
MICROSOFT_PROVIDER_AUTHENTICATION_SECRET = os.getenv("MICROSOFT_PROVIDER_AUTHENTICATION_SECRET")
194196
LOGIN_REDIRECT_URL = os.getenv("LOGIN_REDIRECT_URL")
195197
HOME_REDIRECT_URL = os.getenv("HOME_REDIRECT_URL") # Front Door URL for home page
196-
197-
OIDC_METADATA_URL = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration"
198198
AZURE_ENVIRONMENT = os.getenv("AZURE_ENVIRONMENT", "public") # public, usgovernment, custom
199199

200-
if AZURE_ENVIRONMENT == "custom":
200+
WORD_CHUNK_SIZE = 400
201+
202+
if AZURE_ENVIRONMENT == "custom" or CUSTOM_IDENTITY_URL_VALUE or CUSTOM_GRAPH_AUTHORITY_URL_VALUE:
201203
AUTHORITY = f"{CUSTOM_IDENTITY_URL_VALUE}/{TENANT_ID}"
204+
authority = CUSTOM_GRAPH_AUTHORITY_URL_VALUE or CUSTOM_IDENTITY_URL_VALUE or AUTHORITY.rstrip(f'/{TENANT_ID}')
202205
elif AZURE_ENVIRONMENT == "usgovernment":
203206
AUTHORITY = f"https://login.microsoftonline.us/{TENANT_ID}"
207+
authority = AzureAuthorityHosts.AZURE_GOVERNMENT
204208
else:
205209
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
210+
authority = AzureAuthorityHosts.AZURE_PUBLIC_CLOUD
206211

207-
WORD_CHUNK_SIZE = 400
208-
209-
if AZURE_ENVIRONMENT == "usgovernment":
212+
if AZURE_ENVIRONMENT == "custom":
213+
OIDC_METADATA_URL = CUSTOM_OIDC_METADATA_URL_VALUE or f"https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration"
214+
resource_manager = CUSTOM_RESOURCE_MANAGER_URL_VALUE
215+
video_indexer_endpoint = os.getenv("CUSTOM_VIDEO_INDEXER_ENDPOINT", "https://api.videoindexer.ai")
216+
credential_scopes=[resource_manager + "/.default"]
217+
cognitive_services_scope = CUSTOM_COGNITIVE_SERVICES_URL_VALUE
218+
search_resource_manager = CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE
219+
KEY_VAULT_DOMAIN = os.getenv("KEY_VAULT_DOMAIN", ".vault.azure.net")
220+
elif AZURE_ENVIRONMENT == "usgovernment":
210221
OIDC_METADATA_URL = f"https://login.microsoftonline.us/{TENANT_ID}/v2.0/.well-known/openid-configuration"
211222
resource_manager = "https://management.usgovcloudapi.net"
212-
authority = AzureAuthorityHosts.AZURE_GOVERNMENT
213223
credential_scopes=[resource_manager + "/.default"]
214224
cognitive_services_scope = "https://cognitiveservices.azure.us/.default"
215225
video_indexer_endpoint = "https://api.videoindexer.ai.azure.us"
216226
search_resource_manager = "https://search.azure.us"
217227
KEY_VAULT_DOMAIN = ".vault.usgovcloudapi.net"
218-
219-
elif AZURE_ENVIRONMENT == "custom":
220-
resource_manager = CUSTOM_RESOURCE_MANAGER_URL_VALUE
221-
authority = CUSTOM_IDENTITY_URL_VALUE
222-
video_indexer_endpoint = os.getenv("CUSTOM_VIDEO_INDEXER_ENDPOINT", "https://api.videoindexer.ai")
223-
credential_scopes=[resource_manager + "/.default"]
224-
cognitive_services_scope = CUSTOM_COGNITIVE_SERVICES_URL_VALUE
225-
search_resource_manager = CUSTOM_SEARCH_RESOURCE_MANAGER_URL_VALUE
226-
KEY_VAULT_DOMAIN = os.getenv("KEY_VAULT_DOMAIN", ".vault.azure.net")
227228
else:
228229
OIDC_METADATA_URL = f"https://login.microsoftonline.com/{TENANT_ID}/v2.0/.well-known/openid-configuration"
229230
resource_manager = "https://management.azure.com"
230-
authority = AzureAuthorityHosts.AZURE_PUBLIC_CLOUD
231231
credential_scopes=[resource_manager + "/.default"]
232232
cognitive_services_scope = "https://cognitiveservices.azure.com/.default"
233233
video_indexer_endpoint = "https://api.videoindexer.ai"

application/single_app/example.env

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ TENANT_ID="<your-azure-ad-tenant-id>"
1515
SECRET_KEY="Generate-A-Strong-Random-Secret-Key-Here!"
1616
# AZURE_ENVIRONMENT: Set based on your cloud environment
1717
# Options: "public", "usgovernment", "custom"
18-
AZURE_ENVIRONMENT="public"
18+
AZURE_ENVIRONMENT="public"
19+
20+
# Optional Graph overrides (for cross-cloud identity/Graph scenarios)
21+
# Example values:
22+
# CUSTOM_GRAPH_URL_VALUE="https://graph.microsoft.com"
23+
# CUSTOM_GRAPH_AUTHORITY_URL_VALUE="https://login.microsoftonline.com"

application/single_app/functions_authentication.py

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
847945
def 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}",

application/single_app/route_backend_documents.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,7 @@ def api_get_shared_users(document_id):
13781378
approval_status = entry.get('approval_status', 'unknown')
13791379
try:
13801380
# Get user details from Microsoft Graph
1381-
graph_url = f"https://graph.microsoft.com/v1.0/users/{oid}"
1381+
graph_url = get_graph_endpoint(f"/users/{oid}")
13821382
response = requests.get(graph_url, headers=headers)
13831383

13841384
if response.status_code == 200:

application/single_app/route_backend_public_workspaces.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,7 @@ def get_user_details_from_graph(user_id):
4444
if not token:
4545
return {"displayName": "", "email": ""}
4646

47-
if AZURE_ENVIRONMENT == "usgovernment":
48-
user_endpoint = f"https://graph.microsoft.us/v1.0/users/{user_id}"
49-
elif AZURE_ENVIRONMENT == "custom":
50-
user_endpoint = f"{CUSTOM_GRAPH_URL_VALUE}/{user_id}"
51-
else:
52-
user_endpoint = f"https://graph.microsoft.com/v1.0/users/{user_id}"
47+
user_endpoint = get_graph_endpoint(f"/users/{user_id}")
5348

5449
headers = {
5550
"Authorization": f"Bearer {token}",

application/single_app/route_backend_users.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,7 @@ def api_user_search():
2424
if not token:
2525
return jsonify({"error": "Could not acquire access token"}), 401
2626

27-
if AZURE_ENVIRONMENT == "usgovernment":
28-
user_endpoint = "https://graph.microsoft.us/v1.0/users"
29-
elif AZURE_ENVIRONMENT == "custom":
30-
user_endpoint = CUSTOM_GRAPH_URL_VALUE
31-
else:
32-
user_endpoint = "https://graph.microsoft.com/v1.0/users"
27+
user_endpoint = get_graph_endpoint("/users")
3328

3429
headers = {
3530
"Authorization": f"Bearer {token}",

0 commit comments

Comments
 (0)