microsoft-agents-authentication-msal 0.7.0.dev12__tar.gz → 0.7.0.dev16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/PKG-INFO +2 -2
  2. microsoft_agents_authentication_msal-0.7.0.dev16/VERSION.txt +1 -0
  3. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents/authentication/msal/msal_auth.py +94 -35
  4. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents/authentication/msal/msal_connection_manager.py +32 -12
  5. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents_authentication_msal.egg-info/PKG-INFO +2 -2
  6. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents_authentication_msal.egg-info/requires.txt +1 -1
  7. microsoft_agents_authentication_msal-0.7.0.dev12/VERSION.txt +0 -1
  8. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/LICENSE +0 -0
  9. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/MANIFEST.in +0 -0
  10. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents/authentication/msal/__init__.py +0 -0
  11. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents/authentication/msal/errors/__init__.py +0 -0
  12. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents/authentication/msal/errors/error_resources.py +0 -0
  13. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents_authentication_msal.egg-info/SOURCES.txt +0 -0
  14. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents_authentication_msal.egg-info/dependency_links.txt +0 -0
  15. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/microsoft_agents_authentication_msal.egg-info/top_level.txt +0 -0
  16. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/pyproject.toml +0 -0
  17. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/readme.md +0 -0
  18. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/setup.cfg +0 -0
  19. {microsoft_agents_authentication_msal-0.7.0.dev12 → microsoft_agents_authentication_msal-0.7.0.dev16}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-authentication-msal
3
- Version: 0.7.0.dev12
3
+ Version: 0.7.0.dev16
4
4
  Summary: A msal-based authentication library for Microsoft Agents
5
5
  Author: Microsoft Corporation
6
6
  License-Expression: MIT
@@ -15,7 +15,7 @@ Classifier: Operating System :: OS Independent
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: microsoft-agents-hosting-core==0.7.0.dev12
18
+ Requires-Dist: microsoft-agents-hosting-core==0.7.0.dev16
19
19
  Requires-Dist: msal>=1.31.1
20
20
  Requires-Dist: requests>=2.32.3
21
21
  Requires-Dist: cryptography>=44.0.0
@@ -0,0 +1 @@
1
+ 0.7.0.dev16
@@ -3,6 +3,7 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import re
6
7
  import asyncio
7
8
  import logging
8
9
  import jwt
@@ -13,6 +14,7 @@ from msal import (
13
14
  ManagedIdentityClient,
14
15
  UserAssignedManagedIdentity,
15
16
  SystemAssignedManagedIdentity,
17
+ TokenCache,
16
18
  )
17
19
  from requests import Session
18
20
  from cryptography.x509 import load_pem_x509_certificate
@@ -53,7 +55,12 @@ class MsalAuth(AccessTokenProviderBase):
53
55
  """
54
56
 
55
57
  self._msal_configuration = msal_configuration
56
- self._msal_auth_client = None
58
+ self._msal_auth_client_map: dict[
59
+ str, ConfidentialClientApplication | ManagedIdentityClient
60
+ ] = {}
61
+
62
+ # TokenCache is thread-safe and async-safe per MSAL documentation
63
+ self._token_cache = TokenCache()
57
64
  logger.debug(
58
65
  f"Initializing MsalAuth with configuration: {self._msal_configuration}"
59
66
  )
@@ -67,19 +74,20 @@ class MsalAuth(AccessTokenProviderBase):
67
74
  valid_uri, instance_uri = self._uri_validator(resource_url)
68
75
  if not valid_uri:
69
76
  raise ValueError(str(authentication_errors.InvalidInstanceUrl))
77
+ assert instance_uri is not None # for mypy
70
78
 
71
79
  local_scopes = self._resolve_scopes_list(instance_uri, scopes)
72
- self._create_client_application()
80
+ msal_auth_client = self._get_client()
73
81
 
74
- if isinstance(self._msal_auth_client, ManagedIdentityClient):
82
+ if isinstance(msal_auth_client, ManagedIdentityClient):
75
83
  logger.info("Acquiring token using Managed Identity Client.")
76
84
  auth_result_payload = await _async_acquire_token_for_client(
77
- self._msal_auth_client, resource=resource_url
85
+ msal_auth_client, resource=resource_url
78
86
  )
79
- elif isinstance(self._msal_auth_client, ConfidentialClientApplication):
87
+ elif isinstance(msal_auth_client, ConfidentialClientApplication):
80
88
  logger.info("Acquiring token using Confidential Client Application.")
81
89
  auth_result_payload = await _async_acquire_token_for_client(
82
- self._msal_auth_client, scopes=local_scopes
90
+ msal_auth_client, scopes=local_scopes
83
91
  )
84
92
  else:
85
93
  auth_result_payload = None
@@ -105,21 +113,21 @@ class MsalAuth(AccessTokenProviderBase):
105
113
  :return: The access token as a string.
106
114
  """
