From 775c7604a24099bc43fb1611e9f4f0f9453795d4 Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Mon, 1 Jul 2024 11:08:44 -0500 Subject: [PATCH 1/8] Added CertificateServiceClientCredentialsFactory --- .../botframework/connector/auth/__init__.py | 1 + ...icate_service_client_credential_factory.py | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py diff --git a/libraries/botframework-connector/botframework/connector/auth/__init__.py b/libraries/botframework-connector/botframework/connector/auth/__init__.py index 8747a03c8..113112638 100644 --- a/libraries/botframework-connector/botframework/connector/auth/__init__.py +++ b/libraries/botframework-connector/botframework/connector/auth/__init__.py @@ -17,6 +17,7 @@ from .microsoft_app_credentials import * from .microsoft_government_app_credentials import * from .certificate_app_credentials import * +from .certificate_service_client_credential_factory import * from .claims_identity import * from .jwt_token_validation import * from .credential_provider import * diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py b/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py new file mode 100644 index 000000000..d930ea172 --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py @@ -0,0 +1,70 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from logging import Logger + +from msrest.authentication import Authentication + +from .certificate_app_credentials import CertificateAppCredentials +from .microsoft_app_credentials import MicrosoftAppCredentials +from .service_client_credentials_factory import ServiceClientCredentialsFactory + + +class CertificateServiceClientCredentialsFactory(ServiceClientCredentialsFactory): + def __init__( + self, + certificate_thumbprint: str, + certificate_private_key: str, + app_id: str, + tenant_id: str = None, + certificate_public: str = None, + *, + logger: Logger = None + ) -> None: + """ + CertificateServiceClientCredentialsFactory implementation using a certificate. + + :param certificate_thumbprint: + :param certificate_private_key: + :param app_id: + :param tenant_id: + :param certificate_public: public_certificate (optional) is public key certificate which will be sent + through ‘x5c’ JWT header only for subject name and issuer authentication to support cert auto rolls. + """ + + self.certificate_thumbprint = certificate_thumbprint + self.certificate_private_key = certificate_private_key + self.app_id = app_id + self.tenant_id = tenant_id + self.certificate_public = certificate_public + self._logger = logger + + async def is_valid_app_id(self, app_id: str) -> bool: + return app_id == self.app_id + + async def is_authentication_disabled(self) -> bool: + return not self.app_id + + async def create_credentials( + self, + app_id: str, + oauth_scope: str, + login_endpoint: str, + validate_authority: bool, + ) -> Authentication: + if await self.is_authentication_disabled(): + return MicrosoftAppCredentials.empty() + + if not await self.is_valid_app_id(app_id): + raise Exception("Invalid app_id") + + credentials = CertificateAppCredentials( + app_id, + self.certificate_thumbprint, + self.certificate_private_key, + self.tenant_id, + oauth_scope, + self.certificate_public, + ) + + return credentials From 3e5743afb33341f8277f5efaf6c02c24fb14b8e6 Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Mon, 1 Jul 2024 11:29:30 -0500 Subject: [PATCH 2/8] Added Gov and Private cloud support to CertificateServiceClientCredentialsFactory --- .../botframework/connector/auth/__init__.py | 1 + .../certificate_government_app_credentials.py | 51 ++++++++++++++ ...icate_service_client_credential_factory.py | 66 ++++++++++++++++--- 3 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 libraries/botframework-connector/botframework/connector/auth/certificate_government_app_credentials.py diff --git a/libraries/botframework-connector/botframework/connector/auth/__init__.py b/libraries/botframework-connector/botframework/connector/auth/__init__.py index 113112638..d58dcf5fa 100644 --- a/libraries/botframework-connector/botframework/connector/auth/__init__.py +++ b/libraries/botframework-connector/botframework/connector/auth/__init__.py @@ -17,6 +17,7 @@ from .microsoft_app_credentials import * from .microsoft_government_app_credentials import * from .certificate_app_credentials import * +from .certificate_government_app_credentials import * from .certificate_service_client_credential_factory import * from .claims_identity import * from .jwt_token_validation import * diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_government_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/certificate_government_app_credentials.py new file mode 100644 index 000000000..b2883cfa1 --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/auth/certificate_government_app_credentials.py @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .certificate_app_credentials import CertificateAppCredentials +from .government_constants import GovernmentConstants + + +class CertificateGovernmentAppCredentials(CertificateAppCredentials): + """ + GovernmentAppCredentials implementation using a certificate. + """ + + def __init__( + self, + app_id: str, + certificate_thumbprint: str, + certificate_private_key: str, + channel_auth_tenant: str = None, + oauth_scope: str = None, + certificate_public: str = None, + ): + """ + AppCredentials implementation using a certificate. + + :param app_id: + :param certificate_thumbprint: + :param certificate_private_key: + :param channel_auth_tenant: + :param oauth_scope: + :param certificate_public: public_certificate (optional) is public key certificate which will be sent + through ‘x5c’ JWT header only for subject name and issuer authentication to support cert auto rolls. + """ + + # super will set proper scope and endpoint. + super().__init__( + app_id=app_id, + channel_auth_tenant=channel_auth_tenant, + oauth_scope=oauth_scope, + certificate_thumbprint=certificate_thumbprint, + certificate_private_key=certificate_private_key, + certificate_public=certificate_public, + ) + + def _get_default_channelauth_tenant(self) -> str: + return GovernmentConstants.DEFAULT_CHANNEL_AUTH_TENANT + + def _get_to_channel_from_bot_loginurl_prefix(self) -> str: + return GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + + def _get_to_channel_from_bot_oauthscope(self) -> str: + return GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py b/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py index d930ea172..1e7205f33 100644 --- a/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py +++ b/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py @@ -5,7 +5,10 @@ from msrest.authentication import Authentication +from .authentication_constants import AuthenticationConstants +from .government_constants import GovernmentConstants from .certificate_app_credentials import CertificateAppCredentials +from .certificate_government_app_credentials import CertificateGovernmentAppCredentials from .microsoft_app_credentials import MicrosoftAppCredentials from .service_client_credentials_factory import ServiceClientCredentialsFactory @@ -58,13 +61,60 @@ async def create_credentials( if not await self.is_valid_app_id(app_id): raise Exception("Invalid app_id") - credentials = CertificateAppCredentials( - app_id, - self.certificate_thumbprint, - self.certificate_private_key, - self.tenant_id, - oauth_scope, - self.certificate_public, - ) + normalized_endpoint = login_endpoint.lower() if login_endpoint else "" + + if normalized_endpoint.startswith( + AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + ): + credentials = CertificateAppCredentials( + app_id, + self.certificate_thumbprint, + self.certificate_private_key, + self.tenant_id, + oauth_scope, + self.certificate_public, + ) + elif normalized_endpoint.startswith( + GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + ): + credentials = CertificateGovernmentAppCredentials( + app_id, + self.certificate_thumbprint, + self.certificate_private_key, + self.tenant_id, + oauth_scope, + self.certificate_public, + ) + else: + credentials = _CertificatePrivateCloudAppCredentials( + app_id, + self.password, + self.tenant_id, + oauth_scope, + login_endpoint, + validate_authority, + ) return credentials + + +class _CertificatePrivateCloudAppCredentials(CertificateAppCredentials): + def __init__( + self, + app_id: str, + password: str, + tenant_id: str, + oauth_scope: str, + oauth_endpoint: str, + validate_authority: bool, + ): + super().__init__( + app_id, password, channel_auth_tenant=tenant_id, oauth_scope=oauth_scope + ) + + self.oauth_endpoint = oauth_endpoint + self._validate_authority = validate_authority + + @property + def validate_authority(self): + return self._validate_authority From a2cea0d7d796e9fd37dad449dc54492d7e5e5a72 Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Mon, 1 Jul 2024 11:37:35 -0500 Subject: [PATCH 3/8] Corrected _CertificatePrivateCloudAppCredentials --- ...tificate_service_client_credential_factory.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py b/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py index 1e7205f33..58793f4c1 100644 --- a/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py +++ b/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py @@ -88,7 +88,8 @@ async def create_credentials( else: credentials = _CertificatePrivateCloudAppCredentials( app_id, - self.password, + self.certificate_thumbprint, + self.certificate_private_key, self.tenant_id, oauth_scope, login_endpoint, @@ -102,14 +103,21 @@ class _CertificatePrivateCloudAppCredentials(CertificateAppCredentials): def __init__( self, app_id: str, - password: str, - tenant_id: str, + certificate_thumbprint: str, + certificate_private_key: str, + channel_auth_tenant: str, oauth_scope: str, + certificate_public: str, oauth_endpoint: str, validate_authority: bool, ): super().__init__( - app_id, password, channel_auth_tenant=tenant_id, oauth_scope=oauth_scope + app_id, + certificate_thumbprint, + certificate_private_key, + channel_auth_tenant, + oauth_scope, + certificate_public, ) self.oauth_endpoint = oauth_endpoint From 0add89156065fbe76b7c08f16bc8c6ac272d88f9 Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Mon, 1 Jul 2024 11:47:10 -0500 Subject: [PATCH 4/8] Fixed _CertificatePrivateCloudAppCredentials creation --- .../auth/certificate_service_client_credential_factory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py b/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py index 58793f4c1..7a71c28bd 100644 --- a/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py +++ b/libraries/botframework-connector/botframework/connector/auth/certificate_service_client_credential_factory.py @@ -92,6 +92,7 @@ async def create_credentials( self.certificate_private_key, self.tenant_id, oauth_scope, + self.certificate_public, login_endpoint, validate_authority, ) From e896e43aa63f6c2f0b9609766c9cc095c22ecfac Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Mon, 1 Jul 2024 13:20:25 -0500 Subject: [PATCH 5/8] Added CertificateServiceClientCredentialsFactoryTests --- ...icate_service_client_credential_factory.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 libraries/botframework-connector/tests/test_certificate_service_client_credential_factory.py diff --git a/libraries/botframework-connector/tests/test_certificate_service_client_credential_factory.py b/libraries/botframework-connector/tests/test_certificate_service_client_credential_factory.py new file mode 100644 index 000000000..bb86dcb83 --- /dev/null +++ b/libraries/botframework-connector/tests/test_certificate_service_client_credential_factory.py @@ -0,0 +1,70 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botframework.connector.auth import ( + AppCredentials, + AuthenticationConstants, + GovernmentConstants, + CertificateServiceClientCredentialsFactory, + CertificateAppCredentials, + CertificateGovernmentAppCredentials +) + + +class CertificateServiceClientCredentialsFactoryTests(aiounittest.AsyncTestCase): + test_appid = "test_appid" + test_tenant_id = "test_tenant_id" + test_audience = "test_audience" + login_endpoint = AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + gov_login_endpoint = GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + private_login_endpoint = "https://login.privatecloud.com" + + async def test_can_create_public_credentials(self): + factory = CertificateServiceClientCredentialsFactory( + app_id=CertificateServiceClientCredentialsFactoryTests.test_appid, + certificate_thumbprint="thumbprint", + certificate_private_key="private_key", + ) + + credentials = await factory.create_credentials( + CertificateServiceClientCredentialsFactoryTests.test_appid, + CertificateServiceClientCredentialsFactoryTests.test_audience, + CertificateServiceClientCredentialsFactoryTests.login_endpoint, + True, + ) + + assert isinstance(credentials, CertificateAppCredentials) + + async def test_can_create_gov_credentials(self): + factory = CertificateServiceClientCredentialsFactory( + app_id=CertificateServiceClientCredentialsFactoryTests.test_appid, + certificate_thumbprint="thumbprint", + certificate_private_key="private_key", + ) + + credentials = await factory.create_credentials( + CertificateServiceClientCredentialsFactoryTests.test_appid, + CertificateServiceClientCredentialsFactoryTests.test_audience, + CertificateServiceClientCredentialsFactoryTests.gov_login_endpoint, + True, + ) + + assert isinstance(credentials, CertificateGovernmentAppCredentials) + + async def test_can_create_private_credentials(self): + factory = CertificateServiceClientCredentialsFactory( + app_id=CertificateServiceClientCredentialsFactoryTests.test_appid, + certificate_thumbprint="thumbprint", + certificate_private_key="private_key", + ) + + credentials = await factory.create_credentials( + CertificateServiceClientCredentialsFactoryTests.test_appid, + CertificateServiceClientCredentialsFactoryTests.test_audience, + CertificateServiceClientCredentialsFactoryTests.private_login_endpoint, + True, + ) + + assert isinstance(credentials, CertificateAppCredentials) + assert credentials.oauth_endpoint == CertificateServiceClientCredentialsFactoryTests.private_login_endpoint From 3ff25095dc460cc9d089011a7ae3b206f0593aaa Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Mon, 1 Jul 2024 15:55:54 -0500 Subject: [PATCH 6/8] CertificateServiceClientCredentialsFactoryTests formatting --- .../test_certificate_service_client_credential_factory.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libraries/botframework-connector/tests/test_certificate_service_client_credential_factory.py b/libraries/botframework-connector/tests/test_certificate_service_client_credential_factory.py index bb86dcb83..558397c9f 100644 --- a/libraries/botframework-connector/tests/test_certificate_service_client_credential_factory.py +++ b/libraries/botframework-connector/tests/test_certificate_service_client_credential_factory.py @@ -8,7 +8,7 @@ GovernmentConstants, CertificateServiceClientCredentialsFactory, CertificateAppCredentials, - CertificateGovernmentAppCredentials + CertificateGovernmentAppCredentials, ) @@ -67,4 +67,7 @@ async def test_can_create_private_credentials(self): ) assert isinstance(credentials, CertificateAppCredentials) - assert credentials.oauth_endpoint == CertificateServiceClientCredentialsFactoryTests.private_login_endpoint + assert ( + credentials.oauth_endpoint + == CertificateServiceClientCredentialsFactoryTests.private_login_endpoint + ) From d2990d859606848734909fbd05c7efda16d6d60c Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Tue, 2 Jul 2024 12:16:35 -0500 Subject: [PATCH 7/8] Corrected CertificateAppCredentials scopes --- .../connector/auth/certificate_app_credentials.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py index b39511d82..3d501158d 100644 --- a/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py @@ -44,7 +44,6 @@ def __init__( oauth_scope=oauth_scope, ) - self.scopes = [self.oauth_scope] self.app = None self.certificate_thumbprint = certificate_thumbprint self.certificate_private_key = certificate_private_key @@ -56,16 +55,21 @@ def get_access_token(self, force_refresh: bool = False) -> str: :return: The access token for the given certificate. """ + scope = self.oauth_scope + if not scope.endswith("/.default"): + scope += "/.default" + scopes = [scope] + # Firstly, looks up a token from cache # Since we are looking for token for the current app, NOT for an end user, # notice we give account parameter as None. auth_token = self.__get_msal_app().acquire_token_silent( - self.scopes, account=None + scopes, account=None ) if not auth_token: # No suitable token exists in cache. Let's get a new one from AAD. auth_token = self.__get_msal_app().acquire_token_for_client( - scopes=self.scopes + scopes=scopes ) return auth_token["access_token"] From 7b30d250c069d16eb5d59725823afcb005855550 Mon Sep 17 00:00:00 2001 From: Tracy Boehrer Date: Tue, 2 Jul 2024 14:41:34 -0500 Subject: [PATCH 8/8] CertificateAppCredentials formatting --- .../connector/auth/certificate_app_credentials.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py index 3d501158d..89dbe882d 100644 --- a/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py @@ -63,14 +63,10 @@ def get_access_token(self, force_refresh: bool = False) -> str: # Firstly, looks up a token from cache # Since we are looking for token for the current app, NOT for an end user, # notice we give account parameter as None. - auth_token = self.__get_msal_app().acquire_token_silent( - scopes, account=None - ) + auth_token = self.__get_msal_app().acquire_token_silent(scopes, account=None) if not auth_token: # No suitable token exists in cache. Let's get a new one from AAD. - auth_token = self.__get_msal_app().acquire_token_for_client( - scopes=scopes - ) + auth_token = self.__get_msal_app().acquire_token_for_client(scopes=scopes) return auth_token["access_token"] def __get_msal_app(self):