microsoft-agents-authentication-msal 0.3.2__tar.gz → 0.4.0__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 (16) hide show
  1. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/PKG-INFO +2 -2
  2. microsoft_agents_authentication_msal-0.4.0/microsoft_agents/authentication/msal/msal_auth.py +395 -0
  3. microsoft_agents_authentication_msal-0.4.0/microsoft_agents/authentication/msal/msal_connection_manager.py +134 -0
  4. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/microsoft_agents_authentication_msal.egg-info/PKG-INFO +2 -2
  5. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/microsoft_agents_authentication_msal.egg-info/SOURCES.txt +1 -3
  6. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/microsoft_agents_authentication_msal.egg-info/requires.txt +1 -1
  7. microsoft_agents_authentication_msal-0.3.2/microsoft_agents/authentication/msal/msal_auth.py +0 -188
  8. microsoft_agents_authentication_msal-0.3.2/microsoft_agents/authentication/msal/msal_connection_manager.py +0 -72
  9. microsoft_agents_authentication_msal-0.3.2/tests/test_msal_auth.py +0 -83
  10. microsoft_agents_authentication_msal-0.3.2/tests/test_msal_connection_manager.py +0 -35
  11. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/microsoft_agents/authentication/msal/__init__.py +0 -0
  12. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/microsoft_agents_authentication_msal.egg-info/dependency_links.txt +0 -0
  13. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/microsoft_agents_authentication_msal.egg-info/top_level.txt +0 -0
  14. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/pyproject.toml +0 -0
  15. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/setup.cfg +0 -0
  16. {microsoft_agents_authentication_msal-0.3.2 → microsoft_agents_authentication_msal-0.4.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-authentication-msal
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: A msal-based authentication library for Microsoft Agents
5
5
  Author: Microsoft Corporation
6
6
  Project-URL: Homepage, https://github.com/microsoft/Agents
@@ -8,7 +8,7 @@ Classifier: Programming Language :: Python :: 3
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Operating System :: OS Independent
10
10
  Requires-Python: >=3.9
11
- Requires-Dist: microsoft-agents-hosting-core==0.3.2
11
+ Requires-Dist: microsoft-agents-hosting-core==0.4.0
12
12
  Requires-Dist: msal>=1.31.1
13
13
  Requires-Dist: requests>=2.32.3
14
14
  Requires-Dist: cryptography>=44.0.0
@@ -0,0 +1,395 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ import jwt
8
+ from typing import Optional
9
+ from urllib.parse import urlparse, ParseResult as URI
10
+ from msal import (
11
+ ConfidentialClientApplication,
12
+ ManagedIdentityClient,
13
+ UserAssignedManagedIdentity,
14
+ SystemAssignedManagedIdentity,
15
+ )
16
+ from requests import Session
17
+ from cryptography.x509 import load_pem_x509_certificate
18
+ from cryptography.hazmat.backends import default_backend
19
+ from cryptography.hazmat.primitives import hashes
20
+
21
+ from microsoft_agents.hosting.core import (
22
+ AuthTypes,
23
+ AccessTokenProviderBase,
24
+ AgentAuthConfiguration,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ # this is deferred because jwt.decode is expensive and we don't want to do it unless we
31
+ # have logging.DEBUG enabled
32
+ class _DeferredLogOfBlueprintId:
33
+ def __init__(self, jwt_token: str):
34
+ self.jwt_token = jwt_token
35
+
36
+ def __str__(self):
37
+ payload = jwt.decode(self.jwt_token, options={"verify_signature": False})
38
+ agentic_blueprint_id = payload.get("xms_par_app_azp")
39
+ return f"Agentic blueprint id: {agentic_blueprint_id}"
40
+
41
+
42
+ class MsalAuth(AccessTokenProviderBase):
43
+
44
+ _client_credential_cache = None
45
+
46
+ def __init__(self, msal_configuration: AgentAuthConfiguration):
47
+ self._msal_configuration = msal_configuration
48
+ logger.debug(
49
+ f"Initializing MsalAuth with configuration: {self._msal_configuration}"
50
+ )
51
+
52
+ async def get_access_token(
53
+ self, resource_url: str, scopes: list[str], force_refresh: bool = False
54
+ ) -> str:
55
+ logger.debug(
56
+ f"Requesting access token for resource: {resource_url}, scopes: {scopes}"
57
+ )
58
+ valid_uri, instance_uri = self._uri_validator(resource_url)
59
+ if not valid_uri:
60
+ raise ValueError("Invalid instance URL")
61
+
62
+ local_scopes = self._resolve_scopes_list(instance_uri, scopes)
63
+ msal_auth_client = self._create_client_application()
64
+
65
+ if isinstance(msal_auth_client, ManagedIdentityClient):
66
+ logger.info("Acquiring token using Managed Identity Client.")
67
+ auth_result_payload = msal_auth_client.acquire_token_for_client(
68
+ resource=resource_url
69
+ )
70
+ elif isinstance(msal_auth_client, ConfidentialClientApplication):
71
+ logger.info("Acquiring token using Confidential Client Application.")
72
+ auth_result_payload = msal_auth_client.acquire_token_for_client(
73
+ scopes=local_scopes
74
+ )
75
+ else:
76
+ auth_result_payload = None
77
+
78
+ res = auth_result_payload.get("access_token") if auth_result_payload else None
79
+ if not res:
80
+ logger.error("Failed to acquire token for resource %s", auth_result_payload)
81
+ raise ValueError(f"Failed to acquire token. {str(auth_result_payload)}")
82
+ return res
83
+
84
+ async def acquire_token_on_behalf_of(
85
+ self, scopes: list[str], user_assertion: str
86
+ ) -> str:
87
+ """
88
+ Acquire a token on behalf of a user.
89
+ :param scopes: The scopes for which to get the token.
90
+ :param user_assertion: The user assertion token.
91
+ :return: The access token as a string.
92
+ """
93
+
94
+ msal_auth_client = self._create_client_application()
95
+ if isinstance(msal_auth_client, ManagedIdentityClient):
96
+ logger.error(
97
+ "Attempted on-behalf-of flow with Managed Identity authentication."
98
+ )
99
+ raise NotImplementedError(
100
+ "On-behalf-of flow is not supported with Managed Identity authentication."
101
+ )
102
+ elif isinstance(msal_auth_client, ConfidentialClientApplication):
103
+ # TODO: Handling token error / acquisition failed
104
+
105
+ token = msal_auth_client.acquire_token_on_behalf_of(
106
+ user_assertion=user_assertion, scopes=scopes
107
+ )
108
+
109
+ if "access_token" not in token:
110
+ logger.error(
111
+ f"Failed to acquire token on behalf of user: {user_assertion}"
112
+ )
113
+ raise ValueError(f"Failed to acquire token. {str(token)}")
114
+
115
+ return token["access_token"]
116
+
117
+ logger.error(
118
+ f"On-behalf-of flow is not supported with the current authentication type: {msal_auth_client.__class__.__name__}"
119
+ )
120
+ raise NotImplementedError(
121
+ f"On-behalf-of flow is not supported with the current authentication type: {msal_auth_client.__class__.__name__}"
122
+ )
123
+
124
+ def _create_client_application(
125
+ self,
126
+ ) -> ManagedIdentityClient | ConfidentialClientApplication:
127
+ msal_auth_client = None
128
+
129
+ if self._msal_configuration.AUTH_TYPE == AuthTypes.user_managed_identity:
130
+ msal_auth_client = ManagedIdentityClient(
131
+ UserAssignedManagedIdentity(
132
+ client_id=self._msal_configuration.CLIENT_ID
133
+ ),
134
+ http_client=Session(),
135
+ )
136
+
137
+ elif self._msal_configuration.AUTH_TYPE == AuthTypes.system_managed_identity:
138
+ msal_auth_client = ManagedIdentityClient(
139
+ SystemAssignedManagedIdentity(),
140
+ http_client=Session(),
141
+ )
142
+ else:
143
+ authority_path = self._msal_configuration.TENANT_ID or "botframework.com"
144
+ authority = f"https://login.microsoftonline.com/{authority_path}"
145
+
146
+ if self._client_credential_cache:
147
+ logger.info("Using cached client credentials for MSAL authentication.")
148
+ pass
149
+ elif self._msal_configuration.AUTH_TYPE == AuthTypes.client_secret:
150
+ self._client_credential_cache = self._msal_configuration.CLIENT_SECRET
151
+ elif self._msal_configuration.AUTH_TYPE == AuthTypes.certificate:
152
+ with open(self._msal_configuration.CERT_KEY_FILE) as file:
153
+ logger.info(
154
+ "Loading certificate private key for MSAL authentication."
155
+ )
156
+ private_key = file.read()
157
+
158
+ with open(self._msal_configuration.CERT_PEM_FILE) as file:
159
+ logger.info("Loading public certificate for MSAL authentication.")
160
+ public_certificate = file.read()
161
+
162
+ # Create an X509 object and calculate the thumbprint
163
+ logger.info("Calculating thumbprint for the public certificate.")
164
+ cert = load_pem_x509_certificate(
165
+ data=bytes(public_certificate, "UTF-8"), backend=default_backend()
166
+ )
167
+ thumbprint = cert.fingerprint(hashes.SHA1()).hex()
168
+
169
+ self._client_credential_cache = {
170
+ "thumbprint": thumbprint,
171
+ "private_key": private_key,
172
+ }
173
+ else:
174
+ logger.error(
175
+ f"Unsupported authentication type: {self._msal_configuration.AUTH_TYPE}"
176
+ )
177
+ raise NotImplementedError("Authentication type not supported")
178
+
179
+ msal_auth_client = ConfidentialClientApplication(
180
+ client_id=self._msal_configuration.CLIENT_ID,
181
+ authority=authority,
182
+ client_credential=self._client_credential_cache,
183
+ )
184
+
185
+ return msal_auth_client
186
+
187
+ @staticmethod
188
+ def _uri_validator(url_str: str) -> tuple[bool, Optional[URI]]:
189
+ try:
190
+ result = urlparse(url_str)
191
+ return all([result.scheme, result.netloc]), result
192
+ except AttributeError:
193
+ logger.error(f"URI parsing error for {url_str}")
194
+ return False, None
195
+
196
+ def _resolve_scopes_list(self, instance_url: URI, scopes=None) -> list[str]:
197
+ if scopes:
198
+ return scopes
199
+
200
+ temp_list: list[str] = []
201
+ for scope in self._msal_configuration.SCOPES:
202
+ scope_placeholder = scope
203
+ if "{instance}" in scope_placeholder.lower():
204
+ scope_placeholder = scope_placeholder.replace(
205
+ "{instance}", f"{instance_url.scheme}://{instance_url.hostname}"
206
+ )
207
+ temp_list.append(scope_placeholder)
208
+ logger.debug(f"Resolved scopes: {temp_list}")
209
+ return temp_list
210
+
211
+ # the call to MSAL is blocking, but in the future we want to create an asyncio task
212
+ # to avoid this
213
+ async def get_agentic_application_token(
214
+ self, agent_app_instance_id: str
215
+ ) -> Optional[str]:
216
+ """Gets the agentic application token for the given agent application instance ID.
217
+
218
+ :param agent_app_instance_id: The agent application instance ID.
219
+ :type agent_app_instance_id: str
220
+ :return: The agentic application token, or None if not found.
221
+ :rtype: Optional[str]
222
+ """
223
+
224
+ if not agent_app_instance_id:
225
+ raise ValueError("Agent application instance Id must be provided.")
226
+
227
+ logger.info(
228
+ "Attempting to get agentic application token from agent_app_instance_id %s",
229
+ agent_app_instance_id,
230
+ )
231
+ msal_auth_client = self._create_client_application()
232
+
233
+ if isinstance(msal_auth_client, ConfidentialClientApplication):
234
+
235
+ # https://github.dev/AzureAD/microsoft-authentication-library-for-dotnet
236
+ auth_result_payload = msal_auth_client.acquire_token_for_client(
237
+ ["api://AzureAdTokenExchange/.default"],
238
+ data={"fmi_path": agent_app_instance_id},
239
+ )
240
+
241
+ if auth_result_payload:
242
+ return auth_result_payload.get("access_token")
243
+
244
+ return None
245
+
246
+ async def get_agentic_instance_token(
247
+ self, agent_app_instance_id: str
248
+ ) -> tuple[str, str]:
249
+ """Gets the agentic instance token for the given agent application instance ID.
250
+
251
+ :param agent_app_instance_id: The agent application instance ID.
252
+ :type agent_app_instance_id: str
253
+ :return: A tuple containing the agentic instance token and the agent application token.
254
+ :rtype: tuple[str, str]
255
+ """
256
+
257
+ if not agent_app_instance_id:
258
+ raise ValueError("Agent application instance Id must be provided.")
259
+
260
+ logger.info(
261
+ "Attempting to get agentic instance token from agent_app_instance_id %s",
262
+ agent_app_instance_id,
263
+ )
264
+ agent_token_result = await self.get_agentic_application_token(
265
+ agent_app_instance_id
266
+ )
267
+
268
+ if not agent_token_result:
269
+ logger.error(
270
+ "Failed to acquire agentic instance token or agent token for agent_app_instance_id %s",
271
+ agent_app_instance_id,
272
+ )
273
+ raise Exception(
274
+ f"Failed to acquire agentic instance token or agent token for agent_app_instance_id {agent_app_instance_id}"
275
+ )
276
+
277
+ authority = (
278
+ f"https://login.microsoftonline.com/{self._msal_configuration.TENANT_ID}"
279
+ )
280
+
281
+ instance_app = ConfidentialClientApplication(
282
+ client_id=agent_app_instance_id,
283
+ authority=authority,
284
+ client_credential={"client_assertion": agent_token_result},
285
+ )
286
+
287
+ agentic_instance_token = instance_app.acquire_token_for_client(
288
+ ["api://AzureAdTokenExchange/.default"]
289
+ )
290
+
291
+ if not agentic_instance_token:
292
+ logger.error(
293
+ "Failed to acquire agentic instance token or agent token for agent_app_instance_id %s",
294
+ agent_app_instance_id,
295
+ )
296
+ raise Exception(
297
+ f"Failed to acquire agentic instance token or agent token for agent_app_instance_id {agent_app_instance_id}"
298
+ )
299
+
300
+ # future scenario where we don't know the blueprint id upfront
301
+
302
+ token = agentic_instance_token.get("access_token")
303
+ if not token:
304
+ logger.error(
305
+ "Failed to acquire agentic instance token, %s", agentic_instance_token
306
+ )
307
+ raise ValueError(f"Failed to acquire token. {str(agentic_instance_token)}")
308
+
309
+ logger.debug(_DeferredLogOfBlueprintId(token))
310
+
311
+ return agentic_instance_token["access_token"], agent_token_result
312
+
313
+ async def get_agentic_user_token(
314
+ self, agent_app_instance_id: str, upn: str, scopes: list[str]
315
+ ) -> Optional[str]:
316
+ """Gets the agentic user token for the given agent application instance ID and user principal name and the scopes.
317
+
318
+ :param agent_app_instance_id: The agent application instance ID.
319
+ :type agent_app_instance_id: str
320
+ :param upn: The user principal name.
321
+ :type upn: str
322
+ :param scopes: The scopes to request for the token.
323
+ :type scopes: list[str]
324
+ :return: The agentic user token, or None if not found.
325
+ :rtype: Optional[str]
326
+ """
327
+ if not agent_app_instance_id or not upn:
328
+ raise ValueError(
329
+ "Agent application instance Id and user principal name must be provided."
330
+ )
331
+
332
+ logger.info(
333
+ "Attempting to get agentic user token from agent_app_instance_id %s and upn %s",
334
+ agent_app_instance_id,
335
+ upn,
336
+ )
337
+ instance_token, agent_token = await self.get_agentic_instance_token(
338
+ agent_app_instance_id
339
+ )
340
+
341
+ if not instance_token or not agent_token:
342
+ logger.error(
343
+ "Failed to acquire instance token or agent token for agent_app_instance_id %s and upn %s",
344
+ agent_app_instance_id,
345
+ upn,
346
+ )
347
+ raise Exception(
348
+ f"Failed to acquire instance token or agent token for agent_app_instance_id {agent_app_instance_id} and upn {upn}"
349
+ )
350
+
351
+ authority = (
352
+ f"https://login.microsoftonline.com/{self._msal_configuration.TENANT_ID}"
353
+ )
354
+
355
+ instance_app = ConfidentialClientApplication(
356
+ client_id=agent_app_instance_id,
357
+ authority=authority,
358
+ client_credential={"client_assertion": agent_token},
359
+ )
360
+
361
+ logger.info(
362
+ "Acquiring agentic user token for agent_app_instance_id %s and upn %s",
363
+ agent_app_instance_id,
364
+ upn,
365
+ )
366
+ auth_result_payload = instance_app.acquire_token_for_client(
367
+ scopes,
368
+ data={
369
+ "username": upn,
370
+ "user_federated_identity_credential": instance_token,
371
+ "grant_type": "user_fic",
372
+ },
373
+ )
374
+
375
+ if not auth_result_payload:
376
+ logger.error(
377
+ "Failed to acquire agentic user token for agent_app_instance_id %s and upn %s, %s",
378
+ agent_app_instance_id,
379
+ upn,
380
+ auth_result_payload,
381
+ )
382
+ return None
383
+
384
+ access_token = auth_result_payload.get("access_token")
385
+ if not access_token:
386
+ logger.error(
387
+ "Failed to acquire agentic user token for agent_app_instance_id %s and upn %s, %s",
388
+ agent_app_instance_id,
389
+ upn,
390
+ auth_result_payload,
391
+ )
392
+ return None
393
+
394
+ logger.info("Acquired agentic user token response.")
395
+ return access_token
@@ -0,0 +1,134 @@
1
+ # Copyright (c) Microsoft Corporation. All rights reserved.
2
+ # Licensed under the MIT License.
3
+
4
+ import re
5
+ from typing import Dict, List, Optional
6
+ from microsoft_agents.hosting.core import (
7
+ AgentAuthConfiguration,
8
+ AccessTokenProviderBase,
9
+ ClaimsIdentity,
10
+ Connections,
11
+ )
12
+
13
+ from .msal_auth import MsalAuth
14
+
15
+
16
+ class MsalConnectionManager(Connections):
17
+ _connections: Dict[str, MsalAuth]
18
+ _connections_map: List[Dict[str, str]]
19
+ _service_connection_configuration: AgentAuthConfiguration
20
+
21
+ def __init__(
22
+ self,
23
+ connections_configurations: Optional[Dict[str, AgentAuthConfiguration]] = None,
24
+ connections_map: Optional[List[Dict[str, str]]] = None,
25
+ **kwargs,
26
+ ):
27
+ """
28
+ Initialize the MSAL connection manager.
29
+
30
+ :arg connections_configurations: A dictionary of connection configurations.
31
+ :type connections_configurations: Dict[str, AgentAuthConfiguration]
32
+ :arg connections_map: A list of connection mappings.
33
+ :type connections_map: List[Dict[str, str]]
34
+ :raises ValueError: If no service connection configuration is provided.
35
+ """
36
+
37
+ self._connections: Dict[str, MsalAuth] = {}
38
+ self._connections_map = connections_map or kwargs.get("CONNECTIONSMAP", {})
39
+ self._service_connection_configuration: AgentAuthConfiguration = None
40
+
41
+ if connections_configurations:
42
+ for (
43
+ connection_name,
44
+ connection_settings,
45
+ ) in connections_configurations.items():
46
+ self._connections[connection_name] = MsalAuth(
47
+ AgentAuthConfiguration(**connection_settings)
48
+ )
49
+ else:
50
+ raw_configurations: Dict[str, Dict] = kwargs.get("CONNECTIONS", {})
51
+ for connection_name, connection_settings in raw_configurations.items():
52
+ parsed_configuration = AgentAuthConfiguration(
53
+ **connection_settings.get("SETTINGS", {})
54
+ )
55
+ self._connections[connection_name] = MsalAuth(parsed_configuration)
56
+ if connection_name == "SERVICE_CONNECTION":
57
+ self._service_connection_configuration = parsed_configuration
58
+
59
+ if not self._connections.get("SERVICE_CONNECTION", None):
60
+ raise ValueError("No service connection configuration provided.")
61
+
62
+ def get_connection(self, connection_name: Optional[str]) -> AccessTokenProviderBase:
63
+ """
64
+ Get the OAuth connection for the agent.
65
+
66
+ :arg connection_name: The name of the connection.
67
+ :type connection_name: str
68
+ :return: The OAuth connection for the agent.
69
+ :rtype: AccessTokenProviderBase
70
+ """
71
+ # should never be None
72
+ return self._connections.get(connection_name, None)
73
+
74
+ def get_default_connection(self) -> AccessTokenProviderBase:
75
+ """
76
+ Get the default OAuth connection for the agent.
77
+ """
78
+ # should never be None
79
+ return self._connections.get("SERVICE_CONNECTION", None)
80
+
81
+ def get_token_provider(
82
+ self, claims_identity: ClaimsIdentity, service_url: str
83
+ ) -> AccessTokenProviderBase:
84
+ """
85
+ Get the OAuth token provider for the agent.
86
+
87
+ :arg claims_identity: The claims identity of the bot.
88
+ :type claims_identity: ClaimsIdentity
89
+ :arg service_url: The service URL of the bot.
90
+ :type service_url: str
91
+ :return: The OAuth token provider for the agent.
92
+ :rtype: AccessTokenProviderBase
93
+ :raises ValueError: If no connection is found for the given audience and service URL.
94
+ """
95
+ if not claims_identity or not service_url:
96
+ raise ValueError(
97
+ "Claims identity and Service URL are required to get the token provider."
98
+ )
99
+
100
+ if not self._connections_map:
101
+ return self.get_default_connection()
102
+
103
+ aud = claims_identity.get_app_id() or ""
104
+ for item in self._connections_map:
105
+ audience_match = True
106
+ item_aud = item.get("AUDIENCE", "")
107
+ if item_aud:
108
+ audience_match = item_aud.lower() == aud.lower()
109
+
110
+ if audience_match:
111
+ item_service_url = item.get("SERVICEURL", "")
112
+ if item_service_url == "*" or item_service_url == "":
113
+ connection_name = item.get("CONNECTION")
114
+ connection = self.get_connection(connection_name)
115
+ if connection:
116
+ return connection
117
+
118
+ else:
119
+ res = re.match(item_service_url, service_url, re.IGNORECASE)
120
+ if res:
121
+ connection_name = item.get("CONNECTION")
122
+ connection = self.get_connection(connection_name)
123
+ if connection:
124
+ return connection
125
+
126
+ raise ValueError(
127
+ f"No connection found for audience '{aud}' and serviceUrl '{service_url}'."
128
+ )
129
+
130
+ def get_default_connection_configuration(self) -> AgentAuthConfiguration:
131
+ """
132
+ Get the default connection configuration for the agent.
133
+ """
134
+ return self._service_connection_configuration
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microsoft-agents-authentication-msal
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: A msal-based authentication library for Microsoft Agents
5
5
  Author: Microsoft Corporation
6
6
  Project-URL: Homepage, https://github.com/microsoft/Agents
@@ -8,7 +8,7 @@ Classifier: Programming Language :: Python :: 3
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Operating System :: OS Independent
10
10
  Requires-Python: >=3.9
11
- Requires-Dist: microsoft-agents-hosting-core==0.3.2
11
+ Requires-Dist: microsoft-agents-hosting-core==0.4.0
12
12
  Requires-Dist: msal>=1.31.1
13
13
  Requires-Dist: requests>=2.32.3
14
14
  Requires-Dist: cryptography>=44.0.0
@@ -7,6 +7,4 @@ microsoft_agents_authentication_msal.egg-info/PKG-INFO
7
7
  microsoft_agents_authentication_msal.egg-info/SOURCES.txt
8
8
  microsoft_agents_authentication_msal.egg-info/dependency_links.txt
9
9
  microsoft_agents_authentication_msal.egg-info/requires.txt
10
- microsoft_agents_authentication_msal.egg-info/top_level.txt
11
- tests/test_msal_auth.py
12
- tests/test_msal_connection_manager.py
10
+ microsoft_agents_authentication_msal.egg-info/top_level.txt
@@ -1,4 +1,4 @@
1
- microsoft-agents-hosting-core==0.3.2
1
+ microsoft-agents-hosting-core==0.4.0
2
2
  msal>=1.31.1
3
3
  requests>=2.32.3
4
4
  cryptography>=44.0.0
@@ -1,188 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from typing import Optional
5
- from urllib.parse import urlparse, ParseResult as URI
6
- from msal import (
7
- ConfidentialClientApplication,
8
- ManagedIdentityClient,
9
- UserAssignedManagedIdentity,
10
- SystemAssignedManagedIdentity,
11
- )
12
- from requests import Session
13
- from cryptography.x509 import load_pem_x509_certificate
14
- from cryptography.hazmat.backends import default_backend
15
- from cryptography.hazmat.primitives import hashes
16
-
17
- from microsoft_agents.hosting.core import (
18
- AuthTypes,
19
- AccessTokenProviderBase,
20
- AgentAuthConfiguration,
21
- )
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
-
26
- class MsalAuth(AccessTokenProviderBase):
27
-
28
- _client_credential_cache = None
29
-
30
- def __init__(self, msal_configuration: AgentAuthConfiguration):
31
- self._msal_configuration = msal_configuration
32
- logger.debug(
33
- f"Initializing MsalAuth with configuration: {self._msal_configuration}"
34
- )
35
-
36
- async def get_access_token(
37
- self, resource_url: str, scopes: list[str], force_refresh: bool = False
38
- ) -> str:
39
- logger.debug(
40
- f"Requesting access token for resource: {resource_url}, scopes: {scopes}"
41
- )
42
- valid_uri, instance_uri = self._uri_validator(resource_url)
43
- if not valid_uri:
44
- raise ValueError("Invalid instance URL")
45
-
46
- local_scopes = self._resolve_scopes_list(instance_uri, scopes)
47
- msal_auth_client = self._create_client_application()
48
-
49
- if isinstance(msal_auth_client, ManagedIdentityClient):
50
- logger.info("Acquiring token using Managed Identity Client.")
51
- auth_result_payload = msal_auth_client.acquire_token_for_client(
52
- resource=resource_url
53
- )
54
- elif isinstance(msal_auth_client, ConfidentialClientApplication):
55
- logger.info("Acquiring token using Confidential Client Application.")
56
- auth_result_payload = msal_auth_client.acquire_token_for_client(
57
- scopes=local_scopes
58
- )
59
-
60
- # TODO: Handling token error / acquisition failed
61
- return auth_result_payload["access_token"]
62
-
63
- async def aquire_token_on_behalf_of(
64
- self, scopes: list[str], user_assertion: str
65
- ) -> str:
66
- """
67
- Acquire a token on behalf of a user.
68
- :param scopes: The scopes for which to get the token.
69
- :param user_assertion: The user assertion token.
70
- :return: The access token as a string.
71
- """
72
-
73
- msal_auth_client = self._create_client_application()
74
- if isinstance(msal_auth_client, ManagedIdentityClient):
75
- logger.error(
76
- "Attempted on-behalf-of flow with Managed Identity authentication."
77
- )
78
- raise NotImplementedError(
79
- "On-behalf-of flow is not supported with Managed Identity authentication."
80
- )
81
- elif isinstance(msal_auth_client, ConfidentialClientApplication):
82
- # TODO: Handling token error / acquisition failed
83
-
84
- token = msal_auth_client.acquire_token_on_behalf_of(
85
- user_assertion=user_assertion, scopes=scopes
86
- )
87
-
88
- if "access_token" not in token:
89
- logger.error(
90
- f"Failed to acquire token on behalf of user: {user_assertion}"
91
- )
92
- raise ValueError(f"Failed to acquire token. {str(token)}")
93
-
94
- return token["access_token"]
95
-
96
- logger.error(
97
- f"On-behalf-of flow is not supported with the current authentication type: {msal_auth_client.__class__.__name__}"
98
- )
99
- raise NotImplementedError(
100
- f"On-behalf-of flow is not supported with the current authentication type: {msal_auth_client.__class__.__name__}"
101
- )
102
-
103
- def _create_client_application(
104
- self,
105
- ) -> ManagedIdentityClient | ConfidentialClientApplication:
106
- msal_auth_client = None
107
-
108
- if self._msal_configuration.AUTH_TYPE == AuthTypes.user_managed_identity:
109
- msal_auth_client = ManagedIdentityClient(
110
- UserAssignedManagedIdentity(
111
- client_id=self._msal_configuration.CLIENT_ID
112
- ),
113
- http_client=Session(),
114
- )
115
-
116
- elif self._msal_configuration.AUTH_TYPE == AuthTypes.system_managed_identity:
117
- msal_auth_client = ManagedIdentityClient(
118
- SystemAssignedManagedIdentity(),
119
- http_client=Session(),
120
- )
121
- else:
122
- authority_path = self._msal_configuration.TENANT_ID or "botframework.com"
123
- authority = f"https://login.microsoftonline.com/{authority_path}"
124
-
125
- if self._client_credential_cache:
126
- logger.info("Using cached client credentials for MSAL authentication.")
127
- pass
128
- elif self._msal_configuration.AUTH_TYPE == AuthTypes.client_secret:
129
- self._client_credential_cache = self._msal_configuration.CLIENT_SECRET
130
- elif self._msal_configuration.AUTH_TYPE == AuthTypes.certificate:
131
- with open(self._msal_configuration.CERT_KEY_FILE) as file:
132
- logger.info(
133
- "Loading certificate private key for MSAL authentication."
134
- )
135
- private_key = file.read()
136
-
137
- with open(self._msal_configuration.CERT_PEM_FILE) as file:
138
- logger.info("Loading public certificate for MSAL authentication.")
139
- public_certificate = file.read()
140
-
141
- # Create an X509 object and calculate the thumbprint
142
- logger.info("Calculating thumbprint for the public certificate.")
143
- cert = load_pem_x509_certificate(
144
- data=bytes(public_certificate, "UTF-8"), backend=default_backend()
145
- )
146
- thumbprint = cert.fingerprint(hashes.SHA1()).hex()
147
-
148
- self._client_credential_cache = {
149
- "thumbprint": thumbprint,
150
- "private_key": private_key,
151
- }
152
- else:
153
- logger.error(
154
- f"Unsupported authentication type: {self._msal_configuration.AUTH_TYPE}"
155
- )
156
- raise NotImplementedError("Authentication type not supported")
157
-
158
- msal_auth_client = ConfidentialClientApplication(
159
- client_id=self._msal_configuration.CLIENT_ID,
160
- authority=authority,
161
- client_credential=self._client_credential_cache,
162
- )
163
-
164
- return msal_auth_client
165
-
166
- @staticmethod
167
- def _uri_validator(url_str: str) -> tuple[bool, Optional[URI]]:
168
- try:
169
- result = urlparse(url_str)
170
- return all([result.scheme, result.netloc]), result
171
- except AttributeError:
172
- logger.error(f"URI parsing error for {url_str}")
173
- return False, None
174
-
175
- def _resolve_scopes_list(self, instance_url: URI, scopes=None) -> list[str]:
176
- if scopes:
177
- return scopes
178
-
179
- temp_list: list[str] = []
180
- for scope in self._msal_configuration.SCOPES:
181
- scope_placeholder = scope
182
- if "{instance}" in scope_placeholder.lower():
183
- scope_placeholder = scope_placeholder.replace(
184
- "{instance}", f"{instance_url.scheme}://{instance_url.hostname}"
185
- )
186
- temp_list.append(scope_placeholder)
187
- logger.debug(f"Resolved scopes: {temp_list}")
188
- return temp_list
@@ -1,72 +0,0 @@
1
- from typing import Dict, List, Optional
2
- from microsoft_agents.hosting.core import (
3
- AgentAuthConfiguration,
4
- AccessTokenProviderBase,
5
- ClaimsIdentity,
6
- Connections,
7
- )
8
-
9
- from .msal_auth import MsalAuth
10
-
11
-
12
- class MsalConnectionManager(Connections):
13
-
14
- def __init__(
15
- self,
16
- connections_configurations: Dict[str, AgentAuthConfiguration] = None,
17
- connections_map: List[Dict[str, str]] = None,
18
- **kwargs
19
- ):
20
- self._connections: Dict[str, MsalAuth] = {}
21
- self._connections_map = connections_map or kwargs.get("CONNECTIONS_MAP", {})
22
- self._service_connection_configuration: AgentAuthConfiguration = None
23
-
24
- if connections_configurations:
25
- for (
26
- connection_name,
27
- connection_settings,
28
- ) in connections_configurations.items():
29
- self._connections[connection_name] = MsalAuth(
30
- AgentAuthConfiguration(**connection_settings)
31
- )
32
- else:
33
- raw_configurations: Dict[str, Dict] = kwargs.get("CONNECTIONS", {})
34
- for connection_name, connection_settings in raw_configurations.items():
35
- parsed_configuration = AgentAuthConfiguration(
36
- **connection_settings.get("SETTINGS", {})
37
- )
38
- self._connections[connection_name] = MsalAuth(parsed_configuration)
39
- if connection_name == "SERVICE_CONNECTION":
40
- self._service_connection_configuration = parsed_configuration
41
-
42
- if not self._connections.get("SERVICE_CONNECTION", None):
43
- raise ValueError("No service connection configuration provided.")
44
-
45
- def get_connection(self, connection_name: Optional[str]) -> AccessTokenProviderBase:
46
- """
47
- Get the OAuth connection for the agent.
48
- """
49
- return self._connections.get(connection_name, None)
50
-
51
- def get_default_connection(self) -> AccessTokenProviderBase:
52
- """
53
- Get the default OAuth connection for the agent.
54
- """
55
- return self._connections.get("SERVICE_CONNECTION", None)
56
-
57
- def get_token_provider(
58
- self, claims_identity: ClaimsIdentity, service_url: str
59
- ) -> AccessTokenProviderBase:
60
- """
61
- Get the OAuth token provider for the agent.
62
- """
63
- if not self._connections_map:
64
- return self.get_default_connection()
65
-
66
- # TODO: Implement logic to select the appropriate connection based on the connection map
67
-
68
- def get_default_connection_configuration(self) -> AgentAuthConfiguration:
69
- """
70
- Get the default connection configuration for the agent.
71
- """
72
- return self._service_connection_configuration
@@ -1,83 +0,0 @@
1
- import unittest
2
- from unittest.mock import Mock
3
- import pytest
4
- from msal import ManagedIdentityClient, ConfidentialClientApplication
5
- from microsoft_agents.authentication.msal import MsalAuth
6
- from microsoft_agents.hosting.core.authorization import AgentAuthConfiguration
7
-
8
-
9
- class TestingMsalAuth(MsalAuth):
10
- """
11
- Mock object for MsalAuth
12
- """
13
-
14
- def __init__(self, client_type):
15
- super().__init__(AgentAuthConfiguration())
16
- mock_client = Mock(spec=client_type)
17
-
18
- mock_client.acquire_token_for_client = Mock(
19
- return_value={"access_token": "token"}
20
- )
21
- mock_client.acquire_token_on_behalf_of = Mock(
22
- return_value={"access_token": "token"}
23
- )
24
- self.mock_client = mock_client
25
-
26
- self._create_client_application = Mock(return_value=self.mock_client)
27
-
28
-
29
- class TestMsalAuth:
30
- """
31
- Test suite for testing MsalAuth functionality
32
- """
33
-
34
- @pytest.mark.asyncio
35
- async def test_get_access_token_managed_identity(self):
36
- mock_auth = TestingMsalAuth(ManagedIdentityClient)
37
- token = await mock_auth.get_access_token(
38
- "https://test.api.botframework.com", scopes=["test-scope"]
39
- )
40
-
41
- assert token == "token"
42
- mock_auth.mock_client.acquire_token_for_client.assert_called_with(
43
- resource="https://test.api.botframework.com"
44
- )
45
-
46
- @pytest.mark.asyncio
47
- async def test_get_access_token_confidential(self):
48
- mock_auth = TestingMsalAuth(ConfidentialClientApplication)
49
- token = await mock_auth.get_access_token(
50
- "https://test.api.botframework.com", scopes=["test-scope"]
51
- )
52
-
53
- assert token == "token"
54
- mock_auth.mock_client.acquire_token_for_client.assert_called_with(
55
- scopes=["test-scope"]
56
- )
57
-
58
- @pytest.mark.asyncio
59
- async def test_aquire_token_on_behalf_of_managed_identity(self):
60
- mock_auth = TestingMsalAuth(ManagedIdentityClient)
61
-
62
- try:
63
- await mock_auth.aquire_token_on_behalf_of(
64
- scopes=["test-scope"], user_assertion="test-assertion"
65
- )
66
- except NotImplementedError:
67
- assert True
68
- else:
69
- assert False
70
-
71
- @pytest.mark.asyncio
72
- async def test_aquire_token_on_behalf_of_confidential(self):
73
- mock_auth = TestingMsalAuth(ConfidentialClientApplication)
74
- mock_auth._create_client_application = Mock(return_value=mock_auth.mock_client)
75
-
76
- token = await mock_auth.aquire_token_on_behalf_of(
77
- scopes=["test-scope"], user_assertion="test-assertion"
78
- )
79
-
80
- assert token == "token"
81
- mock_auth.mock_client.acquire_token_on_behalf_of.assert_called_with(
82
- scopes=["test-scope"], user_assertion="test-assertion"
83
- )
@@ -1,35 +0,0 @@
1
- from os import environ
2
- from microsoft_agents.activity import load_configuration_from_env
3
- from microsoft_agents.hosting.core import AuthTypes
4
- from microsoft_agents.authentication.msal import MsalConnectionManager
5
-
6
-
7
- class TestMsalConnectionManager:
8
- """
9
- Test suite for the Msal Connection Manager
10
- """
11
-
12
- def test_msal_connection_manager(self):
13
- mock_environ = {
14
- **environ,
15
- "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID": "test-tenant-id-SERVICE_CONNECTION",
16
- "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID": "test-client-id-SERVICE_CONNECTION",
17
- "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET": "test-client-secret-SERVICE_CONNECTION",
18
- "CONNECTIONS__MCS__SETTINGS__TENANTID": "test-tenant-id-MCS",
19
- "CONNECTIONS__MCS__SETTINGS__CLIENTID": "test-client-id-MCS",
20
- "CONNECTIONS__MCS__SETTINGS__CLIENTSECRET": "test-client-secret-MCS",
21
- }
22
-
23
- config = load_configuration_from_env(mock_environ)
24
- connection_manager = MsalConnectionManager(**config)
25
- for key in connection_manager._connections:
26
- auth = connection_manager.get_connection(key)._msal_configuration
27
- assert auth.AUTH_TYPE == AuthTypes.client_secret
28
- assert auth.CLIENT_ID == f"test-client-id-{key}"
29
- assert auth.TENANT_ID == f"test-tenant-id-{key}"
30
- assert auth.CLIENT_SECRET == f"test-client-secret-{key}"
31
- assert auth.ISSUERS == [
32
- "https://api.botframework.com",
33
- f"https://sts.windows.net/test-tenant-id-{key}/",
34
- f"https://login.microsoftonline.com/test-tenant-id-{key}/v2.0",
35
- ]