107
115
 
108
- self._create_client_application()
109
- if isinstance(self._msal_auth_client, ManagedIdentityClient):
116
+ msal_auth_client = self._get_client()
117
+ if isinstance(msal_auth_client, ManagedIdentityClient):
110
118
  logger.error(
111
119
  "Attempted on-behalf-of flow with Managed Identity authentication."
112
120
  )
113
121
  raise NotImplementedError(
114
122
  str(authentication_errors.OnBehalfOfFlowNotSupportedManagedIdentity)
115
123
  )
116
- elif isinstance(self._msal_auth_client, ConfidentialClientApplication):
124
+ elif isinstance(msal_auth_client, ConfidentialClientApplication):
117
125
  # TODO: Handling token error / acquisition failed
118
126
 
119
127
  # MSAL in Python does not support async, so we use asyncio.to_thread to run it in
120
128
  # a separate thread and avoid blocking the event loop
121
129
  token = await asyncio.to_thread(
122
- lambda: self._msal_auth_client.acquire_token_on_behalf_of(
130
+ lambda: msal_auth_client.acquire_token_on_behalf_of(
123
131
  scopes=scopes, user_assertion=user_assertion
124
132
  )
125
133
  )
@@ -135,21 +143,51 @@ class MsalAuth(AccessTokenProviderBase):
135
143
  return token["access_token"]
136
144
 
137
145
  logger.error(
138
- f"On-behalf-of flow is not supported with the current authentication type: {self._msal_auth_client.__class__.__name__}"
146
+ f"On-behalf-of flow is not supported with the current authentication type: {msal_auth_client.__class__.__name__}"
139
147
  )
140
148
  raise NotImplementedError(
141
149
  authentication_errors.OnBehalfOfFlowNotSupportedAuthType.format(
142
- self._msal_auth_client.__class__.__name__
150
+ msal_auth_client.__class__.__name__
143
151
  )
144
152
  )
145
153
 
146
- def _create_client_application(self) -> None:
154
+ @staticmethod
155
+ def _resolve_authority(
156
+ config: AgentAuthConfiguration, tenant_id: str | None = None
157
+ ) -> str:
158
+ tenant_id = MsalAuth._resolve_tenant_id(config, tenant_id)
159
+ if not tenant_id:
160
+ return (
161
+ config.AUTHORITY
162
+ or f"https://login.microsoftonline.com/{config.TENANT_ID}"
163
+ )
164
+
165
+ if config.AUTHORITY:
166
+ return re.sub(r"/common(?=/|$)", f"/{tenant_id}", config.AUTHORITY)
167
+
168
+ return f"https://login.microsoftonline.com/{tenant_id}"
169
+
170
+ @staticmethod
171
+ def _resolve_tenant_id(
172
+ config: AgentAuthConfiguration, tenant_id: str | None = None
173
+ ) -> str:
147
174
 
148
- if self._msal_auth_client:
149
- return
175
+ if not config.TENANT_ID:
176
+ if tenant_id:
177
+ return tenant_id
178
+ raise ValueError("TENANT_ID is not set in the configuration.")
179
+
180
+ if tenant_id and config.TENANT_ID.lower() == "common":
181
+ return tenant_id
182
+
183
+ return config.TENANT_ID
184
+
185
+ def _create_client_application(
186
+ self, tenant_id: str | None = None
187
+ ) -> ConfidentialClientApplication | ManagedIdentityClient:
150
188
 
151
189
  if self._msal_configuration.AUTH_TYPE == AuthTypes.user_managed_identity:
152
- self._msal_auth_client = ManagedIdentityClient(
190
+ return ManagedIdentityClient(
153
191
  UserAssignedManagedIdentity(
154
192
  client_id=self._msal_configuration.CLIENT_ID
155
193
  ),
@@ -157,13 +195,12 @@ class MsalAuth(AccessTokenProviderBase):
157
195
  )
158
196
 
159
197
  elif self._msal_configuration.AUTH_TYPE == AuthTypes.system_managed_identity:
160
- self._msal_auth_client = ManagedIdentityClient(
198
+ return ManagedIdentityClient(
161
199
  SystemAssignedManagedIdentity(),
162
200
  http_client=Session(),
163
201
  )
164
202
  else:
165
- authority_path = self._msal_configuration.TENANT_ID or "botframework.com"
166
- authority = f"https://login.microsoftonline.com/{authority_path}"
203
+ authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id)
167
204
 
168
205
  if self._client_credential_cache:
169
206
  logger.info("Using cached client credentials for MSAL authentication.")
@@ -200,12 +237,31 @@ class MsalAuth(AccessTokenProviderBase):
200
237
  str(authentication_errors.AuthenticationTypeNotSupported)
201
238
  )
202
239
 
203
- self._msal_auth_client = ConfidentialClientApplication(
240
+ return ConfidentialClientApplication(
204
241
  client_id=self._msal_configuration.CLIENT_ID,
205
242
  authority=authority,
206
243
  client_credential=self._client_credential_cache,
207
244
  )
208
245
 
246
+ def _client_rep(
247
+ self, tenant_id: str | None = None, instance_id: str | None = None
248
+ ) -> str:
249
+ # Create a unique representation for the client based on tenant_id and instance_id
250
+ # instance_id None is for when no agentic instance is associated with the request.
251
+ tenant_id = tenant_id or self._msal_configuration.TENANT_ID
252
+ return f"tenant:{tenant_id}.instance:{instance_id}"
253
+
254
+ def _get_client(
255
+ self, tenant_id: str | None = None, instance_id: str | None = None
256
+ ) -> ConfidentialClientApplication | ManagedIdentityClient:
257
+ rep = self._client_rep(tenant_id, instance_id)
258
+ if rep in self._msal_auth_client_map:
259
+ return self._msal_auth_client_map[rep]
260
+ else:
261
+ client = self._create_client_application(tenant_id)
262
+ self._msal_auth_client_map[rep] = client
263
+ return client
264
+
209
265
  @staticmethod
210
266
  def _uri_validator(url_str: str) -> tuple[bool, Optional[URI]]:
211
267
  try:
@@ -220,7 +276,8 @@ class MsalAuth(AccessTokenProviderBase):
220
276
  return scopes
221
277
 
222
278
  temp_list: list[str] = []
223
- for scope in self._msal_configuration.SCOPES:
279
+ lst = self._msal_configuration.SCOPES or []
280
+ for scope in lst:
224
281
  scope_placeholder = scope
225
282
  if "{instance}" in scope_placeholder.lower():
226
283
  scope_placeholder = scope_placeholder.replace(
@@ -233,7 +290,7 @@ class MsalAuth(AccessTokenProviderBase):
233
290
  # the call to MSAL is blocking, but in the future we want to create an asyncio task
234
291
  # to avoid this
235
292
  async def get_agentic_application_token(
236
- self, agent_app_instance_id: str
293
+ self, tenant_id: str, agent_app_instance_id: str
237
294
  ) -> Optional[str]:
238
295
  """Gets the agentic application token for the given agent application instance ID.
239
296
 
@@ -252,13 +309,13 @@ class MsalAuth(AccessTokenProviderBase):
252
309
  "Attempting to get agentic application token from agent_app_instance_id %s",
253
310
  agent_app_instance_id,
254
311
  )
255
- self._create_client_application()
312
+ msal_auth_client = self._get_client(tenant_id, agent_app_instance_id)
256
313
 
257
- if isinstance(self._msal_auth_client, ConfidentialClientApplication):
314
+ if isinstance(msal_auth_client, ConfidentialClientApplication):
258
315
 
259
316
  # https://github.dev/AzureAD/microsoft-authentication-library-for-dotnet
260
317
  auth_result_payload = await _async_acquire_token_for_client(
261
- self._msal_auth_client,
318
+ msal_auth_client,
262
319
  ["api://AzureAdTokenExchange/.default"],
263
320
  data={"fmi_path": agent_app_instance_id},
264
321
  )
@@ -269,7 +326,7 @@ class MsalAuth(AccessTokenProviderBase):
269
326
  return None
270
327
 
271
328
  async def get_agentic_instance_token(
272
- self, agent_app_instance_id: str
329
+ self, tenant_id: str, agent_app_instance_id: str
273
330
  ) -> tuple[str, str]:
274
331
  """Gets the agentic instance token for the given agent application instance ID.
275
332
 
@@ -289,7 +346,7 @@ class MsalAuth(AccessTokenProviderBase):
289
346
  agent_app_instance_id,
290
347
  )
291
348
  agent_token_result = await self.get_agentic_application_token(
292
- agent_app_instance_id
349
+ tenant_id, agent_app_instance_id
293
350
  )
294
351
 
295
352
  if not agent_token_result:
@@ -303,14 +360,13 @@ class MsalAuth(AccessTokenProviderBase):
303
360
  )
304
361
  )
