8000 Merge branch 'master' into axsuarez/error-handling-in-adapter · zigri2612/botbuilder-python@23ee046 · GitHub
[go: up one dir, main page]

Skip to content

Commit 23ee046

Browse files
authored
Merge branch 'master' into axsuarez/error-handling-in-adapter
2 parents 52fb7d4 + fe82333 commit 23ee046

File tree

9 files changed

+131
-42
lines changed

9 files changed

+131
-42
lines changed

libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,19 @@
1919

2020

2121
class BotFrameworkAdapterSettings(object):
22-
def __init__(self, app_id: str, app_password: str):
22+
def __init__(self, app_id: str, app_password: str, channel_auth_tenant: str= None):
2323
self.app_id = app_id
2424
self.app_password = app_password
25+
self.channel_auth_tenant = channel_auth_tenant
2526

2627

2728
class BotFrameworkAdapter(BotAdapter):
2829

2930
def __init__(self, settings: BotFrameworkAdapterSettings):
3031
super(BotFrameworkAdapter, self).__init__()
3132
self.settings = settings or BotFrameworkAdapterSettings('', '')
32-
self._credentials = MicrosoftAppCredentials(self.settings.app_id, self.settings.app_password)
33+
self._credentials = MicrosoftAppCredentials(self.settings.app_id, self.settings.app_password,
34+
self.settings.channel_auth_tenant)
3335
self._credential_provider = SimpleCredentialProvider(self.settings.app_id, self.settings.app_password)
3436

3537
async def continue_conversation(self, reference: ConversationReference, logic):

libraries/botbuilder-dialogs/botbuilder/dialogs/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from .dialog import Dialog
1818
from .waterfall_dialog import WaterfallDialog
1919
from .waterfall_step_context import WaterfallStepContext
20+
from .prompts import *
2021

2122
__all__ = [
2223
'ComponentDialog',
@@ -29,5 +30,15 @@
2930
'DialogTurnStatus',
3031
'Dialog',
3132
'WaterfallDialog',
32-
'WaterfallStepContext',
33+
'WaterfallStepContext',
34+
'ConfirmPrompt',
35+
'DateTimePrompt',
36+
'DateTimeResolution',
37+
'NumberPrompt',
38+
'PromptOptions',
39+
'PromptRecognizerResult',
40+
'PromptValidatorContext',
41+
'Prompt',
42+
'PromptOptions',
43+
'TextPrompt',
3344
'__version__']

libraries/botframework-connector/botframework/connector/auth/constants.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
class Constants: # pylint: disable=too-few-public-methods
2+
"""
3+
TO CHANNEL FROM BOT: Login URL prefix
4+
"""
5+
TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX = 'https://login.microsoftonline.com/'
6+
7+
"""
8+
TO CHANNEL FROM BOT: Login URL token endpoint path
9+
"""
10+
TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH = '/oauth2/v2.0/token'
11+
12+
"""
13+
TO CHANNEL FROM BOT: Default tenant from which to obtain a token for bot to channel communication
14+
"""
15+
DEFAULT_CHANNEL_AUTH_TENANT = 'botframework.com'
16+
217
TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.com"
318

