Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 69 additions & 30 deletions libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

# pylint: disable=too-many-lines

import asyncio
import base64
import json
Expand All @@ -22,6 +24,7 @@
JwtTokenValidation,
SimpleCredentialProvider,
SkillValidation,
CertificateAppCredentials,
)
from botframework.connector.token_api import TokenApiClient
from botframework.connector.token_api.models import TokenStatus
Expand Down Expand Up @@ -76,13 +79,15 @@ class BotFrameworkAdapterSettings:
def __init__(
self,
app_id: str,
app_password: str,
app_password: str = None,
channel_auth_tenant: str = None,
oauth_endpoint: str = None,
open_id_metadata: str = None,
channel_service: str = None,
channel_provider: ChannelProvider = None,
auth_configuration: AuthenticationConfiguration = None,
certificate_thumbprint: str = None,
certificate_private_key: str = None,
):
"""
Contains the settings used to initialize a :class:`BotFrameworkAdapter` instance.
Expand All @@ -104,6 +109,15 @@ def __init__(
:type channel_provider: :class:`botframework.connector.auth.ChannelProvider`
:param auth_configuration:
:type auth_configuration: :class:`botframework.connector.auth.AuthenticationConfiguration`
:param certificate_thumbprint: X509 thumbprint
:type certificate_thumbprint: str
:param certificate_private_key: X509 private key
:type certificate_private_key: str

.. remarks::
For credentials authorization, both app_id and app_password are required.
For certificate authorization, app_id, certificate_thumbprint, and certificate_private_key are required.

"""
self.app_id = app_id
self.app_password = app_password
Expand All @@ -113,6 +127,8 @@ def __init__(
self.channel_service = channel_service
self.channel_provider = channel_provider
self.auth_configuration = auth_configuration or AuthenticationConfiguration()
self.certificate_thumbprint = certificate_thumbprint
self.certificate_private_key = certificate_private_key


class BotFrameworkAdapter(BotAdapter, UserTokenProvider):
Expand Down Expand Up @@ -141,23 +157,42 @@ def __init__(self, settings: 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)
)
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
Expand Down Expand Up @@ -878,35 +913,39 @@ async def create_connector_client(

: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)

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
# 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.oauth_scope = (
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
credentials = MicrosoftAppCredentials(
bot_app_id_claim, password, oauth_scope=scope
)
else:
credentials = self._credentials
else:
credentials = self._credentials
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 ''}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
# coding=utf-8
# --------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
#
# Code generated by Microsoft (R) AutoRest Code Generator.
# Changes may cause incorrect behavior and will be lost if the code is
# regenerated.
# --------------------------------------------------------------------------
# pylint: disable=missing-docstring
from .authentication_constants import *
from .government_constants import *
from .channel_provider import *
from .simple_channel_provider import *
from .microsoft_app_credentials import *
from .claims_identity import *
from .jwt_token_validation import *
from .credential_provider import *
from .channel_validation import *
from .emulator_validation import *
from .jwt_token_extractor import *
from .authentication_configuration import *
# coding=utf-8
# --------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
#
# Code generated by Microsoft (R) AutoRest Code Generator.
# Changes may cause incorrect behavior and will be lost if the code is
# regenerated.
# --------------------------------------------------------------------------
# pylint: disable=missing-docstring
from .authentication_constants import *
from .government_constants import *
from .channel_provider import *
from .simple_channel_provider import *
from .microsoft_app_credentials import *
from .certificate_app_credentials import *
from .claims_identity import *
from .jwt_token_validation import *
from .credential_provider import *
from .channel_validation import *
from .emulator_validation import *
from .jwt_token_extractor import *
from .authentication_configuration import *
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# 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


class AppCredentials(Authentication):
"""
Base class for token retrieval. Subclasses MUST override get_access_token in
order to supply a valid token for the specific credentials.
"""

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 = {}

def __init__(
self,
app_id: str = None,
channel_auth_tenant: str = None,
oauth_scope: str = None,
):
"""
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.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.microsoft_app_id = app_id

@staticmethod
def trust_service_url(service_url: str, expiration=None):
"""
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:
"""
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

@staticmethod
def _is_trusted_url(host: str) -> bool:
expiration = AppCredentials.trustedHostNames.get(host, datetime.min)
return expiration > (datetime.now() - timedelta(minutes=5))

# pylint: disable=arguments-differ
def signed_session(self, session: requests.Session = None) -> requests.Session:
"""
Gets the signed session. This is called by the msrest package
:returns: Signed requests.Session object
"""
if not session:
session = requests.Session()

if not self._should_authorize(session):
session.headers.pop("Authorization", None)
else:
auth_token = self.get_access_token()
header = "{} {}".format("Bearer", auth_token)
session.headers["Authorization"] = header

return session

def _should_authorize(
self, session: requests.Session # pylint: disable=unused-argument
) -> bool:
return True

def get_access_token(self, force_refresh: bool = False) -> str:
"""
Returns a token for the current AppCredentials.
:return: The token
"""
raise NotImplementedError()
Loading