diff --git a/doc/SkillClaimsValidation.md b/doc/SkillClaimsValidation.md index ee55c2894..53d761ddf 100644 --- a/doc/SkillClaimsValidation.md +++ b/doc/SkillClaimsValidation.md @@ -48,3 +48,11 @@ ADAPTER = BotFrameworkAdapter( SETTINGS, ) ``` + +For SingleTenant type bots, the additional issuers must be added based on the tenant id: +```python +AUTH_CONFIG = AuthenticationConfiguration( + claims_validator=AllowedSkillsClaimsValidator(CONFIG).claims_validator, + tenant_id=the_tenant_id +) +``` diff --git a/libraries/botbuilder-adapters-slack/tests/test_slack_client.py b/libraries/botbuilder-adapters-slack/tests/test_slack_client.py index 9174058a5..1f13c19b0 100644 --- a/libraries/botbuilder-adapters-slack/tests/test_slack_client.py +++ b/libraries/botbuilder-adapters-slack/tests/test_slack_client.py @@ -12,7 +12,7 @@ import requests import pytest -SKIP = os.getenv("SlackChannel") == '' +SKIP = os.getenv("SlackChannel") == "" class SlackClient(aiounittest.AsyncTestCase): diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py index 41ce8ff72..42df5d73a 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py @@ -279,19 +279,6 @@ async def continue_conversation( context.turn_state[BotAdapter.BOT_CALLBACK_HANDLER_KEY] = callback context.turn_state[BotAdapter.BOT_OAUTH_SCOPE_KEY] = audience - # If we receive a valid app id in the incoming token claims, add the channel service URL to the - # trusted services list so we can send messages back. - # The service URL for skills is trusted because it is applied by the SkillHandler based on the original - # request received by the root bot - app_id_from_claims = JwtTokenValidation.get_app_id_from_claims( - claims_identity.claims - ) - if app_id_from_claims: - if SkillValidation.is_skill_claim( - claims_identity.claims - ) or await self._credential_provider.is_valid_appid(app_id_from_claims): - AppCredentials.trust_service_url(reference.service_url) - client = await self.create_connector_client( reference.service_url, claims_identity, audience ) diff --git a/libraries/botbuilder-core/botbuilder/core/configuration_service_client_credentials_factory.py b/libraries/botbuilder-core/botbuilder/core/configuration_service_client_credentials_factory.py deleted file mode 100644 index cd9fbefc5..000000000 --- a/libraries/botbuilder-core/botbuilder/core/configuration_service_client_credentials_factory.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from typing import Any - -from botframework.connector.auth import PasswordServiceClientCredentialFactory - - -class ConfigurationServiceClientCredentialFactory( - PasswordServiceClientCredentialFactory -): - def __init__(self, configuration: Any) -> None: - if not hasattr(configuration, "APP_ID"): - raise Exception("Property 'APP_ID' is expected in configuration object") - if not hasattr(configuration, "APP_PASSWORD"): - raise Exception( - "Property 'APP_PASSWORD' is expected in configuration object" - ) - super().__init__(configuration.APP_ID, configuration.APP_PASSWORD) diff --git a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py index 8e987665a..7e9268ee7 100644 --- a/libraries/botbuilder-core/tests/test_bot_framework_adapter.py +++ b/libraries/botbuilder-core/tests/test_bot_framework_adapter.py @@ -621,14 +621,8 @@ async def callback(context: TurnContext): scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY] assert AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE == scope - # Ensure the serviceUrl was added to the trusted hosts - assert AppCredentials.is_trusted_service(channel_service_url) - refs = ConversationReference(service_url=channel_service_url) - # Ensure the serviceUrl is NOT in the trusted hosts - assert not AppCredentials.is_trusted_service(channel_service_url) - await adapter.continue_conversation( refs, callback, claims_identity=skills_identity ) @@ -694,14 +688,8 @@ async def callback(context: TurnContext): scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY] assert skill_2_app_id == scope - # Ensure the serviceUrl was added to the trusted hosts - assert AppCredentials.is_trusted_service(skill_2_service_url) - refs = ConversationReference(service_url=skill_2_service_url) - # Ensure the serviceUrl is NOT in the trusted hosts - assert not AppCredentials.is_trusted_service(skill_2_service_url) - await adapter.continue_conversation( refs, callback, claims_identity=skills_identity, audience=skill_2_app_id ) diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_adapter.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_adapter.py index da1b7c3c3..879bdeacd 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_adapter.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_adapter.py @@ -188,12 +188,6 @@ async def _http_authenticate_request(self, request: Request) -> bool: ) ) - # Add ServiceURL to the cache of trusted sites in order to allow token refreshing. - self._credentials.trust_service_url( - claims_identity.claims.get( - AuthenticationConstants.SERVICE_URL_CLAIM - ) - ) self.claims_identity = claims_identity return True except Exception as error: diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/configuration_service_client_credential_factory.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/configuration_service_client_credential_factory.py index b620e3b68..34e7c7644 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/configuration_service_client_credential_factory.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/configuration_service_client_credential_factory.py @@ -11,8 +11,38 @@ class ConfigurationServiceClientCredentialFactory( PasswordServiceClientCredentialFactory ): def __init__(self, configuration: Any, *, logger: Logger = None) -> None: - super().__init__( - app_id=getattr(configuration, "APP_ID", None), - password=getattr(configuration, "APP_PASSWORD", None), - logger=logger, + app_type = ( + configuration.APP_TYPE + if hasattr(configuration, "APP_TYPE") + else "MultiTenant" ) + app_id = configuration.APP_ID if hasattr(configuration, "APP_ID") else None + app_password = ( + configuration.APP_PASSWORD + if hasattr(configuration, "APP_PASSWORD") + else None + ) + app_tenantid = None + + if app_type == "UserAssignedMsi": + raise Exception("UserAssignedMsi APP_TYPE is not supported") + + if app_type == "SingleTenant": + app_tenantid = ( + configuration.APP_TENANTID + if hasattr(configuration, "APP_TENANTID") + else None + ) + + if not app_id: + raise Exception("Property 'APP_ID' is expected in configuration object") + if not app_password: + raise Exception( + "Property 'APP_PASSWORD' is expected in configuration object" + ) + if not app_tenantid: + raise Exception( + "Property 'APP_TENANTID' is expected in configuration object" + ) + + super().__init__(app_id, app_password, app_tenantid, logger=logger) diff --git a/libraries/botframework-connector/botframework/connector/auth/_bot_framework_client_impl.py b/libraries/botframework-connector/botframework/connector/auth/_bot_framework_client_impl.py index 512207cd4..df4313c0e 100644 --- a/libraries/botframework-connector/botframework/connector/auth/_bot_framework_client_impl.py +++ b/libraries/botframework-connector/botframework/connector/auth/_bot_framework_client_impl.py @@ -48,10 +48,6 @@ async def post_activity( conversation_id: str, activity: Activity, ) -> InvokeResponse: - if not from_bot_id: - raise TypeError("from_bot_id") - if not to_bot_id: - raise TypeError("to_bot_id") if not to_url: raise TypeError("to_url") if not service_url: @@ -100,6 +96,7 @@ async def post_activity( headers_dict = { "Content-type": "application/json; charset=utf-8", + "x-ms-conversation-id": conversation_id, } if token: headers_dict.update( diff --git a/libraries/botframework-connector/botframework/connector/auth/_built_in_bot_framework_authentication.py b/libraries/botframework-connector/botframework/connector/auth/_built_in_bot_framework_authentication.py index 25c5b0acd..8be3b200f 100644 --- a/libraries/botframework-connector/botframework/connector/auth/_built_in_bot_framework_authentication.py +++ b/libraries/botframework-connector/botframework/connector/auth/_built_in_bot_framework_authentication.py @@ -166,7 +166,7 @@ async def create_user_token_client( credentials = await self._credentials_factory.create_credentials( app_id, - audience=self._to_channel_from_bot_oauth_scope, + oauth_scope=self._to_channel_from_bot_oauth_scope, login_endpoint=self._login_endpoint, validate_authority=True, ) diff --git a/libraries/botframework-connector/botframework/connector/auth/_government_cloud_bot_framework_authentication.py b/libraries/botframework-connector/botframework/connector/auth/_government_cloud_bot_framework_authentication.py index cbdaa61dc..8cde743e5 100644 --- a/libraries/botframework-connector/botframework/connector/auth/_government_cloud_bot_framework_authentication.py +++ b/libraries/botframework-connector/botframework/connector/auth/_government_cloud_bot_framework_authentication.py @@ -24,7 +24,7 @@ def __init__( ): super(_GovernmentCloudBotFrameworkAuthentication, self).__init__( GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, - GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL, + GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX, CallerIdConstants.us_gov_channel, GovernmentConstants.CHANNEL_SERVICE, GovernmentConstants.OAUTH_URL_GOV, diff --git a/libraries/botframework-connector/botframework/connector/auth/_parameterized_bot_framework_authentication.py b/libraries/botframework-connector/botframework/connector/auth/_parameterized_bot_framework_authentication.py index 3d857eccb..1388094fe 100644 --- a/libraries/botframework-connector/botframework/connector/auth/_parameterized_bot_framework_authentication.py +++ b/libraries/botframework-connector/botframework/connector/auth/_parameterized_bot_framework_authentication.py @@ -155,7 +155,7 @@ async def create_user_token_client( credentials = await self._credentials_factory.create_credentials( app_id, - audience=self._to_channel_from_bot_oauth_scope, + oauth_scope=self._to_channel_from_bot_oauth_scope, login_endpoint=self._to_channel_from_bot_login_url, validate_authority=self._validate_authority, ) @@ -274,6 +274,11 @@ async def _skill_validation_authenticate_channel_token( ignore_expiration=False, ) + if self._auth_configuration.valid_token_issuers: + validation_params.issuer.append( + self._auth_configuration.valid_token_issuers + ) + # TODO: what should the openIdMetadataUrl be here? token_extractor = JwtTokenExtractor( validation_params, @@ -362,6 +367,11 @@ async def _emulator_validation_authenticate_emulator_token( ignore_expiration=False, ) + if self._auth_configuration.valid_token_issuers: + to_bot_from_emulator_validation_params.issuer.append( + self._auth_configuration.valid_token_issuers + ) + token_extractor = JwtTokenExtractor( to_bot_from_emulator_validation_params, metadata_url=self._to_bot_from_emulator_open_id_metadata_url, diff --git a/libraries/botframework-connector/botframework/connector/auth/app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/app_credentials.py index db657e25f..b054f0c2f 100644 --- a/libraries/botframework-connector/botframework/connector/auth/app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/app_credentials.py @@ -1,13 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from datetime import datetime, timedelta -from urllib.parse import urlparse - import requests from msrest.authentication import Authentication -from botframework.connector.auth import AuthenticationConstants +from .authentication_constants import AuthenticationConstants class AppCredentials(Authentication): @@ -17,16 +14,8 @@ class AppCredentials(Authentication): """ schema = "Bearer" - - trustedHostNames = { - # "state.botframework.com": datetime.max, - # "state.botframework.azure.us": datetime.max, - "api.botframework.com": datetime.max, - "token.botframework.com": datetime.max, - "api.botframework.azure.us": datetime.max, - "token.botframework.azure.us": datetime.max, - } cache = {} + __tenant = None def __init__( self, @@ -38,50 +27,55 @@ def __init__( Initializes a new instance of MicrosoftAppCredentials class :param channel_auth_tenant: Optional. The oauth token tenant. """ - tenant = ( - channel_auth_tenant - if channel_auth_tenant - else AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT - ) + self.microsoft_app_id = app_id + self.tenant = channel_auth_tenant self.oauth_endpoint = ( - AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + tenant - ) - self.oauth_scope = ( - oauth_scope or AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + self._get_to_channel_from_bot_loginurl_prefix() + self.tenant ) + self.oauth_scope = oauth_scope or self._get_to_channel_from_bot_oauthscope() - self.microsoft_app_id = app_id + def _get_default_channelauth_tenant(self) -> str: + return AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT + + def _get_to_channel_from_bot_loginurl_prefix(self) -> str: + return AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + + def _get_to_channel_from_bot_oauthscope(self) -> str: + return AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + + @property + def tenant(self) -> str: + return self.__tenant + + @tenant.setter + def tenant(self, value: str): + self.__tenant = value or self._get_default_channelauth_tenant() @staticmethod def trust_service_url(service_url: str, expiration=None): """ + Obsolete: trust_service_url is not a required part of the security model. Checks if the service url is for a trusted host or not. :param service_url: The service url. :param expiration: The expiration time after which this service url is not trusted anymore. - :returns: True if the host of the service url is trusted; False otherwise. """ - if expiration is None: - expiration = datetime.now() + timedelta(days=1) - host = urlparse(service_url).hostname - if host is not None: - AppCredentials.trustedHostNames[host] = expiration @staticmethod - def is_trusted_service(service_url: str) -> bool: + def is_trusted_service(service_url: str) -> bool: # pylint: disable=unused-argument """ + Obsolete: is_trusted_service is not a required part of the security model. Checks if the service url is for a trusted host or not. :param service_url: The service url. :returns: True if the host of the service url is trusted; False otherwise. """ - host = urlparse(service_url).hostname - if host is not None: - return AppCredentials._is_trusted_url(host) - return False + return True @staticmethod - def _is_trusted_url(host: str) -> bool: - expiration = AppCredentials.trustedHostNames.get(host, datetime.min) - return expiration > (datetime.now() - timedelta(minutes=5)) + def _is_trusted_url(host: str) -> bool: # pylint: disable=unused-argument + """ + Obsolete: _is_trusted_url is not a required part of the security model. + """ + return True # pylint: disable=arguments-differ def signed_session(self, session: requests.Session = None) -> requests.Session: @@ -92,7 +86,7 @@ def signed_session(self, session: requests.Session = None) -> requests.Session: if not session: session = requests.Session() - if not self._should_authorize(session): + if not self._should_set_token(session): session.headers.pop("Authorization", None) else: auth_token = self.get_access_token() @@ -101,13 +95,13 @@ def signed_session(self, session: requests.Session = None) -> requests.Session: return session - def _should_authorize( + def _should_set_token( self, session: requests.Session # pylint: disable=unused-argument ) -> bool: # We don't set the token if the AppId is not set, since it means that we are in an un-authenticated scenario. return ( self.microsoft_app_id != AuthenticationConstants.ANONYMOUS_SKILL_APP_ID - and self.microsoft_app_id is not None + and self.microsoft_app_id ) def get_access_token(self, force_refresh: bool = False) -> str: diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py index 59642d9ff..93314692d 100644 --- a/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py +++ b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py @@ -3,12 +3,39 @@ from typing import Awaitable, Callable, Dict, List +from .authentication_constants import AuthenticationConstants + class AuthenticationConfiguration: def __init__( self, required_endorsements: List[str] = None, claims_validator: Callable[[List[Dict]], Awaitable] = None, + valid_token_issuers: List[str] = None, + tenant_id: str = None, ): self.required_endorsements = required_endorsements or [] self.claims_validator = claims_validator + self.valid_token_issuers = valid_token_issuers or [] + + if tenant_id: + self.add_tenant_issuers(self, tenant_id) + + @staticmethod + def add_tenant_issuers(authentication_configuration, tenant_id: str): + authentication_configuration.valid_token_issuers.append( + AuthenticationConstants.VALID_TOKEN_ISSUER_URL_TEMPLATE_V1.format(tenant_id) + ) + authentication_configuration.valid_token_issuers.append( + AuthenticationConstants.VALID_TOKEN_ISSUER_URL_TEMPLATE_V2.format(tenant_id) + ) + authentication_configuration.valid_token_issuers.append( + AuthenticationConstants.VALID_GOVERNMENT_TOKEN_ISSUER_URL_TEMPLATE_V1.format( + tenant_id + ) + ) + authentication_configuration.valid_token_issuers.append( + AuthenticationConstants.VALID_GOVERNMENT_TOKEN_ISSUER_URL_TEMPLATE_V2.format( + tenant_id + ) + ) diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py index 6cda3226f..f1a24de08 100644 --- a/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py +++ b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py @@ -45,7 +45,7 @@ class AuthenticationConstants(ABC): EMULATE_OAUTH_CARDS_KEY = "EmulateOAuthCards" # TO BOT FROM CHANNEL: OpenID metadata document for tokens coming from MSA - TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL = ( + TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL = ( "https://login.botframework.com/v1/.well-known/openidconfiguration" ) @@ -56,10 +56,30 @@ class AuthenticationConstants(ABC): ) # TO BOT FROM EMULATOR: OpenID metadata document for tokens coming from MSA - TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = ( + TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL = ( "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration" ) + # The V1 Azure AD token issuer URL template that will contain the tenant id where + # the token was issued from. + VALID_TOKEN_ISSUER_URL_TEMPLATE_V1 = "https://sts.windows.net/{0}/" + + # The V2 Azure AD token issuer URL template that will contain the tenant id where + # the token was issued from. + VALID_TOKEN_ISSUER_URL_TEMPLATE_V2 = "https://login.microsoftonline.com/{0}/v2.0" + + # The Government V1 Azure AD token issuer URL template that will contain the tenant id + # where the token was issued from. + VALID_GOVERNMENT_TOKEN_ISSUER_URL_TEMPLATE_V1 = ( + "https://login.microsoftonline.us/{0}/" + ) + + # The Government V2 Azure AD token issuer URL template that will contain the tenant id + # where the token was issued from. + VALID_GOVERNMENT_TOKEN_ISSUER_URL_TEMPLATE_V2 = ( + "https://login.microsoftonline.us/{0}/v2.0" + ) + # Allowed token signing algorithms. Tokens come from channels to the bot. The code # that uses this also supports tokens coming from the emulator. ALLOWED_SIGNING_ALGORITHMS = ["RS256", "RS384", "RS512"] diff --git a/libraries/botframework-connector/botframework/connector/auth/channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/channel_validation.py index 671086a80..590e39862 100644 --- a/libraries/botframework-connector/botframework/connector/auth/channel_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/channel_validation.py @@ -88,7 +88,7 @@ async def authenticate_channel_token( metadata_endpoint = ( ChannelValidation.open_id_metadata_endpoint if ChannelValidation.open_id_metadata_endpoint - else AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL + else AuthenticationConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL ) token_extractor = JwtTokenExtractor( diff --git a/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py b/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py index 57c961ddc..4cd43ea9e 100644 --- a/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py @@ -113,9 +113,9 @@ async def authenticate_emulator_token( is_gov = JwtTokenValidation.is_government(channel_service_or_provider) open_id_metadata = ( - GovernmentConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL + GovernmentConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL if is_gov - else AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL + else AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL ) token_extractor = JwtTokenExtractor( diff --git a/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py index c7438865e..d3ec16da1 100644 --- a/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py @@ -33,7 +33,7 @@ async def authenticate_channel_token( endpoint = ( GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT if GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT - else GovernmentConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL + else GovernmentConstants.TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL ) token_extractor = JwtTokenExtractor( GovernmentChannelValidation.TO_BOT_FROM_GOVERNMENT_CHANNEL_TOKEN_VALIDATION_PARAMETERS, diff --git a/libraries/botframework-connector/botframework/connector/auth/government_constants.py b/libraries/botframework-connector/botframework/connector/auth/government_constants.py index c15c8e41e..6574859eb 100644 --- a/libraries/botframework-connector/botframework/connector/auth/government_constants.py +++ b/libraries/botframework-connector/botframework/connector/auth/government_constants.py @@ -14,11 +14,20 @@ class GovernmentConstants(ABC): """ TO CHANNEL FROM BOT: Login URL + + DEPRECATED: DO NOT USE """ TO_CHANNEL_FROM_BOT_LOGIN_URL = ( "https://login.microsoftonline.us/MicrosoftServices.onmicrosoft.us" ) + """ + TO CHANNEL FROM BOT: Login URL prefix + """ + TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX = "https://login.microsoftonline.com/" + + DEFAULT_CHANNEL_AUTH_TENANT = "MicrosoftServices.onmicrosoft.us" + """ TO CHANNEL FROM BOT: OAuth scope to request """ @@ -37,14 +46,14 @@ class GovernmentConstants(ABC): """ TO BOT FROM CHANNEL: OpenID metadata document for tokens coming from MSA """ - TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL = ( + TO_BOT_FROM_CHANNEL_OPENID_METADATA_URL = ( "https://login.botframework.azure.us/v1/.well-known/openidconfiguration" ) """ TO BOT FROM GOV EMULATOR: OpenID metadata document for tokens coming from MSA """ - TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = ( + TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL = ( "https://login.microsoftonline.us/" "cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/" ".well-known/openid-configuration" diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py index e4cbddd39..659559f14 100644 --- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py @@ -11,7 +11,6 @@ from .emulator_validation import EmulatorValidation from .enterprise_channel_validation import EnterpriseChannelValidation from .channel_validation import ChannelValidation -from .microsoft_app_credentials import MicrosoftAppCredentials from .credential_provider import CredentialProvider from .claims_identity import ClaimsIdentity from .government_constants import GovernmentConstants @@ -76,9 +75,6 @@ async def authenticate_request( auth_configuration, ) - # On the standard Auth path, we need to trust the URL that was incoming. - MicrosoftAppCredentials.trust_service_url(activity.service_url) - return claims_identity @staticmethod @@ -115,7 +111,7 @@ async def get_claims() -> ClaimsIdentity: ) is_gov = ( isinstance(channel_service_or_provider, ChannelProvider) - and channel_service_or_provider.is_public_azure() + and channel_service_or_provider.is_government() or isinstance(channel_service_or_provider, str) and JwtTokenValidation.is_government(channel_service_or_provider) ) diff --git a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py index d625d6ede..24c230007 100644 --- a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py @@ -3,7 +3,6 @@ from abc import ABC -import requests from msal import ConfidentialClientApplication from .app_credentials import AppCredentials @@ -14,9 +13,6 @@ class MicrosoftAppCredentials(AppCredentials, ABC): AppCredentials implementation using application ID and password. """ - MICROSOFT_APP_ID = "MicrosoftAppId" - MICROSOFT_PASSWORD = "MicrosoftPassword" - def __init__( self, app_id: str, @@ -73,11 +69,3 @@ def __get_msal_app(self): ) return self.app - - def _should_authorize(self, session: requests.Session) -> bool: - """ - Override of AppCredentials._should_authorize - :param session: - :return: - """ - return self.microsoft_app_id and self.microsoft_app_password diff --git a/libraries/botframework-connector/botframework/connector/auth/microsoft_government_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/microsoft_government_app_credentials.py index eb59fe941..a2d9a6f1e 100644 --- a/libraries/botframework-connector/botframework/connector/auth/microsoft_government_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_government_app_credentials.py @@ -1,7 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from botframework.connector.auth import MicrosoftAppCredentials, GovernmentConstants +from .microsoft_app_credentials import MicrosoftAppCredentials +from .government_constants import GovernmentConstants class MicrosoftGovernmentAppCredentials(MicrosoftAppCredentials): @@ -16,12 +17,22 @@ def __init__( channel_auth_tenant: str = None, scope: str = None, ): - super().__init__(app_id, app_password, channel_auth_tenant, scope) - self.oauth_endpoint = GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL - self.oauth_scope = ( - scope if scope else GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + super().__init__( + app_id, + app_password, + channel_auth_tenant, + scope, ) @staticmethod def empty(): return MicrosoftGovernmentAppCredentials("", "") + + 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/password_service_client_credential_factory.py b/libraries/botframework-connector/botframework/connector/auth/password_service_client_credential_factory.py index 1e14b496c..a8ff069d2 100644 --- a/libraries/botframework-connector/botframework/connector/auth/password_service_client_credential_factory.py +++ b/libraries/botframework-connector/botframework/connector/auth/password_service_client_credential_factory.py @@ -8,15 +8,22 @@ from .authentication_constants import AuthenticationConstants from .government_constants import GovernmentConstants from .microsoft_app_credentials import MicrosoftAppCredentials +from .microsoft_government_app_credentials import MicrosoftGovernmentAppCredentials from .service_client_credentials_factory import ServiceClientCredentialsFactory class PasswordServiceClientCredentialFactory(ServiceClientCredentialsFactory): def __init__( - self, app_id: str = None, password: str = None, *, logger: Logger = None + self, + app_id: str = None, + password: str = None, + tenant_id: str = None, + *, + logger: Logger = None ) -> None: self.app_id = app_id self.password = password + self.tenant_id = tenant_id self._logger = logger async def is_valid_app_id(self, app_id: str) -> bool: @@ -26,7 +33,11 @@ async def is_authentication_disabled(self) -> bool: return not self.app_id async def create_credentials( - self, app_id: str, audience: str, login_endpoint: str, validate_authority: bool + self, + app_id: str, + oauth_scope: str, + login_endpoint: str, + validate_authority: bool, ) -> Authentication: if await self.is_authentication_disabled(): return MicrosoftAppCredentials.empty() @@ -34,44 +45,32 @@ async def create_credentials( if not await self.is_valid_app_id(app_id): raise Exception("Invalid app_id") - credentials: MicrosoftAppCredentials = None + credentials: MicrosoftAppCredentials normalized_endpoint = login_endpoint.lower() if login_endpoint else "" if normalized_endpoint.startswith( AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX ): - # TODO: Unpack necessity of these empty credentials based on the - # loginEndpoint as no tokensare fetched when auth is disabled. - credentials = ( - MicrosoftAppCredentials.empty() - if not app_id - else MicrosoftAppCredentials(app_id, self.password, None, audience) + credentials = MicrosoftAppCredentials( + app_id, self.password, self.tenant_id, oauth_scope ) - elif normalized_endpoint == GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL: - credentials = ( - MicrosoftAppCredentials( - None, - None, - None, - GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, - ) - if not app_id - else MicrosoftAppCredentials(app_id, self.password, None, audience) + elif normalized_endpoint.startswith( + GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + ): + credentials = MicrosoftGovernmentAppCredentials( + app_id, + self.password, + self.tenant_id, + oauth_scope, ) - normalized_endpoint = login_endpoint else: - credentials = ( - _PrivateCloudAppCredentials( - None, None, None, normalized_endpoint, validate_authority - ) - if not app_id - else MicrosoftAppCredentials( - app_id, - self.password, - audience, - normalized_endpoint, - validate_authority, - ) + credentials = _PrivateCloudAppCredentials( + app_id, + self.password, + self.tenant_id, + oauth_scope, + login_endpoint, + validate_authority, ) return credentials @@ -82,12 +81,13 @@ 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=None, oauth_scope=oauth_scope + app_id, password, channel_auth_tenant=tenant_id, oauth_scope=oauth_scope ) self.oauth_endpoint = oauth_endpoint diff --git a/libraries/botframework-connector/botframework/connector/auth/service_client_credentials_factory.py b/libraries/botframework-connector/botframework/connector/auth/service_client_credentials_factory.py index 1c765ad9a..cbd008beb 100644 --- a/libraries/botframework-connector/botframework/connector/auth/service_client_credentials_factory.py +++ b/libraries/botframework-connector/botframework/connector/auth/service_client_credentials_factory.py @@ -28,7 +28,11 @@ async def is_authentication_disabled(self) -> bool: @abstractmethod async def create_credentials( - self, app_id: str, audience: str, login_endpoint: str, validate_authority: bool + self, + app_id: str, + oauth_scope: str, + login_endpoint: str, + validate_authority: bool, ) -> AppCredentials: """ A factory method for creating AppCredentials. diff --git a/libraries/botframework-connector/botframework/connector/auth/skill_validation.py b/libraries/botframework-connector/botframework/connector/auth/skill_validation.py index d23572e3f..b708e27cb 100644 --- a/libraries/botframework-connector/botframework/connector/auth/skill_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/skill_validation.py @@ -24,22 +24,6 @@ class SkillValidation: Validates JWT tokens sent to and from a Skill. """ - _token_validation_parameters = VerifyOptions( - issuer=[ - "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", # Auth v3.1, 1.0 token - "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", # Auth v3.1, 2.0 token - "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", # Auth v3.2, 1.0 token - "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", # Auth v3.2, 2.0 token - "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", # Auth for US Gov, 1.0 token - "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0", # Auth for US Gov, 2.0 token - "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", # Auth for US Gov, 1.0 token - "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", # Auth for US Gov, 2.0 token - ], - audience=None, - clock_tolerance=timedelta(minutes=5), - ignore_expiration=False, - ) - @staticmethod def is_skill_token(auth_header: str) -> bool: """ @@ -114,13 +98,34 @@ async def authenticate_channel_token( is_gov = JwtTokenValidation.is_government(channel_service_or_provider) open_id_metadata_url = ( - GovernmentConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL + GovernmentConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL if is_gov - else AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL + else AuthenticationConstants.TO_BOT_FROM_EMULATOR_OPENID_METADATA_URL + ) + + token_validation_parameters = VerifyOptions( + issuer=[ + "https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/", # v3.1, 1.0 token + "https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", # v3.1, 2.0 token + "https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", # v3.2, 1.0 token + "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", # v3.2, 2.0 token + "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", # US Gov, 1.0 token + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0", # US Gov, 2.0 token + "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/", # US Gov, 1.0 token + "https://login.microsoftonline.us/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", # US Gov, 2.0 token + ], + audience=None, + clock_tolerance=timedelta(minutes=5), + ignore_expiration=False, ) + if auth_configuration.valid_token_issuers: + token_validation_parameters.issuer.append( + auth_configuration.valid_token_issuers + ) + token_extractor = JwtTokenExtractor( - SkillValidation._token_validation_parameters, + token_validation_parameters, open_id_metadata_url, AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS, ) diff --git a/libraries/botframework-connector/tests/test_auth.py b/libraries/botframework-connector/tests/test_auth.py index 39a29a1ea..cc3abf66a 100644 --- a/libraries/botframework-connector/tests/test_auth.py +++ b/libraries/botframework-connector/tests/test_auth.py @@ -352,54 +352,6 @@ async def test_channel_authentication_disabled_should_be_anonymous(self): == AuthenticationConstants.ANONYMOUS_AUTH_TYPE ) - @pytest.mark.asyncio - # Tests with no authentication header and makes sure the service URL is not added to the trusted list. - async def test_channel_authentication_disabled_service_url_should_not_be_trusted( - self, - ): - activity = Activity(service_url="https://webchat.botframework.com/") - header = "" - credentials = SimpleCredentialProvider("", "") - - await JwtTokenValidation.authenticate_request(activity, header, credentials) - - assert not MicrosoftAppCredentials.is_trusted_service( - "https://webchat.botframework.com/" - ) - - # @pytest.mark.asyncio - # async def test_emulator_auth_header_correct_app_id_and_service_url_with_gov_channel_service_should_validate( - # self, - # ): - # await jwt_token_validation_validate_auth_header_with_channel_service_succeeds( - # "", # emulator creds - # "", - # GovernmentConstants.CHANNEL_SERVICE, - # ) - # - # await jwt_token_validation_validate_auth_header_with_channel_service_succeeds( - # "", # emulator creds - # "", - # SimpleChannelProvider(GovernmentConstants.CHANNEL_SERVICE), - # ) - - # @pytest.mark.asyncio - # async def - # test_emulator_auth_header_correct_app_id_and_service_url_with_private_channel_service_should_validate( - # self, - # ): - # await jwt_token_validation_validate_auth_header_with_channel_service_succeeds( - # "", # emulator creds - # "", - # "TheChannel", - # ) - # - # await jwt_token_validation_validate_auth_header_with_channel_service_succeeds( - # "", # emulator creds - # "", - # SimpleChannelProvider("TheChannel"), - # ) - @pytest.mark.asyncio async def test_government_channel_validation_succeeds(self): credentials = SimpleCredentialProvider("", "")