7
7
import os
8
8
from typing import List , Callable , Awaitable , Union , Dict
9
9
from msrest .serialization import Model
10
+ from botbuilder .core import InvokeResponse
10
11
from botbuilder .schema import (
11
12
Activity ,
13
+ ActivityTypes ,
12
14
ConversationAccount ,
13
15
ConversationParameters ,
14
16
ConversationReference ,
19
21
from botframework .connector .auth import (
20
22
AuthenticationConstants ,
21
23
ChannelValidation ,
24
+ ClaimsIdentity ,
25
+ CredentialProvider ,
22
26
GovernmentChannelValidation ,
23
27
GovernmentConstants ,
24
28
MicrosoftAppCredentials ,
25
29
JwtTokenValidation ,
30
+ SkillValidation ,
26
31
SimpleCredentialProvider ,
27
32
)
28
33
from botframework .connector .token_api import TokenApiClient
@@ -71,17 +76,20 @@ def __init__(
71
76
oauth_endpoint : str = None ,
72
77
open_id_metadata : str = None ,
73
78
channel_service : str = None ,
79
+ credential_provider : CredentialProvider = None ,
74
80
):
75
81
self .app_id = app_id
76
82
self .app_password = app_password
77
83
self .channel_auth_tenant = channel_auth_tenant
78
84
self .oauth_endpoint = oauth_endpoint
79
85
self .open_id_metadata = open_id_metadata
80
86
self .channel_service = channel_service
87
+ self .credential_provider = credential_provider
81
88
82
89
83
90
class BotFrameworkAdapter (BotAdapter , UserTokenProvider ):
84
91
_INVOKE_RESPONSE_KEY = "BotFrameworkAdapter.InvokeResponse"
92
+ _BOT_IDENTITY_KEY = "BotIdentity"
85
93
86
94
def __init__ (self , settings : BotFrameworkAdapterSettings ):
87
95
super (BotFrameworkAdapter , self ).__init__ ()
@@ -98,8 +106,11 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
98
106
self .settings .app_password ,
99
107
self .settings .channel_auth_tenant ,
100
108
)
101
- self ._credential_provider = SimpleCredentialProvider (
102
- self .settings .app_id , self .settings .app_password
109
+ self ._credential_provider = (
110
+ settings .credential_provider
111
+ or SimpleCredentialProvider (
112
+ self .settings .app_id , self .settings .app_password
113
+ )
103
114
)
104
115
self ._is_emulating_oauth_cards = False
105
116
@@ -117,6 +128,8 @@ def __init__(self, settings: BotFrameworkAdapterSettings):
117
128
GovernmentConstants .TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
118
129
)
119
130
131
+ self ._APP_CREDENTIALS_CACHE : Dict [str , MicrosoftAppCredentials ] = {}
132
+
120
133
async def continue_conversation (
121
134
self , bot_id : str , reference : ConversationReference , callback : Callable
122
135
):
@@ -227,6 +240,40 @@ async def process_activity(self, req, auth_header: str, logic: Callable):
227
240
228
241
return await self .run_pipeline (context , logic )
229
242
243
+ async def process_activity_with_claims (
244
+ self ,
245
+ identity : ClaimsIdentity ,
246
+ activity : Activity ,
247
+ logic : Callable [[TurnContext ], Awaitable ],
248
+ ) -> InvokeResponse :
249
+ if not activity :
250
+ raise TypeError (f"{ Activity .__name__ } can not be None" )
251
+
252
+ context = TurnContext (self , activity )
253
+
254
+ context .turn_state [BotFrameworkAdapter ._BOT_IDENTITY_KEY ] = identity
255
+ context .turn_state ["BotCallbackHandler" ] = logic
256
+
257
+ client = await self .create_connector_client_with_claims (
258
+ activity .service_url , identity
259
+ )
260
+ context .turn_state [client .__class__ .__name__ ] = client
261
+
262
+ await self .run_pipeline (context , logic )
263
+
264
+ # Handle Invoke scenarios, which deviate from the request/response model in that
265
+ # the Bot will return a specific body and return code.
266
+ if activity .type == ActivityTypes .invoke :
267
+ activity_invoke_response = context .turn_state .get (self ._INVOKE_RESPONSE_KEY )
268
+ if not activity_invoke_response :
269
+ return InvokeResponse (status = 501 )
270
+
271
+ return activity_invoke_response
272
+
273
+ # For all non-invoke scenarios, the HTTP layers above don't have to mess
274
+ # with the Body and return codes.
275
+ return None
276
+
230
277
async def authenticate_request (self , request : Activity , auth_header : str ):
231
278
"""
232
279
Allows for the overriding of authentication in unit tests.
@@ -579,6 +626,31 @@ def create_connector_client(self, service_url: str) -> ConnectorClient:
579
626
client .config .add_user_agent (USER_AGENT )
580
627
return client
581
628
629
+ def create_connector_client_with_claims (
630
+ self , service_url : str , identity : ClaimsIdentity = None
631
+ ) -> ConnectorClient :
632
+ credentials : MicrosoftAppCredentials = None
633
+ if identity :
634
+ # For requests from channel App Id is in Audience claim of JWT token. For emulator it is in AppId claim. For
635
+ # unauthenticated requests we have anonymous identity provided auth is disabled.
636
+ # For Activities coming from Emulator AppId claim contains the Bot's AAD AppId.
637
+ bot_app_id_claim = identity .claims .get (
638
+ AuthenticationConstants .AUDIENCE_CLAIM
639
+ ) or identity .claims .get (AuthenticationConstants .APP_ID_CLAIM )
640
+
641
+ # For anonymous requests (requests with no header) appId is not set in claims.
642
+ if bot_app_id_claim :
643
+ scope = None
644
+ if SkillValidation .is_skill_claim (identity .claims ):
645
+ # The skill connector has the target skill in the OAuthScope.
646
+ scope = JwtTokenValidation .get_app_id_from_claims (identity .claims )
647
+
648
+ credentials = await self ._get_app_credentials (bot_app_id_claim , scope )
649
+
650
+ client = ConnectorClient (credentials , base_url = service_url )
651
+ client .config .add_user_agent (USER_AGENT )
652
+ return client
653
+
582
654
def create_token_api_client (self , service_url : str ) -> TokenApiClient :
583
655
client = TokenApiClient (self ._credentials , service_url )
584
656
client .config .add_user_agent (USER_AGENT )
@@ -593,7 +665,6 @@ async def emulate_oauth_cards(
593
665
await EmulatorApiClient .emulate_oauth_cards (self ._credentials , url , emulate )
594
666
595
667
def oauth_api_url (self , context_or_service_url : Union [TurnContext , str ]) -> str :
596
- url = None
597
668
if self ._is_emulating_oauth_cards :
598
669
url = (
599
670
context_or_service_url .activity .service_url
@@ -622,3 +693,36 @@ def check_emulating_oauth_cards(self, context: TurnContext):
622
693
)
623
694
):
624
695
self ._is_emulating_oauth_cards = True
696
+
697
+ async def _get_app_credentials (
698
+ self , app_id : str , oauth_scope : str
699
+ ) -> MicrosoftAppCredentials :
700
+ if not app_id :
701
+ return MicrosoftAppCredentials (None , None )
702
+
703
+ cache_key = f"{ app_id } { oauth_scope } "
704
+ app_credentials = self ._APP_CREDENTIALS_CACHE .get (cache_key )
705
+
706
+ if app_credentials :
707
+ return app_credentials
708
+
709
+ # If app credentials were provided, use them as they are the preferred choice moving forward
710
+ if self ._credentials .microsoft_app_id :
711
+ # Cache credentials
712
+ self ._APP_CREDENTIALS_CACHE [cache_key ] = self ._credentials
713
+ return self ._credentials
714
+
715
+ app_password = await self ._credential_provider .get_app_password (app_id )
716
+ app_credentials = MicrosoftAppCredentials (
717
+ app_id , app_password , oauth_scope = oauth_scope
718
+ )
719
+ if JwtTokenValidation .is_government (self .settings .channel_service ):
720
+ app_credentials .oauth_endpoint = (
721
+ GovernmentConstants .TO_CHANNEL_FROM_BOT_LOGIN_URL
722
+ )
723
+ app_credentials .oauth_scope = (
724
+ GovernmentConstants .TO_CHANNEL_FROM_BOT_OAUTH_SCOPE
725
+ )
726
+
727
+ self ._APP_CREDENTIALS_CACHE [cache_key ] = app_credentials
728
+ return app_credentials
0 commit comments