419
TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = (

libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from datetime import datetime, timedelta
22
from urllib.parse import urlparse
3-
43
from msrest.authentication import (
54
BasicTokenAuthentication,
65
Authentication)
76
import requests
7+
import aiohttp
8+
import asyncio
9+
from .constants import Constants
810

11+
#TODO: Decide to move this to Constants or viceversa (when porting OAuth)
912
AUTH_SETTINGS = {
1013
"refreshEndpoint": 'https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token',
1114
"refreshScope": 'https://api.botframework.com/.default',
@@ -43,20 +46,41 @@ def from_json(json_values):
4346

4447

4548
class MicrosoftAppCredentials(Authentication):
49+
"""
50+
MicrosoftAppCredentials auth implementation and cache.
51+
"""
4652
refreshEndpoint = AUTH_SETTINGS["refreshEndpoint"]
4753
refreshScope = AUTH_SETTINGS["refreshScope"]
4854
schema = 'Bearer'
4955

5056
trustedHostNames = {}
5157
cache = {}
5258

53-
def __init__(self, appId: str, password: str):
54-
self.microsoft_app_id = appId
59+
def __init__(self, app_id: str, password: str, channel_auth_tenant: str = None):
60+
"""
61+
Initializes a new instance of MicrosoftAppCredentials class
62+
:param app_id: The Microsoft app ID.
63+
:param app_password: The Microsoft app password.
64+
:param channel_auth_tenant: Optional. The oauth token tenant.
65+
"""
66+
#The configuration property for the Microsoft app ID.
67+
self.microsoft_app_id = app_id
68+
# The configuration property for the Microsoft app Password.
5569
self.microsoft_app_password = password
56-
self.token_cache_key = appId + '-cache'
57-
58-
def signed_session(self):
59-
basic_authentication = BasicTokenAuthentication({"access_token": self.get_access_token()})
70+
tenant = (channel_auth_tenant if channel_auth_tenant is not None and len(channel_auth_tenant) > 0
71+
else Constants.DEFAULT_CHANNEL_AUTH_TENANT)
72+
self.oauth_endpoint = (Constants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + tenant +
73+
Constants.TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH)
74+
self.token_cache_key = app_id + '-cache'
75+
76+
def signed_session(self) -> requests.Session:
77+
"""
78+
Gets the signed session.
79+
:returns: Signed requests.Session object
80+
"""
81+
auth_token = asyncio.ensure_future(self.get_access_token())
82+
83+
basic_authentication = BasicTokenAuthentication({"access_token": auth_token})
6084
session = basic_authentication.signed_session()
6185

6286
# If there is no microsoft_app_id and no self.microsoft_app_password, then there shouldn't
@@ -65,7 +89,13 @@ def signed_session(self):
6589
del session.headers['Authorization']
6690
return session
6791

68-
def get_access_token(self, force_refresh=False):
92+
async def get_access_token(self, force_refresh: bool=False) -> str:
93+
"""
94+
Gets an OAuth access token.
95+
:param force_refresh: True to force a refresh of the token; or false to get
96+
a cached token if it exists.
97+
:returns: Access token string
98+
"""
6999
if self.microsoft_app_id and self.microsoft_app_password:
70100
if not force_refresh:
71101
# check the global cache for the token. If we have it, and it's valid, we're done.
@@ -78,27 +108,36 @@ def get_access_token(self, force_refresh=False):
78108
# 1. The user requested it via the force_refresh parameter
79109
# 2. We have it, but it's expired
80110
# 3. We don't have it in the cache.
81-
oauth_token = self.refresh_token()
111+
oauth_token = await self.refresh_token()
82112
MicrosoftAppCredentials.cache.setdefault(self.token_cache_key, oauth_token)
83113
return oauth_token.access_token
84114
else:
85115
return ''
86-
87-
def refresh_token(self):
116+
async def refresh_token(self) -> _OAuthResponse:
117+
"""
118+
returns: _OAuthResponse
119+
"""
88120
options = {
89121
'grant_type': 'client_credentials',
90122
'client_id': self.microsoft_app_id,
91123
'client_secret': self.microsoft_app_password,
92124
'scope': MicrosoftAppCredentials.refreshScope}
93-
response = requests.post(MicrosoftAppCredentials.refreshEndpoint, data=options)
94-
response.raise_for_status()
95-
oauth_response = _OAuthResponse.from_json(response.json())
96-
oauth_response.expiration_time = datetime.now() + \
97-
timedelta(seconds=(oauth_response.expires_in - 300))
125+
async with aiohttp.ClientSession() as session:
126+
async with session.post(self.oauth_endpoint, data=aiohttp.FormData(options)) as response:
127+
response.raise_for_status()
128+
oauth_response = _OAuthResponse.from_json(await response.json())
129+
oauth_response.expiration_time = datetime.now() + \
130+
timedelta(seconds=(oauth_response.expires_in - 300))
98131
return oauth_response
99132

100133
@staticmethod
101134
def trust_service_url(service_url: str, expiration=None):
135+
"""
136+
Checks if the service url is for a trusted host or not.
137+
:param service_url: The service url.
138+
:param expiration: The expiration time after which this service url is not trusted anymore.
139+
:returns: True if the host of the service url is trusted; False otherwise.
140+
"""
102141
if expiration is None:
103142
expiration = datetime.now() + timedelta(days=1)
104143
host = urlparse(service_url).hostname
@@ -107,12 +146,17 @@ def trust_service_url(service_url: str, expiration=None):
107146

108147
@staticmethod
109148
def is_trusted_service(service_url: str) -> bool:
149+
"""
150+
Checks if the service url is for a trusted host or not.
151+
:param service_url: The service url.
152+
:returns: True if the host of the service url is trusted; False otherwise.
153+
"""
110154
host = urlparse(service_url).hostname
111155
if host is not None:
112-
return MicrosoftAppCredentials.is_trusted_url(host)
156+
return MicrosoftAppCredentials._is_trusted_url(host)
113157
return False
114158

115159
@staticmethod
116-
def is_trusted_url(host: str) -> bool:
160+
def _is_trusted_url(host: str) -> bool:
117161
expiration = MicrosoftAppCredentials.trustedHostNames.get(host, datetime.min)
118162
return expiration > (datetime.now() - timedelta(minutes=5))

libraries/botframework-connector/tests/test_attachments.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import base64
33
import pytest
4+
import asyncio
45
from azure_devtools.scenario_tests import ReplayableTest
56

67
import msrest
@@ -17,13 +18,13 @@
1718
RECIPIENT_ID = 'U19KH8EHJ:T03CWQ0QB'
1819
CONVERSATION_ID = 'B21UTEF8S:T03CWQ0QB:D2369CT7C'
1920

20-
def get_auth_token():
21+
async def get_auth_token():
2122
try:
2223
from .app_creds_real import MICROSOFT_APP_ID, MICROSOFT_APP_PASSWORD
2324
# Define a "app_creds_real.py" file with your bot credentials as follows:
2425
# MICROSOFT_APP_ID = '...'
2526
# MICROSOFT_APP_PASSWORD = '...'
26-
return MicrosoftAppCredentials(
27+
return await MicrosoftAppCredentials(
2728
MICROSOFT_APP_ID,
2829
MICROSOFT_APP_PASSWORD).get_access_token()
2930
except ImportError:
@@ -38,7 +39,8 @@ def read_base64(path_to_file):
3839
encoded_string = base64.b64encode(image_file.read())
3940
return encoded_string
4041

41-
auth_token = get_auth_token()
42+
loop = asyncio.get_event_loop()
43+
auth_token = loop.run_until_complete(get_auth_token())
4244

4345
class AttachmentsTest(ReplayableTest):
4446
def __init__(self, method_name):

libraries/botframework-connector/tests/test_attachments_async.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
RECIPIENT_ID = 'U19KH8EHJ:T03CWQ0QB'
1919
CONVERSATION_ID = 'B21UTEF8S:T03CWQ0QB:D2369CT7C'
2020

21-
def get_auth_token():
21+
async def get_auth_token():
2222
try:
2323
from .app_creds_real import MICROSOFT_APP_ID, MICROSOFT_APP_PASSWORD
2424
# Define a "app_creds_real.py" file with your bot credentials as follows:
2525
# MICROSOFT_APP_ID = '...'
2626
# MICROSOFT_APP_PASSWORD = '...'
27-
return MicrosoftAppCredentials(
27+
return await MicrosoftAppCredentials(
2828
MICROSOFT_APP_ID,
2929
MICROSOFT_APP_PASSWORD).get_access_token()
3030
except ImportError:
@@ -45,7 +45,8 @@ async def return_sum(attachment_stream):
4545
counter += len(_)
4646
return counter
4747

48-
auth_token = get_auth_token()
48+
loop = asyncio.get_event_loop()
49+
auth_token = loop.run_until_complete(get_auth_token())
4950

5051
class AttachmentsTest(ReplayableTest):
5152
def __init__(self, method_name):

libraries/botframework-connector/tests/test_auth.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,23 @@ class TestAuth:
1414

1515
@pytest.mark.asyncio
1616
async def test_connector_auth_header_correct_app_id_and_service_url_should_validate(self):
17-
header = 'Bearer ' + MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
17+
header = 'Bearer ' + await MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
1818
credentials = SimpleCredentialProvider('2cd87869-38a0-4182-9251-d056e8f0ac24', '')
1919
result = await JwtTokenValidation.validate_auth_header(header, credentials, '', 'https://webchat.botframework.com/')
2020

2121
assert result
2222

2323
@pytest.mark.asyncio
2424
async def test_connector_auth_header_with_different_bot_app_id_should_not_validate(self):
25-
header = 'Bearer ' + MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
25+
header = 'Bearer ' + await MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
2626
credentials = SimpleCredentialProvider('00000000-0000-0000-0000-000000000000', '')
2727
with pytest.raises(Exception) as excinfo:
2828
await JwtTokenValidation.validate_auth_header(header, credentials, '', 'https://webchat.botframework.com/')
2929
assert 'Unauthorized' in str(excinfo.value)
3030

3131
@pytest.mark.asyncio
3232
async def test_connector_auth_header_and_no_credential_should_not_validate(self):
33-
header = 'Bearer ' + MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
33+
header = 'Bearer ' + await MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
3434
credentials = SimpleCredentialProvider('', '')
3535
with pytest.raises(Exception) as excinfo:
3636
await JwtTokenValidation.validate_auth_header(header, credentials, '', 'https://webchat.botframework.com/')
@@ -46,15 +46,15 @@ async def test_empty_header_and_no_credential_should_validate(self):
4646

4747
@pytest.mark.asyncio
4848
async def test_emulator_msa_header_correct_app_id_and_service_url_should_validate(self):
49-
header = 'Bearer ' + MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
49+
header = 'Bearer ' + await MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
5050
credentials = SimpleCredentialProvider('2cd87869-38a0-4182-9251-d056e8f0ac24', '')
5151
result = await JwtTokenValidation.validate_auth_header(header, credentials, '', 'https://webchat.botframework.com/')
5252

5353
assert result
5454

5555
@pytest.mark.asyncio
5656
async def test_emulator_msa_header_and_no_credential_should_not_validate(self):
57-
header = 'Bearer ' + MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
57+
header = 'Bearer ' + await MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
5858
credentials = SimpleCredentialProvider('00000000-0000-0000-0000-000000000000', '')
5959
with pytest.raises(Exception) as excinfo:
6060
await JwtTokenValidation.validate_auth_header(header, credentials, '', None)
@@ -64,26 +64,36 @@ async def test_emulator_msa_header_and_no_credential_should_not_validate(self):
6464
# Tests with a valid Token and service url; and ensures that Service url is added to Trusted service url list.
6565
async def test_channel_msa_header_Valid_service_url_should_be_trusted(self):
6666
activity = Activity(service_url = 'https://smba.trafficmanager.net/amer-client-ss.msg/')
67-
header = 'Bearer ' + MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
67+
header = 'Bearer ' + await MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
6868
credentials = SimpleCredentialProvider('2cd87869-38a0-4182-9251-d056e8f0ac24', '')
6969

7070
await JwtTokenValidation.authenticate_request(activity, header, credentials)
7171

7272
assert MicrosoftAppCredentials.is_trusted_service('https://smba.trafficmanager.net/amer-client-ss.msg/')
7373

74+
@pytest.mark.asyncio
75+
async def test_channel_msa_header_from_user_specified_tenant(self):
76+
activity = Activity(service_url = 'https://smba.trafficmanager.net/amer-client-ss.msg/')
77+
header = 'Bearer ' + await MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F', 'microsoft.com').get_access_token(True)
78+
credentials = SimpleCredentialProvider('2cd87869-38a0-4182-9251-d056e8f0ac24', '')
79+
80+
claims = await JwtTokenValidation.authenticate_request(activity, header, credentials)
81+
82+
assert claims.get_claim_value("tid") == '72f988bf-86f1-41af-91ab-2d7cd011db47'
83+
7484
@pytest.mark.asyncio
7585
# Tests with a valid Token and invalid service url; and ensures that Service url is NOT added to Trusted service url list.
7686
async def test_channel_msa_header_invalid_service_url_should_not_be_trusted(self):
7787
activity = Activity(service_url = 'https://webchat.botframework.com/')
78-
header = 'Bearer ' + MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
88+
header = 'Bearer ' + await MicrosoftAppCredentials('2cd87869-38a0-4182-9251-d056e8f0ac24', '2.30Vs3VQLKt974F').get_access_token()
7989
credentials = SimpleCredentialProvider('7f74513e-6f96-4dbc-be9d-9a81fea22b88', '')
8090

8191
with pytest.raises(Exception) as excinfo:
8292
await JwtTokenValidation.authenticate_request(activity, header, credentials)
8393
assert 'Unauthorized' in str(excinfo.value)
8494

8595
assert not MicrosoftAppCredentials.is_trusted_service('https://webchat.botframework.com/')
86-
96+
8797
@pytest.mark.asyncio
8898
# Tests with no authentication header and makes sure the service URL is not added to the trusted list.
8999
async def test_channel_authentication_disabled_should_be_anonymous(self):

libraries/botframework-connector/tests/test_conversations.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
import asyncio
23
from azure_devtools.scenario_tests import ReplayableTest
34

45
from botbuilder.schema import *
@@ -15,21 +16,21 @@
1516
CONVERSATION_ID = 'B21UTEF8S:T03CWQ0QB:D2369CT7C'
1617

1718

18-
def get_auth_token():
19+
async def get_auth_token():
1920
try:
2021
from .app_creds_real import MICROSOFT_APP_ID, MICROSOFT_APP_PASSWORD
2122
# Define a "app_creds_real.py" file with your bot credentials as follows:
2223
# MICROSOFT_APP_ID = '...'
2324
# MICROSOFT_APP_PASSWORD = '...'
24-
return MicrosoftAppCredentials(
25+
return await MicrosoftAppCredentials(
2526
MICROSOFT_APP_ID,
2627
MICROSOFT_APP_PASSWORD).get_access_token()
2728
except ImportError:
2829
return 'STUB_ACCESS_TOKEN'
2930

3031

31-
auth_token = get_auth_token()
32-
32+
loop = asyncio.get_event_loop()
33+
auth_token = loop.run_until_complete(get_auth_token())
3334

3435
class ConversationTest(ReplayableTest):
3536
def __init__(self, method_name):

0 commit comments

Comments
 (0)
0