8000 Merge pull request #732 from microsoft/trboehre/auth · guptarohan41/botbuilder-python@9fbc318 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9fbc318

Browse files
authored
Merge pull request microsoft#732 from microsoft/trboehre/auth
AppCredentials object model and update to MSAL
2 parents 863be5f + 81a855c commit 9fbc318

File tree

8 files changed

+357
-245
lines changed

8 files changed

+357
-245
lines changed

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

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

4+
# pylint: disable=too-many-lines
5+
46
import asyncio
57
import base64
68
import json
@@ -22,6 +24,7 @@
2224
JwtTokenValidation,
2325
SimpleCredentialProvider,
2426
SkillValidation,
27+
CertificateAppCredentials,
2528
)
2629
from botframework.connector.token_api import TokenApiClient
2730
from botframework.connector.token_api.models import TokenStatus
@@ -76,13 +79,15 @@ class BotFrameworkAdapterSettings:
7679
def __init__(
7780
self,
7881
app_id: str,
79-
app_password: str,
82+
app_password: str = None,
8083
channel_auth_tenant: str = None,
8184
oauth_endpoint: str = None,
8285
open_id_metadata: str = None,
8386
channel_service: str = None,
8487
channel_provider: ChannelProvider = None,
8588
auth_configuration: AuthenticationConfiguration = None,
89+
certificate_thumbprint: str = None,
90+
certificate_private_key: str = None,
8691
):
8792
"""
8893
Contains the settings used to initialize a :class:`BotFrameworkAdapter` instance.
@@ -104,6 +109,15 @@ def __init__(
104109
:type channel_provider: :class:`botframework.connector.auth.ChannelProvider`
105110
:param auth_configuration:
106111
:type auth_configuration: :class:`botframework.connector.auth.AuthenticationConfiguration`
112+
:param certificate_thumbprint: X509 thumbprint
113+
:type certificate_thumbprint: str
114+
:param certificate_private_key: X509 private key
115+
:type certificate_private_key: str
116+
117+
.. remarks::
118+
For credentials authorization, both app_id and app_password are required.
119+
For certificate authorization, app_id, certificate_thumbprint, and certificate_private_key are required.
120+
107121
"""
108122
self.app_id = app_id
109123
self.app_password = app_password
@@ -113,6 +127,8 @@ def __init__(
113127
self.channel_service = channel_service
114128
self.channel_provider = channel_provider
115129
self.auth_configuration = auth_configuration or AuthenticationConfiguration()
130+
self.certificate_thumbprint = certificate_thumbprint
131+
self.certificate_private_key = certificate_private_key
116132

117133

118134
class BotFrameworkAdapter(BotAdapter, UserTokenProvider):
@@ -141,23 +157,42 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
141157
"""
142158
super(BotFrameworkAdapter, self).__init__()
143159
self.settings = settings or BotFrameworkAdapterSettings("", "")
160+
161+
# If settings.certificate_thumbprint & settings.certificate_private_key are provided,
162+
# use CertificateAppCredentials.
163+
if self.settings.certificate_thumbprint and settings.certificate_private_key:
164+
self._credentials = CertificateAppCredentials(
165+
self.settings.app_id,
166+
self.settings.certificate_thumbprint,
167+
self.settings.certificate_private_key,
168+
self.settings.channel_auth_tenant,
169+
)
170+
self._credential_provider = SimpleCredentialProvider(
171+
self.settings.app_id, ""
172+
)
173+
else:
174+
self._credentials = MicrosoftAppCredentials(
175+
self.settings.app_id,
176+
self.settings.app_password,
177+
self.settings.channel_auth_tenant,
178+
)
179+
self._credential_provider = SimpleCredentialProvider(
180+
self.settings.app_id, self.settings.app_password
181+
)
182+
183+
self._is_emulating_oauth_cards = False
184+
185+
# If no channel_service or open_id_metadata values were passed in the settings, check the
186+
# process' Environment Variables for values.
187+
# These values may be set when a bot is provisioned on Azure and if so are required for
188+
# the bot to properly work in Public Azure or a National Cloud.
144189
self.settings.channel_service = self.settings.channel_service or os.environ.get(
145190
AuthenticationConstants.CHANNEL_SERVICE
146191
)
147-
148192
self.settings.open_id_metadata = (
149193
self.settings.open_id_metadata
150194
or os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY)
151195
)
152-
self._credentials = MicrosoftAppCredentials(
153-
self.settings.app_id,
154-
self.settings.app_password,
155-
self.settings.channel_auth_tenant,
156-
)
157-
self._credential_provider = SimpleCredentialProvider(
158-
self.settings.app_id, self.settings.app_password
159-
)
160-
self._is_emulating_oauth_cards = False
161196

162197
if self.settings.open_id_metadata:
163198
ChannelValidation.open_id_metadata_endpoint = self.settings.open_id_metadata
@@ -878,35 +913,39 @@ async def create_connector_client(
878913
879914
:return: An instance of the :class:`ConnectorClient` class
880915
"""
916+
917+
# Anonymous claims and non-skill claims should fall through without modifying the scope.
918+
credentials = self._credentials
919+
881920
if identity:
882921
bot_app_id_claim = identity.claims.get(
883922
AuthenticationConstants.AUDIENCE_CLAIM
884923
) or identity.claims.get(AuthenticationConstants.APP_ID_CLAIM)
885924

886-
credentials = None
887925
if bot_app_id_claim and SkillValidation.is_skill_claim(identity.claims):
888926
scope = JwtTokenValidation.get_app_id_from_claims(identity.claims)
889927

890-
password = await self._credential_provider.get_app_password(
891-
bot_app_id_claim
892-
)
893-
credentials = MicrosoftAppCredentials(
894-
bot_app_id_claim, password, oauth_scope=scope
895-
)
896-
if (
897-
10000 self.settings.channel_provider
898-
and self.settings.channel_provider.is_government()
899-
):
900-
credentials.oauth_endpoint = (
901-
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
928+
# Do nothing, if the current credentials and its scope are valid for the skill.
929+
# i.e. the adapter instance is pre-configured to talk with one skill.
930+
# Otherwise we will create a new instance of the AppCredentials
931+
# so self._credentials.oauth_scope isn't overridden.
932+
if self._credentials.oauth_scope != scope:
933+
password = await self._credential_provider.get_app_password(
934+
bot_app_id_claim
902935
)
903-
credentials.oauth_scope = (
904-
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
936+
credentials = MicrosoftAppCredentials(
937+
bot_app_id_claim, password, oauth_scope=scope
905938
)
906-
else:
907-
credentials = self._credentials
908-
else:
909-
credentials = self._credentials
939+
if (
940+
self.settings.channel_provider
941+
and self.settings.channel_provider.is_government()
942+
):
943+
credentials.oauth_endpoint = (
944+
GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL
945+
)
946+
credentials.oauth_scope = (
947+
GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
948+
)
910949

911950
client_key = (
912951
f"{service_url}{credentials.microsoft_app_id if credentials else ''}"
Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
# coding=utf-8
2-
# --------------------------------------------------------------------------
3-
# Copyright (c) Microsoft Corporation. All rights reserved.
4-
# Licensed under the MIT License. See License.txt in the project root for
5-
# license information.
6-
#
7-
# Code generated by Microsoft (R) AutoRest Code Generator.
8-
# Changes may cause incorrect behavior and will be lost if the code is
9-
# regenerated.
10-
# --------------------------------------------------------------------------
11-
# pylint: disable=missing-docstring
12-
from .authentication_constants import *
13-
from .government_constants import *
14-
from .channel_provider import *
15-
from .simple_channel_provider import *
16-
from .microsoft_app_credentials import *
17-
from .claims_identity import *
18-
from .jwt_token_validation import *
19-
from .credential_provider import *
20-
from .channel_validation import *
21-
from .emulator_validation import *
22-
from .jwt_token_extractor import *
23-
from .authentication_configuration import *
1+
# coding=utf-8
2+
# --------------------------------------------------------------------------
3+
# Copyright (c) Microsoft Corporation. All rights reserved.
4+
# Licensed under the MIT License. See License.txt in the project root for
5+
# license information.
6+
#
7+
# Code generated by Microsoft (R) AutoRest Code Generator.
8+
# Changes may cause incorrect behavior and will be lost if the code is
9+
# regenerated.
10+
# --------------------------------------------------------------------------
11+
# pylint: disable=missing-docstring
12+
from .authentication_constants import *
13+
from .government_constants import *
14+
from .channel_provider import *
15+
from .simple_channel_provider import *
16+
from .microsoft_app_credentials import *
17+
from .certificate_app_credentials import *
18+
from .claims_identity import *
19+
from .jwt_token_validation import *
20+
from .credential_provider import *
21+
from .channel_validation import *
22+
from .emulator_validation import *
23+
from .jwt_token_extractor import *
24+
from .authentication_configuration import *
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from datetime import datetime, timedelta
5+
from urllib.parse import urlparse
6+
7+
import requests
8+
from msrest.authentication import Authentication
9+
10+
from botframework.connector.auth import AuthenticationConstants
11+
12+
13+
class AppCredentials(Authentication):
14+
"""
15+
Base class for token retrieval. Subclasses MUST override get_access_token in
16+
order to supply a valid token for the specific credentials.
17+
"""
18+
19+
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+
}
29+
cache = {}
30+
31+
def __init__(
32+
self,
33+
app_id: str = None,
34+
channel_auth_tenant: str = None,
35+
oauth_scope: str = None,
36+
):
37+
"""
38+
Initializes a new instance of MicrosoftAppCredentials class
39+
:param channel_auth_tenant: Optional. The oauth token tenant.
40+
"""
41+
tenant = (
42+
channel_auth_tenant
43+
if channel_auth_tenant
44+
else AuthenticationConstants.DEFAULT_CHANNEL_AUTH_TENANT
45+
)
46+
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
51+
)
52+
53+
self.microsoft_app_id = app_id
54+
55+
@staticmethod
56+
def trust_service_url(service_url: str, expiration=None):
57+
"""
58+
Checks if the service url is for a trusted host or not.
59+
:param service_url: The service url.
60+
: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.
62+
"""
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
68+
69+
@staticmethod
70+
def is_trusted_service(service_url: str) -> bool:
71+
"""
72+
Checks if the service url is for a trusted host or not.
73+
:param service_url: The service url.
74+
:returns: True if the host of the service url is trusted; False otherwise.
75+
"""
76+
host = urlparse(service_url).hostname
77+
if host is not None:
78+
return AppCredentials._is_trusted_url(host)
79+
return False
80+
81+
@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))
85+
86+
# pylint: disable=arguments-differ
87+
def signed_session(self, session: requests.Session = None) -> requests.Session:
88+
"""
89+
Gets the signed session. This is called by the msrest package
90+
:returns: Signed requests.Session object
91+
"""
92+
if not session:
93+
session = requests.Session()
94+
95+
if not self._should_authorize(session):
96+
session.headers.pop("Authorization", None)
97+
else:
98+
auth_token = self.get_access_token()
99+
header = "{} {}".format("Bearer", auth_token)
100+
session.headers["Authorization"] = header
101+
102+
return session
103+
104+
def _should_authorize(
105+
self, session: requests.Session # pylint: disable=unused-argument
106+
) -> bool:
107+
return True
108+
109+
def get_access_token(self, force_refresh: bool = False) -> str:
110+
"""
111+
Returns a token for the current AppCredentials.
112+
:return: The token
113+
"""
114+
raise NotImplementedError()

0 commit comments

Comments
 (0)
0