From ea3ecdb06a842336095114f5acd591343d9e5c2a Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 1 Apr 2026 10:59:41 -0700 Subject: [PATCH 1/4] Using PFX files for cert-based auth --- .../authentication/msal/msal_auth.py | 23 +--------------- .../setup.py | 1 - .../authorization/agent_auth_configuration.py | 27 ++++++++----------- tests/hosting_core/test_auth_configuration.py | 9 +++---- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py index 8c46a7f9..50d97e7b 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py @@ -17,9 +17,6 @@ TokenCache, ) from requests import Session -from cryptography.x509 import load_pem_x509_certificate -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes from microsoft_agents.activity._utils import _DeferredString @@ -212,26 +209,8 @@ def _create_client_application( elif self._msal_configuration.AUTH_TYPE == AuthTypes.client_secret: self._client_credential_cache = self._msal_configuration.CLIENT_SECRET elif self._msal_configuration.AUTH_TYPE == AuthTypes.certificate: - with open(self._msal_configuration.CERT_KEY_FILE) as file: - logger.info( - "Loading certificate private key for MSAL authentication." - ) - private_key = file.read() - - with open(self._msal_configuration.CERT_PEM_FILE) as file: - logger.info("Loading public certificate for MSAL authentication.") - public_certificate = file.read() - - # Create an X509 object and calculate the thumbprint - logger.info("Calculating thumbprint for the public certificate.") - cert = load_pem_x509_certificate( - data=bytes(public_certificate, "UTF-8"), backend=default_backend() - ) - thumbprint = cert.fingerprint(hashes.SHA1()).hex() - self._client_credential_cache = { - "thumbprint": thumbprint, - "private_key": private_key, + "private_key_pfx_path": self._msal_configuration.CERT_PFX_FILE, } else: logger.error( diff --git a/libraries/microsoft-agents-authentication-msal/setup.py b/libraries/microsoft-agents-authentication-msal/setup.py index b68c2141..cc90cd35 100644 --- a/libraries/microsoft-agents-authentication-msal/setup.py +++ b/libraries/microsoft-agents-authentication-msal/setup.py @@ -15,6 +15,5 @@ f"microsoft-agents-hosting-core=={package_version}", "msal>=1.34.0", "requests>=2.32.3", - "cryptography>=44.0.0", ], ) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py index dfccfde0..0042ba87 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py @@ -15,8 +15,7 @@ class AgentAuthConfiguration: CLIENT_ID: The client ID for the Azure AD application. AUTH_TYPE: The type of authentication to use (microsoft_agents.hosting.core.authorization.auth_types.AuthTypes). CLIENT_SECRET: The client secret for the Azure AD application (if using client secret authentication). - CERT_PEM_FILE: The path to the PEM file for certificate authentication (if using certificate authentication). - CERT_KEY_FILE: The path to the key file for certificate authentication (if using certificate authentication). + CERT_PFX_FILE: The path to the PFX certificate file (if using certificate authentication). CONNECTION_NAME: The name of the connection SCOPES: The scopes to request AUTHORITY: The authority URL for the Azure AD (if different from the default).f @@ -24,11 +23,9 @@ class AgentAuthConfiguration: """ TENANT_ID: Optional[str] - CLIENT_ID: Optional[str] AUTH_TYPE: AuthTypes CLIENT_SECRET: Optional[str] - CERT_PEM_FILE: Optional[str] - CERT_KEY_FILE: Optional[str] + CERT_PFX_FILE: Optional[str] CONNECTION_NAME: Optional[str] SCOPES: Optional[list[str]] AUTHORITY: Optional[str] @@ -46,15 +43,14 @@ class AgentAuthConfiguration: def __init__( self, - auth_type: AuthTypes = None, - client_id: str = None, - tenant_id: Optional[str] = None, - client_secret: Optional[str] = None, - cert_pem_file: Optional[str] = None, - cert_key_file: Optional[str] = None, - connection_name: Optional[str] = None, - authority: Optional[str] = None, - scopes: Optional[list[str]] = None, + auth_type: AuthTypes | None = None, + client_id: str | None = None, + tenant_id: str | None = None, + client_secret: str | None = None, + cert_pfx_file: str | None = None, + connection_name: str | None = None, + authority: str | None = None, + scopes: list[str] | None = None, anonymous_allowed: bool = False, **kwargs: Optional[dict[str, str]], ): @@ -64,8 +60,7 @@ def __init__( self.AUTHORITY = authority or kwargs.get("AUTHORITY", None) self.TENANT_ID = tenant_id or kwargs.get("TENANTID", None) self.CLIENT_SECRET = client_secret or kwargs.get("CLIENTSECRET", None) - self.CERT_PEM_FILE = cert_pem_file or kwargs.get("CERTPEMFILE", None) - self.CERT_KEY_FILE = cert_key_file or kwargs.get("CERTKEYFILE", None) + self.CERT_PFX_FILE = cert_pfx_file or kwargs.get("CERTPFXFILE", None) self.CONNECTION_NAME = connection_name or kwargs.get("CONNECTIONNAME", None) self.SCOPES = scopes or kwargs.get("SCOPES", None) self.ALT_BLUEPRINT_ID = kwargs.get("ALT_BLUEPRINT_NAME", None) diff --git a/tests/hosting_core/test_auth_configuration.py b/tests/hosting_core/test_auth_configuration.py index 59cf9804..e8f5be35 100644 --- a/tests/hosting_core/test_auth_configuration.py +++ b/tests/hosting_core/test_auth_configuration.py @@ -16,8 +16,7 @@ def test_auth_configuration_basic(self): tenant_id="test-tenant-id", client_id="test-client-id", client_secret="test-client-secret", - cert_pem_file="test-cert.pem", - cert_key_file="test-cert.key", + cert_pfx_file="test-cert.pfx", connection_name="test-connection", authority="https://login.microsoftonline.com", scopes=["test-scope-1", "test-scope-2"], @@ -27,8 +26,7 @@ def test_auth_configuration_basic(self): assert auth_config.TENANT_ID == "test-tenant-id" assert auth_config.CLIENT_ID == "test-client-id" assert auth_config.CLIENT_SECRET == "test-client-secret" - assert auth_config.CERT_PEM_FILE == "test-cert.pem" - assert auth_config.CERT_KEY_FILE == "test-cert.key" + assert auth_config.CERT_PFX_FILE == "test-cert.pfx" assert auth_config.CONNECTION_NAME == "test-connection" assert auth_config.AUTHORITY == "https://login.microsoftonline.com" assert auth_config.SCOPES == ["test-scope-1", "test-scope-2"] @@ -72,8 +70,7 @@ def test_empty_settings(self): assert auth_config.TENANT_ID == None assert auth_config.CLIENT_ID == None assert auth_config.CLIENT_SECRET == None - assert auth_config.CERT_PEM_FILE == None - assert auth_config.CERT_KEY_FILE == None + assert auth_config.CERT_PFX_FILE == None assert auth_config.CONNECTION_NAME == None assert auth_config.AUTHORITY == None assert auth_config.SCOPES == None From 35dfe8f203db98347322eef4ee0190762cab4922 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Thu, 2 Apr 2026 13:34:54 -0700 Subject: [PATCH 2/4] Adding MI with FIC auth type handling --- .../authentication/msal/msal_auth.py | 31 +++++++++++++------ .../authorization/agent_auth_configuration.py | 22 +++++++------ .../hosting/core/authorization/auth_types.py | 1 + tests/hosting_core/test_auth_configuration.py | 17 +++++----- 4 files changed, 46 insertions(+), 25 deletions(-) diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py index 50d97e7b..771fc524 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py @@ -41,8 +41,6 @@ async def _async_acquire_token_for_client(msal_auth_client, *args, **kwargs): class MsalAuth(AccessTokenProviderBase): - _client_credential_cache = None - def __init__(self, msal_configuration: AgentAuthConfiguration): """Initializes the MsalAuth class with the given configuration. @@ -202,16 +200,31 @@ def _create_client_application( ) else: authority = MsalAuth._resolve_authority(self._msal_configuration, tenant_id) + client_credential = None - if self._client_credential_cache: - logger.info("Using cached client credentials for MSAL authentication.") - pass - elif self._msal_configuration.AUTH_TYPE == AuthTypes.client_secret: - self._client_credential_cache = self._msal_configuration.CLIENT_SECRET + if self._msal_configuration.AUTH_TYPE == AuthTypes.client_secret: + client_credential = self._msal_configuration.CLIENT_SECRET elif self._msal_configuration.AUTH_TYPE == AuthTypes.certificate: - self._client_credential_cache = { + client_credential = { "private_key_pfx_path": self._msal_configuration.CERT_PFX_FILE, } + elif self._msal_configuration.AUTH_TYPE == AuthTypes.federated_credentials: + mi_client = ManagedIdentityClient( + UserAssignedManagedIdentity(client_id=self._msal_configuration.FEDERATED_CLIENT_ID), + http_client=Session(), + ) + def get_assertion() -> str: + result = mi_client.acquire_token_for_client(resource="api://AzureADTokenExchange") + if "access_token" not in result: + logger.error( + f"Failed to acquire token for federated credentials: {result}" + ) + raise ValueError( + authentication_errors.FailedToAcquireToken.format(str(result)) + ) + return result["access_token"] + + client_credential = {"client_assertion": get_assertion} else: logger.error( f"Unsupported authentication type: {self._msal_configuration.AUTH_TYPE}" @@ -223,7 +236,7 @@ def _create_client_application( return ConfidentialClientApplication( client_id=self._msal_configuration.CLIENT_ID, authority=authority, - client_credential=self._client_credential_cache, + client_credential=client_credential ) def _client_rep( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py index 0042ba87..efd8f584 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py @@ -18,18 +18,20 @@ class AgentAuthConfiguration: CERT_PFX_FILE: The path to the PFX certificate file (if using certificate authentication). CONNECTION_NAME: The name of the connection SCOPES: The scopes to request - AUTHORITY: The authority URL for the Azure AD (if different from the default).f + AUTHORITY: The authority URL for the Azure AD (if different from the default). ALT_BLUEPRINT_ID: An optional alternative blueprint ID used when constructing a connector client. """ - TENANT_ID: Optional[str] + TENANT_ID: str | None AUTH_TYPE: AuthTypes - CLIENT_SECRET: Optional[str] - CERT_PFX_FILE: Optional[str] - CONNECTION_NAME: Optional[str] - SCOPES: Optional[list[str]] - AUTHORITY: Optional[str] - ALT_BLUEPRINT_ID: Optional[str] + CLIENT_ID: str | None + CLIENT_SECRET: str | None + CERT_PFX_FILE: str | None + CONNECTION_NAME: str | None + FEDERATED_CLIENT_ID: str | None + SCOPES: list[str] | None + AUTHORITY: str | None + ALT_BLUEPRINT_ID: str | None ANONYMOUS_ALLOWED: bool = False # Multi-connection support: Maintains a map of all configured connections @@ -49,10 +51,11 @@ def __init__( client_secret: str | None = None, cert_pfx_file: str | None = None, connection_name: str | None = None, + federated_client_id: str | None = None, authority: str | None = None, scopes: list[str] | None = None, anonymous_allowed: bool = False, - **kwargs: Optional[dict[str, str]], + **kwargs: str, ): self.AUTH_TYPE = auth_type or kwargs.get("AUTHTYPE", AuthTypes.client_secret) @@ -62,6 +65,7 @@ def __init__( self.CLIENT_SECRET = client_secret or kwargs.get("CLIENTSECRET", None) self.CERT_PFX_FILE = cert_pfx_file or kwargs.get("CERTPFXFILE", None) self.CONNECTION_NAME = connection_name or kwargs.get("CONNECTIONNAME", None) + self.FEDERATED_CLIENT_ID = federated_client_id or kwargs.get("FEDERATEDCLIENTID", None) self.SCOPES = scopes or kwargs.get("SCOPES", None) self.ALT_BLUEPRINT_ID = kwargs.get("ALT_BLUEPRINT_NAME", None) self.ANONYMOUS_ALLOWED = anonymous_allowed or kwargs.get( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/auth_types.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/auth_types.py index 58784ae8..655630a4 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/auth_types.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/auth_types.py @@ -10,3 +10,4 @@ class AuthTypes(str, Enum): client_secret = "ClientSecret" user_managed_identity = "UserManagedIdentity" system_managed_identity = "SystemManagedIdentity" + federated_credentials = "FederatedCredentials" diff --git a/tests/hosting_core/test_auth_configuration.py b/tests/hosting_core/test_auth_configuration.py index e8f5be35..8668450d 100644 --- a/tests/hosting_core/test_auth_configuration.py +++ b/tests/hosting_core/test_auth_configuration.py @@ -17,6 +17,7 @@ def test_auth_configuration_basic(self): client_id="test-client-id", client_secret="test-client-secret", cert_pfx_file="test-cert.pfx", + cert_pfx_file="test-cert.pfx", connection_name="test-connection", authority="https://login.microsoftonline.com", scopes=["test-scope-1", "test-scope-2"], @@ -27,6 +28,7 @@ def test_auth_configuration_basic(self): assert auth_config.CLIENT_ID == "test-client-id" assert auth_config.CLIENT_SECRET == "test-client-secret" assert auth_config.CERT_PFX_FILE == "test-cert.pfx" + assert auth_config.CERT_PFX_FILE == "test-cert.pfx" assert auth_config.CONNECTION_NAME == "test-connection" assert auth_config.AUTHORITY == "https://login.microsoftonline.com" assert auth_config.SCOPES == ["test-scope-1", "test-scope-2"] @@ -67,10 +69,11 @@ def test_load_configuration_from_env(self): def test_empty_settings(self): auth_config = AgentAuthConfiguration() assert auth_config.AUTH_TYPE == AuthTypes.client_secret - assert auth_config.TENANT_ID == None - assert auth_config.CLIENT_ID == None - assert auth_config.CLIENT_SECRET == None - assert auth_config.CERT_PFX_FILE == None - assert auth_config.CONNECTION_NAME == None - assert auth_config.AUTHORITY == None - assert auth_config.SCOPES == None + assert auth_config.TENANT_ID is None + assert auth_config.CLIENT_ID is None + assert auth_config.CLIENT_SECRET is None + assert auth_config.CERT_PFX_FILE is None + assert auth_config.FEDERATED_CLIENT_ID is None + assert auth_config.CONNECTION_NAME is None + assert auth_config.AUTHORITY is None + assert auth_config.SCOPES is None From 619845a21583e9212bfaceac52e07c72f933c515 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Thu, 2 Apr 2026 13:42:31 -0700 Subject: [PATCH 3/4] Formatting --- .../authentication/msal/msal_auth.py | 17 ++++++++++++----- .../authorization/agent_auth_configuration.py | 7 +++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py index 771fc524..5fe40cb4 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft_agents/authentication/msal/msal_auth.py @@ -210,20 +210,27 @@ def _create_client_application( } elif self._msal_configuration.AUTH_TYPE == AuthTypes.federated_credentials: mi_client = ManagedIdentityClient( - UserAssignedManagedIdentity(client_id=self._msal_configuration.FEDERATED_CLIENT_ID), + UserAssignedManagedIdentity( + client_id=self._msal_configuration.FEDERATED_CLIENT_ID + ), http_client=Session(), ) + def get_assertion() -> str: - result = mi_client.acquire_token_for_client(resource="api://AzureADTokenExchange") + result = mi_client.acquire_token_for_client( + resource="api://AzureADTokenExchange" + ) if "access_token" not in result: logger.error( f"Failed to acquire token for federated credentials: {result}" ) raise ValueError( - authentication_errors.FailedToAcquireToken.format(str(result)) + authentication_errors.FailedToAcquireToken.format( + str(result) + ) ) return result["access_token"] - + client_credential = {"client_assertion": get_assertion} else: logger.error( @@ -236,7 +243,7 @@ def get_assertion() -> str: return ConfidentialClientApplication( client_id=self._msal_configuration.CLIENT_ID, authority=authority, - client_credential=client_credential + client_credential=client_credential, ) def _client_rep( diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py index 5730fcee..f9890471 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. from __future__ import annotations -from typing import Optional from microsoft_agents.hosting.core.authorization.auth_types import AuthTypes @@ -56,7 +55,6 @@ def __init__( scopes: list[str] | None = None, anonymous_allowed: bool = False, **kwargs: str, - **kwargs: str, ): self.AUTH_TYPE = auth_type or kwargs.get("AUTHTYPE", AuthTypes.client_secret) @@ -65,9 +63,10 @@ def __init__( self.TENANT_ID = tenant_id or kwargs.get("TENANTID", None) self.CLIENT_SECRET = client_secret or kwargs.get("CLIENTSECRET", None) self.CERT_PFX_FILE = cert_pfx_file or kwargs.get("CERTPFXFILE", None) - self.CERT_PFX_FILE = cert_pfx_file or kwargs.get("CERTPFXFILE", None) self.CONNECTION_NAME = connection_name or kwargs.get("CONNECTIONNAME", None) - self.FEDERATED_CLIENT_ID = federated_client_id or kwargs.get("FEDERATEDCLIENTID", None) + self.FEDERATED_CLIENT_ID = federated_client_id or kwargs.get( + "FEDERATEDCLIENTID", None + ) self.SCOPES = scopes or kwargs.get("SCOPES", None) self.ALT_BLUEPRINT_ID = kwargs.get("ALT_BLUEPRINT_NAME", None) self.ANONYMOUS_ALLOWED = anonymous_allowed or kwargs.get( From fa130fd2fc620782ad4338da0898ab18d67b021d Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Thu, 2 Apr 2026 13:55:13 -0700 Subject: [PATCH 4/4] Addressing PR comments --- .../hosting/core/authorization/agent_auth_configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py index f9890471..ee5798d7 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/agent_auth_configuration.py @@ -16,6 +16,7 @@ class AgentAuthConfiguration: CLIENT_SECRET: The client secret for the Azure AD application (if using client secret authentication). CERT_PFX_FILE: The path to the PFX certificate file (if using certificate authentication). CONNECTION_NAME: The name of the connection + FEDERATED_CLIENT_ID: The client ID for federated credentials (if using federated credentials authentication). SCOPES: The scopes to request AUTHORITY: The authority URL for the Azure AD (if different from the default). ALT_BLUEPRINT_ID: An optional alternative blueprint ID used when constructing a connector client.