async def callback(context: TurnContext): context.turn_state[ SkillHandler.SKILL_CONVERSATION_REFERENCE_KEY ] = skill_conversation_reference TurnContext.apply_conversation_reference( activity, skill_conversation_reference.conversation_reference ) context.activity.id = reply_to_activity_id app_id = JwtTokenValidation.get_app_id_from_claims(claims_identity.claims) context.activity.caller_id = ( f"{CallerIdConstants.bot_to_bot_prefix}{app_id}" ) if activity.type == ActivityTypes.end_of_conversation: await self._conversation_id_factory.delete_conversation_reference( conversation_id ) self._apply_eoc_to_turn_context_activity(context, activity) await self._bot.on_turn(context) elif activity.type == ActivityTypes.event: self._apply_event_to_turn_context_activity(context, activity) await self._bot.on_turn(context) else: await context.send_activity(activity)
def __init__(self, settings: BotFrameworkAdapterSettings): super(BotFrameworkAdapter, self).__init__() self.settings = settings or BotFrameworkAdapterSettings("", "") self.settings.channel_service = self.settings.channel_service or os.environ.get( AuthenticationConstants.CHANNEL_SERVICE ) self.settings.open_id_metadata = ( self.settings.open_id_metadata or os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY) ) self._credentials = MicrosoftAppCredentials( self.settings.app_id, self.settings.app_password, self.settings.channel_auth_tenant, ) self._credential_provider = SimpleCredentialProvider( self.settings.app_id, self.settings.app_password ) self._is_emulating_oauth_cards = False if self.settings.open_id_metadata: ChannelValidation.open_id_metadata_endpoint = self.settings.open_id_metadata GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT = ( self.settings.open_id_metadata ) if JwtTokenValidation.is_government(self.settings.channel_service): self._credentials.oauth_endpoint = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL ) self._credentials.oauth_scope = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE )
async def callback(context: TurnContext): nonlocal resource_response context.turn_state[ SkillHandler. SKILL_CONVERSATION_REFERENCE_KEY] = skill_conversation_reference TurnContext.apply_conversation_reference( activity, skill_conversation_reference.conversation_reference) context.activity.id = reply_to_activity_id app_id = JwtTokenValidation.get_app_id_from_claims( claims_identity.claims) context.activity.caller_id = ( f"{CallerIdConstants.bot_to_bot_prefix}{app_id}") if activity.type == ActivityTypes.end_of_conversation: await self._conversation_id_factory.delete_conversation_reference( conversation_id) await self._send_to_bot(activity, context) elif activity.type == ActivityTypes.event: await self._send_to_bot(activity, context) elif activity.type in (ActivityTypes.command, ActivityTypes.command_result): if activity.name.startswith("application/"): # Send to channel and capture the resource response for the SendActivityCall so we can return it. resource_response = await context.send_activity(activity) else: await self._send_to_bot(activity, context) else: # Capture the resource response for the SendActivityCall so we can return it. resource_response = await context.send_activity(activity)
async def validate_claims(self, claims: dict): if SkillValidation.is_skill_claim(claims) and self.allowed_callers: # Check that the appId claim in the skill request is in the list of skills configured for this bot. app_id = JwtTokenValidation.get_app_id_from_claims(claims) if app_id not in self.allowed_callers: raise ValueError( f'Received a request from an application with an appID of "{ app_id }". To enable requests from this bot, add the id to your configuration file.' )
async def test_should_authenticate_anonymous_skill_claim(self): sut = TestChannelServiceHandler() await sut.handle_reply_to_activity(None, "123", "456", {}) assert (sut.claims_identity.authentication_type == AuthenticationConstants.ANONYMOUS_AUTH_TYPE) assert (JwtTokenValidation.get_app_id_from_claims( sut.claims_identity.claims) == AuthenticationConstants.ANONYMOUS_SKILL_APP_ID)
def __create_caller_info(context: TurnContext) -> CallerInfo: bot_identity = context.turn_state.get(BotAdapter.BOT_IDENTITY_KEY) if bot_identity and SkillValidation.is_skill_claim(bot_identity.claims): return CallerInfo( caller_service_url=context.activity.service_url, scope=JwtTokenValidation.get_app_id_from_claims(bot_identity.claims), ) return None
def receive(self, auth_header: str, activity: Activity): loop = asyncio.new_event_loop() try: loop.run_until_complete( JwtTokenValidation.assert_valid_activity( activity, auth_header, self._credential_provider)) finally: loop.close() if self.on_receive is not None: self.on_receive(activity)
async def create_connector_client( self, service_url: str, identity: ClaimsIdentity = None ) -> ConnectorClient: """Allows for mocking of the connector client in unit tests :param service_url: The service URL :param identity: The claims identity :return: An instance of the :class:`ConnectorClient` class """ # Anonymous claims and non-skill claims should fall through without modifying the scope. credentials = self._credentials if identity: bot_app_id_claim = identity.claims.get( AuthenticationConstants.AUDIENCE_CLAIM ) or identity.claims.get(AuthenticationConstants.APP_ID_CLAIM) if bot_app_id_claim and SkillValidation.is_skill_claim(identity.claims): scope = JwtTokenValidation.get_app_id_from_claims(identity.claims) # Do nothing, if the current credentials and its scope are valid for the skill. # i.e. the adapter instance is pre-configured to talk with one skill. # Otherwise we will create a new instance of the AppCredentials # so self._credentials.oauth_scope isn't overridden. if self._credentials.oauth_scope != scope: password = await self._credential_provider.get_app_password( bot_app_id_claim ) credentials = MicrosoftAppCredentials( bot_app_id_claim, password, oauth_scope=scope ) if ( self.settings.channel_provider and self.settings.channel_provider.is_government() ): credentials.oauth_endpoint = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL ) credentials.oauth_scope = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE ) client_key = ( f"{service_url}{credentials.microsoft_app_id if credentials else ''}" ) client = self._connector_client_cache.get(client_key) if not client: client = ConnectorClient(credentials, base_url=service_url) client.config.add_user_agent(USER_AGENT) self._connector_client_cache[client_key] = client return client
def _get_calling_skill( self, claims_identity: ClaimsIdentity) -> Union[SkillDefinition, None]: app_id = JwtTokenValidation.get_app_id_from_claims( claims_identity.claims) if not app_id: return None return next(skill for skill in self._skills_config.SKILLS.values() if skill.app_id == app_id)
def __init__(self, settings: BotFrameworkAdapterSettings): """ Initializes a new instance of the :class:`BotFrameworkAdapter` class. :param settings: The settings to initialize the adapter :type settings: :class:`BotFrameworkAdapterSettings` """ super(BotFrameworkAdapter, self).__init__() self.settings = settings or BotFrameworkAdapterSettings("", "") # If settings.certificate_thumbprint & settings.certificate_private_key are provided, # use CertificateAppCredentials. if self.settings.certificate_thumbprint and settings.certificate_private_key: self._credentials = CertificateAppCredentials( self.settings.app_id, self.settings.certificate_thumbprint, self.settings.certificate_private_key, self.settings.channel_auth_tenant, ) self._credential_provider = SimpleCredentialProvider( self.settings.app_id, "") else: self._credentials = MicrosoftAppCredentials( self.settings.app_id, self.settings.app_password, self.settings.channel_auth_tenant, ) self._credential_provider = SimpleCredentialProvider( self.settings.app_id, self.settings.app_password) self._is_emulating_oauth_cards = False # If no channel_service or open_id_metadata values were passed in the settings, check the # process' Environment Variables for values. # These values may be set when a bot is provisioned on Azure and if so are required for # the bot to properly work in Public Azure or a National Cloud. self.settings.channel_service = self.settings.channel_service or os.environ.get( AuthenticationConstants.CHANNEL_SERVICE) self.settings.open_id_metadata = ( self.settings.open_id_metadata or os.environ.get( AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY)) if self.settings.open_id_metadata: ChannelValidation.open_id_metadata_endpoint = self.settings.open_id_metadata GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT = ( self.settings.open_id_metadata) if JwtTokenValidation.is_government(self.settings.channel_service): self._credentials.oauth_endpoint = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL) self._credentials.oauth_scope = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE) self._connector_client_cache: Dict[str, ConnectorClient] = {}
async def allow_callers_claims_validator(claims: Dict[str, object]): if SkillValidation.is_skill_claim(claims): # Check that the appId claim in the skill request is in the list of skills configured for this bot. app_id = JwtTokenValidation.get_app_id_from_claims(claims) if app_id not in self._allowed_skills: raise PermissionError( f'Received a request from a bot with an app ID of "{app_id}".' f" To enable requests from this caller, add the app ID to your configuration file." ) return
async def process_activity_with_identity(self, activity: Activity, identity: ClaimsIdentity, logic: Callable): context = self._create_context(activity) context.turn_state[BotAdapter.BOT_IDENTITY_KEY] = identity context.turn_state[BotAdapter.BOT_CALLBACK_HANDLER_KEY] = logic # To create the correct cache key, provide the OAuthScope when calling CreateConnectorClientAsync. # The OAuthScope is also stored on the TurnState to get the correct AppCredentials if fetching a token # is required. scope = (self.__get_botframework_oauth_scope() if not SkillValidation.is_skill_claim(identity.claims) else JwtTokenValidation.get_app_id_from_claims(identity.claims)) context.turn_state[BotAdapter.BOT_OAUTH_SCOPE_KEY] = scope client = await self.create_connector_client(activity.service_url, identity, scope) context.turn_state[BotAdapter.BOT_CONNECTOR_CLIENT_KEY] = client # Fix to assign tenant_id from channelData to Conversation.tenant_id. # MS Teams currently sends the tenant ID in channelData and the correct behavior is to expose # this value in Activity.Conversation.tenant_id. # This code copies the tenant ID from channelData to Activity.Conversation.tenant_id. # Once MS Teams sends the tenant_id in the Conversation property, this code can be removed. if (Channels.ms_teams == context.activity.channel_id and context.activity.conversation is not None and not context.activity.conversation.tenant_id and context.activity.channel_data): teams_channel_data = context.activity.channel_data if teams_channel_data.get("tenant", {}).get("id", None): context.activity.conversation.tenant_id = str( teams_channel_data["tenant"]["id"]) await self.run_pipeline(context, logic) if activity.type == ActivityTypes.invoke: invoke_response = context.turn_state.get( BotFrameworkAdapter._INVOKE_RESPONSE_KEY # pylint: disable=protected-access ) if invoke_response is None: return InvokeResponse(status=501) return invoke_response.value # Return the buffered activities in the response. In this case, the invoker # should deserialize accordingly: # activities = [Activity().deserialize(activity) for activity in response.body] if context.activity.delivery_mode == DeliveryModes.buffered_replies: serialized_activities = [ activity.serialize() for activity in context.buffered_replies ] return InvokeResponse(status=200, body=serialized_activities) return None
def _handle_authentication(self, authorization, activity): credential_provider = SimpleCredentialProvider(self._app_id, self._app_password) loop = asyncio.new_event_loop() try: loop.run_until_complete(JwtTokenValidation.assert_valid_activity( activity, authorization, credential_provider)) return True except Exception as ex: logger.info(ex) return False finally: loop.close()
def __handle_authentication(self, activity): credential_provider = SimpleCredentialProvider(APP_ID, APP_PASSWORD) loop = asyncio.new_event_loop() try: loop.run_until_complete(JwtTokenValidation.authenticate_request( activity, self.headers.get("Authorization"), credential_provider)) return True except Exception as ex: self.send_response(401, ex) self.end_headers() return False finally: loop.close()
async def create_connector_client( self, service_url: str, identity: ClaimsIdentity = None ) -> ConnectorClient: """Allows for mocking of the connector client in unit tests :param service_url: The service URL :param identity: The claims identity :return: An instance of the :class:`ConnectorClient` class """ if identity: bot_app_id_claim = identity.claims.get( AuthenticationConstants.AUDIENCE_CLAIM ) or identity.claims.get(AuthenticationConstants.APP_ID_CLAIM) credentials = None if bot_app_id_claim and SkillValidation.is_skill_claim(identity.claims): scope = JwtTokenValidation.get_app_id_from_claims(identity.claims) password = await self._credential_provider.get_app_password( bot_app_id_claim ) credentials = MicrosoftAppCredentials( bot_app_id_claim, password, oauth_scope=scope ) if ( self.settings.channel_provider and self.settings.channel_provider.is_government() ): credentials.oauth_endpoint = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL ) credentials.oauth_scope = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE ) else: credentials = self._credentials else: credentials = self._credentials client_key = ( f"{service_url}{credentials.microsoft_app_id if credentials else ''}" ) client = self._connector_client_cache.get(client_key) if not client: client = ConnectorClient(credentials, base_url=service_url) client.config.add_user_agent(USER_AGENT) self._connector_client_cache[client_key] = client return client
def oauth_api_url(self, context_or_service_url: Union[TurnContext, str]) -> str: url = None if self._is_emulating_oauth_cards: url = (context_or_service_url.activity.service_url if isinstance( context_or_service_url, object) else context_or_service_url) else: if self.settings.oauth_endpoint: url = self.settings.oauth_endpoint else: url = (US_GOV_OAUTH_ENDPOINT if JwtTokenValidation.is_government( self.settings.channel_service) else OAUTH_ENDPOINT) return url
def test_get_app_id_from_claims(self): v1_claims = {} v2_claims = {} app_id = str(uuid.uuid4()) # Empty list assert not JwtTokenValidation.get_app_id_from_claims(v1_claims) # AppId there but no version (assumes v1) v1_claims[AuthenticationConstants.APP_ID_CLAIM] = app_id assert JwtTokenValidation.get_app_id_from_claims(v1_claims) == app_id # AppId there with v1 version v1_claims[AuthenticationConstants.VERSION_CLAIM] = "1.0" assert JwtTokenValidation.get_app_id_from_claims(v1_claims) == app_id # v2 version but no azp v2_claims[AuthenticationConstants.VERSION_CLAIM] = "2.0" assert not JwtTokenValidation.get_app_id_from_claims(v2_claims) # v2 version but no azp v2_claims[AuthenticationConstants.AUTHORIZED_PARTY] = app_id assert JwtTokenValidation.get_app_id_from_claims(v2_claims) == app_id
async def create_connector_client(self, service_url: str, identity: ClaimsIdentity = None, audience: str = None) -> ConnectorClient: """ Creates the connector client :param service_url: The service URL :param identity: The claims identity :param audience: :return: An instance of the :class:`ConnectorClient` class """ if not identity: # This is different from C# where an exception is raised. In this case # we are creating a ClaimsIdentity to retain compatibility with this # method. identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: self.settings.app_id, AuthenticationConstants.APP_ID_CLAIM: self.settings.app_id, }, is_authenticated=True, ) # For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim. # For unauthenticated requests we have anonymous claimsIdentity provided auth is disabled. # For Activities coming from Emulator AppId claim contains the Bot's AAD AppId. bot_app_id = identity.claims.get( AuthenticationConstants.AUDIENCE_CLAIM) or identity.claims.get( AuthenticationConstants.APP_ID_CLAIM) # Anonymous claims and non-skill claims should fall through without modifying the scope. credentials = None if bot_app_id: scope = audience if not scope: scope = (JwtTokenValidation.get_app_id_from_claims( identity.claims) if SkillValidation.is_skill_claim(identity.claims) else self.__get_botframework_oauth_scope()) credentials = await self.__get_app_credentials(bot_app_id, scope) return self._get_or_create_connector_client(service_url, credentials)
def __handle_authentication(self, activity): credential_provider = SimpleCredentialProvider( MICROSOFT_CLIENT.get_microsoft_app_id(), MICROSOFT_CLIENT.get_microsoft_app_password()) loop = asyncio.new_event_loop() try: loop.run_until_complete( JwtTokenValidation.authenticate_request( activity, self.headers.get("Authorization"), credential_provider)) return True except Exception as ex: self.send_response(401, ex) self.end_headers() return False finally: loop.close()
async def test_channel_authentication_disabled_and_skill_should_be_anonymous( self): activity = Activity( channel_id=Channels.emulator, service_url="https://webchat.botframework.com/", relates_to=ConversationReference(), recipient=ChannelAccount(role=RoleTypes.skill), ) header = "" credentials = SimpleCredentialProvider("", "") claims_principal = await JwtTokenValidation.authenticate_request( activity, header, credentials) assert (claims_principal.authentication_type == AuthenticationConstants.ANONYMOUS_AUTH_TYPE) assert (JwtTokenValidation.get_app_id_from_claims( claims_principal.claims) == AuthenticationConstants.ANONYMOUS_SKILL_APP_ID)
def test_create_anonymous_skill_claim(): sut = SkillValidation.create_anonymous_skill_claim() assert (JwtTokenValidation.get_app_id_from_claims( sut.claims) == AuthenticationConstants.ANONYMOUS_SKILL_APP_ID) assert sut.authentication_type == AuthenticationConstants.ANONYMOUS_AUTH_TYPE