Skip to content

Commit a078027

Browse files
tracyboehrerTracy Boehrer
andauthored
SingleTenant support (#2055)
* SingleTenant support * Pylint corrections * Black correction * SingleTenant Gov correction * Ported Gov SingleTenant fixes from DotNet * Black correction * Pylint corrections * Black corrections for app_credentials * Corrected AppCredentials._should_set_token * Changed auth constant to match setting name * black corrections --------- Co-authored-by: Tracy Boehrer <[email protected]>
1 parent 184a2da commit a078027

25 files changed

+234
-233
lines changed

doc/SkillClaimsValidation.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,11 @@ ADAPTER = BotFrameworkAdapter(
4848
SETTINGS,
4949
)
5050
```
51+
52+
For SingleTenant type bots, the additional issuers must be added based on the tenant id:
53+
```python
54+
AUTH_CONFIG = AuthenticationConfiguration(
55+
claims_validator=AllowedSkillsClaimsValidator(CONFIG).claims_validator,
56+
tenant_id=the_tenant_id
57+
)
58+
```

libraries/botbuilder-adapters-slack/tests/test_slack_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import requests
1313
import pytest
1414

15-
SKIP = os.getenv("SlackChannel") == ''
15+
SKIP = os.getenv("SlackChannel") == ""
1616

1717

1818
class SlackClient(aiounittest.AsyncTestCase):

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

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -279,19 +279,6 @@ async def continue_conversation(
279279
context.turn_state[BotAdapter.BOT_CALLBACK_HANDLER_KEY] = callback
280280
context.turn_state[BotAdapter.BOT_OAUTH_SCOPE_KEY] = audience
281281

282-
# If we receive a valid app id in the incoming token claims, add the channel service URL to the
283-
# trusted services list so we can send messages back.
284-
# The service URL for skills is trusted because it is applied by the SkillHandler based on the original
285-
# request received by the root bot
286-
app_id_from_claims = JwtTokenValidation.get_app_id_from_claims(
287-
claims_identity.claims
288-
)
289-
if app_id_from_claims:
290-
if SkillValidation.is_skill_claim(
291-
claims_identity.claims
292-
) or await self._credential_provider.is_valid_appid(app_id_from_claims):
293-
AppCredentials.trust_service_url(reference.service_url)
294-
295282
client = await self.create_connector_client(
296283
reference.service_url, claims_identity, audience
297284
)

libraries/botbuilder-core/botbuilder/core/configuration_service_client_credentials_factory.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

libraries/botbuilder-core/tests/test_bot_framework_adapter.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -621,14 +621,8 @@ async def callback(context: TurnContext):
621621
scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY]
622622
assert AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE == scope
623623

624-
# Ensure the serviceUrl was added to the trusted hosts
625-
assert AppCredentials.is_trusted_service(channel_service_url)
626-
627624
refs = ConversationReference(service_url=channel_service_url)
628625

629-
# Ensure the serviceUrl is NOT in the trusted hosts
630-
assert not AppCredentials.is_trusted_service(channel_service_url)
631-
632626
await adapter.continue_conversation(
633627
refs, callback, claims_identity=skills_identity
634628
)
@@ -694,14 +688,8 @@ async def callback(context: TurnContext):
694688
scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY]
695689
assert skill_2_app_id == scope
696690

697-
# Ensure the serviceUrl was added to the trusted hosts
698-
assert AppCredentials.is_trusted_service(skill_2_service_url)
699-
700691
refs = ConversationReference(service_url=skill_2_service_url)
701692

702-
# Ensure the serviceUrl is NOT in the trusted hosts
703-
assert not AppCredentials.is_trusted_service(skill_2_service_url)
704-
705693
await adapter.continue_conversation(
706694
refs, callback, claims_identity=skills_identity, audience=skill_2_app_id
707695
)

libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_adapter.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,12 +188,6 @@ async def _http_authenticate_request(self, request: Request) -> bool:
188188
)
189189
)
190190

191-
# Add ServiceURL to the cache of trusted sites in order to allow token refreshing.
192-
self._credentials.trust_service_url(
193-
claims_identity.claims.get(
194-
AuthenticationConstants.SERVICE_URL_CLAIM
195-
)
196-
)
197191
self.claims_identity = claims_identity
198192
return True
199193
except Exception as error:

libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/configuration_service_client_credential_factory.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,38 @@ class ConfigurationServiceClientCredentialFactory(
1111
PasswordServiceClientCredentialFactory
1212
):
1313
def __init__(self, configuration: Any, *, logger: Logger = None) -> None:
14-
super().__init__(
15-
app_id=getattr(configuration, "APP_ID", None),
16-
password=getattr(configuration, "APP_PASSWORD", None),
17-
logger=logger,
14+
app_type = (
15+
configuration.APP_TYPE
16+
if hasattr(configuration, "APP_TYPE")
17+
else "MultiTenant"
1818
)
19+
app_id = configuration.APP_ID if hasattr(configuration, "APP_ID") else None
20+
app_password = (
21+
configuration.APP_PASSWORD
22+
if hasattr(configuration, "APP_PASSWORD")
23+
else None
24+
)
25+
app_tenantid = None
26+
27+
if app_type == "UserAssignedMsi":
28+
raise Exception("UserAssignedMsi APP_TYPE is not supported")
29+
30+
if app_type == "SingleTenant":
31+
app_tenantid = (
32+
configuration.APP_TENANTID
33+
if hasattr(configuration, "APP_TENANTID")
34+
else None
35+
)
36+
37+
if not app_id:
38+
raise Exception("Property 'APP_ID' is expected in configuration object")
39+
if not app_password:
40+
raise Exception(
41+
"Property 'APP_PASSWORD' is expected in configuration object"
42+
)
43+
if not app_tenantid:
44+
raise Exception(
45+
"Property 'APP_TENANTID' is expected in configuration object"
46+
)
47+
48+
super().__init__(app_id, app_password, app_tenantid, logger=logger)

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ async def post_activity(
4848
conversation_id: str,
4949
activity: Activity,
5050
) -> InvokeResponse:
51-
if not from_bot_id:
52-
raise TypeError("from_bot_id")
53-
if not to_bot_id:
54-
raise TypeError("to_bot_id")
5551
if not to_url:
5652
raise TypeError("to_url")
5753
if not service_url:
@@ -100,6 +96,7 @@ async def post_activity(
10096

10197
headers_dict = {
10298
"Content-type": "application/json; charset=utf-8",
99+
"x-ms-conversation-id": conversation_id,
103100
}
104101
if token:
105102
headers_dict.update(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ async def create_user_token_client(
166166

167167
credentials = await self._credentials_factory.create_credentials(
168168
app_id,
169-
audience=self._to_channel_from_bot_oauth_scope,
169+
oauth_scope=self._to_channel_from_bot_oauth_scope,
170170
login_endpoint=self._login_endpoint,
171171
validate_authority=True,
172172
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def __init__(
2424
):
2525
super(_GovernmentCloudBotFrameworkAuthentication, self).__init__(
2626
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE,
27-
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL,
27+
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX,
2828
CallerIdConstants.us_gov_channel,
2929
GovernmentConstants.CHANNEL_SERVICE,
3030
GovernmentConstants.OAUTH_URL_GOV,

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ async def create_user_token_client(
155155

156156
credentials = await self._credentials_factory.create_credentials(
157157
app_id,
158-
audience=self._to_channel_from_bot_oauth_scope,
158+
oauth_scope=self._to_channel_from_bot_oauth_scope,
159159
login_endpoint=self._to_channel_from_bot_login_url,
160160
validate_authority=self._validate_authority,
161161
)
@@ -274,6 +274,11 @@ async def _skill_validation_authenticate_channel_token(
274274
ignore_expiration=False,
275275
)
276276

277+
if self._auth_configuration.valid_token_issuers:
278+
validation_params.issuer.append(
279+
self._auth_configuration.valid_token_issuers
280+
)
281+
277282
# TODO: what should the openIdMetadataUrl be here?
278283
token_extractor = JwtTokenExtractor(
279284
validation_params,
@@ -362,6 +367,11 @@ async def _emulator_validation_authenticate_emulator_token(
362367
ignore_expiration=False,
363368
)
364369

370+
if self._auth_configuration.valid_token_issuers:
371+
to_bot_from_emulator_validation_params.issuer.append(
372+
self._auth_configuration.valid_token_issuers
373+
)
374+
365375
token_extractor = JwtTokenExtractor(
366376
to_bot_from_emulator_validation_params,
367377
metadata_url=self._to_bot_from_emulator_open_id_metadata_url,

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

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# Licensed under the MIT License.
33

4-
from datetime import datetime, timedelta
5-
from urllib.parse import urlparse
6-
74
import requests
85
from msrest.authentication import Authentication
96

10-
from botframework.connector.auth import AuthenticationConstants
7+
from .authentication_constants import AuthenticationConstants
118

129

1310
class AppCredentials(Authentication):
@@ -17,16 +14,8 @@ class AppCredentials(Authentication):
1714
"""
1815

1916
schema = "Bearer"
20-
21-
trustedHostNames = {
22-
# "state.botframework.com": datetime.max,
23-
# "state.botframework.azure.us": datetime.max,
24-
"api.botframework.com": datetime.max,
25-
"token.botframework.com": datetime.max,
26-
"api.botframework.azure.us": datetime.max,
27-
"token.botframework.azure.us": datetime.max,
28-
}
2917
cache = {}
18+
__tenant = None
3019

3120
def __init__(
3221
self,
@@ -38,50 +27,55 @@ def __init__(
3827
Initializes a new instance of MicrosoftAppCredentials class
3928
:param channel_auth_tenant: Optional. The oauth token tenant.
4029
"""
41-
tenant = (
42-
channel_auth_tenant
43-
if channel_auth_tenant
44-
else AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT
45-
)
30+
self.microsoft_app_id = app_id
31+
self.tenant = channel_auth_tenant
4632
self.oauth_endpoint = (
47-
AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX + tenant
48-
)
49-
self.oauth_scope = (
50-
oauth_scope or AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
33+
self._get_to_channel_from_bot_loginurl_prefix() + self.tenant
5134
)
35+
self.oauth_scope = oauth_scope or self._get_to_channel_from_bot_oauthscope()
5236

53-
self.microsoft_app_id = app_id
37+
def _get_default_channelauth_tenant(self) -> str:
38+
return AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT
39+
40+
def _get_to_channel_from_bot_loginurl_prefix(self) -> str:
41+
return AuthenticationConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX
42+
43+
def _get_to_channel_from_bot_oauthscope(self) -> str:
44+
return AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
45+
46+
@property
47+
def tenant(self) -> str:
48+
return self.__tenant
49+
50+
@tenant.setter
51+
def tenant(self, value: str):
52+
self.__tenant = value or self._get_default_channelauth_tenant()
5453

5554
@staticmethod
5655
def trust_service_url(service_url: str, expiration=None):
5756
"""
57+
Obsolete: trust_service_url is not a required part of the security model.
5858
Checks if the service url is for a trusted host or not.
5959
:param service_url: The service url.
6060
:param expiration: The expiration time after which this service url is not trusted anymore.
61-
:returns: True if the host of the service url is trusted; False otherwise.
6261
"""
63-
if expiration is None:
64-
expiration = datetime.now() + timedelta(days=1)
65-
host = urlparse(service_url).hostname
66-
if host is not None:
67-
AppCredentials.trustedHostNames[host] = expiration
6862

6963
@staticmethod
70-
def is_trusted_service(service_url: str) -> bool:
64+
def is_trusted_service(service_url: str) -> bool: # pylint: disable=unused-argument
7165
"""
66+
Obsolete: is_trusted_service is not a required part of the security model.
7267
Checks if the service url is for a trusted host or not.
7368
:param service_url: The service url.
7469
:returns: True if the host of the service url is trusted; False otherwise.
7570
"""
76-
host = urlparse(service_url).hostname
77-
if host is not None:
78-
return AppCredentials._is_trusted_url(host)
79-
return False
71+
return True
8072

8173
@staticmethod
82-
def _is_trusted_url(host: str) -> bool:
83-
expiration = AppCredentials.trustedHostNames.get(host, datetime.min)
84-
return expiration > (datetime.now() - timedelta(minutes=5))
74+
def _is_trusted_url(host: str) -> bool: # pylint: disable=unused-argument
75+
"""
76+
Obsolete: _is_trusted_url is not a required part of the security model.
77+
"""
78+
return True
8579

8680
# pylint: disable=arguments-differ
8781
def signed_session(self, session: requests.Session = None) -> requests.Session:
@@ -92,7 +86,7 @@ def signed_session(self, session: requests.Session = None) -> requests.Session:
9286
if not session:
9387
session = requests.Session()
9488

95-
if not self._should_authorize(session):
89+
if not self._should_set_token(session):
9690
session.headers.pop("Authorization", None)
9791
else:
9892
auth_token = self.get_access_token()
@@ -101,13 +95,13 @@ def signed_session(self, session: requests.Session = None) -> requests.Session:
10195

10296
return session
10397

104-
def _should_authorize(
98+
def _should_set_token(
10599
self, session: requests.Session # pylint: disable=unused-argument
106100
) -> bool:
107101
# We don't set the token if the AppId is not set, since it means that we are in an un-authenticated scenario.
108102
return (
109103
self.microsoft_app_id != AuthenticationConstants.ANONYMOUS_SKILL_APP_ID
110-
and self.microsoft_app_id is not None
104+
and self.microsoft_app_id
111105
)
112106

113107
def get_access_token(self, force_refresh: bool = False) -> str:

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,39 @@
33

44
from typing import Awaitable, Callable, Dict, List
55

6+
from .authentication_constants import AuthenticationConstants
7+
68

79
class AuthenticationConfiguration:
810
def __init__(
911
self,
1012
required_endorsements: List[str] = None,
1113
claims_validator: Callable[[List[Dict]], Awaitable] = None,
14+
valid_token_issuers: List[str] = None,
15+
tenant_id: str = None,
1216
):
1317
self.required_endorsements = required_endorsements or []
1418
self.claims_validator = claims_validator
19+
self.valid_token_issuers = valid_token_issuers or []
20+
21+
if tenant_id:
22+
self.add_tenant_issuers(self, tenant_id)
23+
24+
@staticmethod
25+
def add_tenant_issuers(authentication_configuration, tenant_id: str):
26+
authentication_configuration.valid_token_issuers.append(
27+
AuthenticationConstants.VALID_TOKEN_ISSUER_URL_TEMPLATE_V1.format(tenant_id)
28+
)
29+
authentication_configuration.valid_token_issuers.append(
30+
AuthenticationConstants.VALID_TOKEN_ISSUER_URL_TEMPLATE_V2.format(tenant_id)
31+
)
32+
authentication_configuration.valid_token_issuers.append(
33+
AuthenticationConstants.VALID_GOVERNMENT_TOKEN_ISSUER_URL_TEMPLATE_V1.format(
34+
tenant_id
35+
)
36+
)
37+
authentication_configuration.valid_token_issuers.append(
38+
AuthenticationConstants.VALID_GOVERNMENT_TOKEN_ISSUER_URL_TEMPLATE_V2.format(
39+
tenant_id
40+
)
41+
)

0 commit comments

Comments
 (0)