305
362
 
306
- authority = (
307
- f"https://login.microsoftonline.com/{self._msal_configuration.TENANT_ID}"
308
- )
363
+ authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id)
309
364
 
310
365
  instance_app = ConfidentialClientApplication(
311
366
  client_id=agent_app_instance_id,
312
367
  authority=authority,
313
368
  client_credential={"client_assertion": agent_token_result},
369
+ token_cache=self._token_cache,
314
370
  )
315
371
 
316
372
  agentic_instance_token = await _async_acquire_token_for_client(
@@ -353,7 +409,11 @@ class MsalAuth(AccessTokenProviderBase):
353
409
  return agentic_instance_token["access_token"], agent_token_result
354
410
 
355
411
  async def get_agentic_user_token(
356
- self, agent_app_instance_id: str, agentic_user_id: str, scopes: list[str]
412
+ self,
413
+ tenant_id: str,
414
+ agent_app_instance_id: str,
415
+ agentic_user_id: str,
416
+ scopes: list[str],
357
417
  ) -> Optional[str]:
358
418
  """Gets the agentic user token for the given agent application instance ID and agentic user Id and the scopes.
359
419
 
@@ -377,7 +437,7 @@ class MsalAuth(AccessTokenProviderBase):
377
437
  agentic_user_id,
378
438
  )
379
439
  instance_token, agent_token = await self.get_agentic_instance_token(
380
- agent_app_instance_id
440
+ tenant_id, agent_app_instance_id
381
441
  )
382
442
 
383
443
  if not instance_token or not agent_token:
@@ -392,14 +452,13 @@ class MsalAuth(AccessTokenProviderBase):
392
452
  )
393
453
  )
394
454
 
395
- authority = (
396
- f"https://login.microsoftonline.com/{self._msal_configuration.TENANT_ID}"
397
- )
455
+ authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id)
398
456
 
399
457
  instance_app = ConfidentialClientApplication(
400
458
  client_id=agent_app_instance_id,
401
459
  authority=authority,
402
460
  client_credential={"client_assertion": agent_token},
461
+ token_cache=self._token_cache,
403
462
  )
404
463
 
405
464
  logger.info(
@@ -36,16 +36,15 @@ class MsalConnectionManager(Connections):
36
36
 
37
37
  self._connections: Dict[str, MsalAuth] = {}
38
38
  self._connections_map = connections_map or kwargs.get("CONNECTIONSMAP", {})
39
- self._service_connection_configuration: AgentAuthConfiguration = None
39
+ self._config_map: dict[str, AgentAuthConfiguration] = {}
40
40
 
41
41
  if connections_configurations:
42
42
  for (
43
43
  connection_name,
44
- connection_settings,
44
+ agent_auth_config,
45
45
  ) in connections_configurations.items():
46
- self._connections[connection_name] = MsalAuth(
47
- AgentAuthConfiguration(**connection_settings)
48
- )
46
+ self._connections[connection_name] = MsalAuth(agent_auth_config)
47
+ self._config_map[connection_name] = agent_auth_config
49
48
  else:
50
49
  raw_configurations: Dict[str, Dict] = kwargs.get("CONNECTIONS", {})
51
50
  for connection_name, connection_settings in raw_configurations.items():
@@ -53,8 +52,11 @@ class MsalConnectionManager(Connections):
53
52
  **connection_settings.get("SETTINGS", {})
54
53
  )
55
54
  self._connections[connection_name] = MsalAuth(parsed_configuration)
56
- if connection_name == "SERVICE_CONNECTION":
57
- self._service_connection_configuration = parsed_configuration
55
+ self._config_map[connection_name] = parsed_configuration
56
+
57
+ # JWT-patch
58
+ for connection_name, config in self._config_map.items():
59
+ config._connections = self._config_map
58
60
 
59
61
  if not self._connections.get("SERVICE_CONNECTION", None):
60
62
  raise ValueError("No service connection configuration provided.")
@@ -68,8 +70,17 @@ class MsalConnectionManager(Connections):
68
70
  :return: The OAuth connection for the agent.
69
71
  :rtype: :class:`microsoft_agents.hosting.core.AccessTokenProviderBase`
70
72
  """
71
- # should never be None
72
- return self._connections.get(connection_name, None)
73
+ original_name = connection_name
74
+ connection_name = connection_name or "SERVICE_CONNECTION"
75
+ connection = self._connections.get(connection_name, None)
76
+ if not connection:
77
+ if original_name:
78
+ raise ValueError(f"No connection found for '{original_name}'.")
79
+ else:
80
+ raise ValueError(
81
+ "No default service connection found. Expected 'SERVICE_CONNECTION'."
82
+ )
83
+ return connection
73
84
 
74
85
  def get_default_connection(self) -> AccessTokenProviderBase:
75
86
  """
@@ -78,8 +89,12 @@ class MsalConnectionManager(Connections):
78
89
  :return: The default OAuth connection for the agent.
79
90
  :rtype: :class:`microsoft_agents.hosting.core.AccessTokenProviderBase`
80
91
  """
81
- # should never be None
82
- return self._connections.get("SERVICE_CONNECTION", None)
92
+ connection = self._connections.get("SERVICE_CONNECTION", None)
93
+ if not connection:
94
+ raise ValueError(
95
+ "No default service connection found. Expected 'SERVICE_CONNECTION'."
96
+ )
97
+ return connection
83
98
 
84
99
  def get_token_provider(
85
100
  self, claims_identity: ClaimsIdentity, service_url: str
@@ -137,4 +152,9 @@ class MsalConnectionManager(Connections):
137
152
  :return: The default connection configuration for the agent.
138
153
  :rtype: :class:`microsoft_agents.hosting.core.AgentAuthConfiguration`
139
154
  """
140
- return self._service_connection_configuration
155
+ config = self._config_map.get("SERVICE_CONNECTION")
156
+ if not config:
157
+ raise ValueError(
158
+ "No default service connection configuration found. Expected 'SERVICE_CONNECTION'."
159
+ )
160
+ return config
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-authentication-msal
3
- Version: 0.7.0.dev12
3
+ Version: 0.7.0.dev16
4
4
  Summary: A msal-based authentication library for Microsoft Agents
5
5
  Author: Microsoft Corporation
6
6
  License-Expression: MIT
@@ -15,7 +15,7 @@ Classifier: Operating System :: OS Independent
15
15
  Requires-Python: >=3.10
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
- Requires-Dist: microsoft-agents-hosting-core==0.7.0.dev12
18
+ Requires-Dist: microsoft-agents-hosting-core==0.7.0.dev16
19
19
  Requires-Dist: msal>=1.31.1
20
20
  Requires-Dist: requests>=2.32.3
21
21
  Requires-Dist: cryptography>=44.0.0
@@ -1,4 +1,4 @@
1
- microsoft-agents-hosting-core==0.7.0.dev12
1
+ microsoft-agents-hosting-core==0.7.0.dev16
2
2
  msal>=1.31.1
3
3
  requests>=2.32.3
4
4
  cryptography>=44.0.0
@@ -1 +0,0 @@
1
- 0.7.0.dev12