8000 Port Token Endorsements · dong-cg/botbuilder-python@2bbe72d · GitHub
[go: up one dir, main page]

Skip to content

Commit 2bbe72d

Browse files
author
Amit Stein
committed
Port Token Endorsements
1 parent ec47b07 commit 2bbe72d

File tree

9 files changed

+175
-66
lines changed

9 files changed

+175
-66
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ async def authenticate_request(self, request: Activity, auth_header: str):
9999
:param auth_header:
100100
:return:
101101
"""
102-
await JwtTokenValidation.assert_valid_activity(request, auth_header, self._credential_provider)
102+
await JwtTokenValidation.authenticate_request(request, auth_header, self._credential_provider)
103103

104104
def create_context(self, activity):
105105
"""

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class ChannelValidation:
2222
)
2323

2424
@staticmethod
25-
async def authenticate_token_service_url(auth_header: str, credentials: CredentialProvider, service_url: str) -> ClaimsIdentity:
25+
async def authenticate_token_service_url(auth_header: str, credentials: CredentialProvider, service_url: str, channel_id: str) -> ClaimsIdentity:
2626
""" Validate the incoming Auth Header
2727
2828
Validate the incoming Auth Header as a token sent from the Bot Framework Service.
@@ -39,7 +39,7 @@ async def authenticate_token_service_url(auth_header: str, credentials: Credenti
3939
:raises Exception:
4040
"""
4141
identity = await asyncio.ensure_future(
42-
ChannelValidation.authenticate_token(auth_header, credentials))
42+
ChannelValidation.authenticate_token(auth_header, credentials, channel_id))
4343

4444
service_url_claim = identity.get_claim_value(ChannelValidation.SERVICE_URL_CLAIM)
4545
if service_url_claim != service_url:
@@ -49,7 +49,7 @@ async def authenticate_token_service_url(auth_header: str, credentials: Credenti
4949
return identity
5050

5151
@staticmethod
52-
async def authenticate_token(auth_header: str, credentials: CredentialProvider) -> ClaimsIdentity:
52+
async def authenticate_token(auth_header: str, credentials: CredentialProvider, channel_id: str) -> ClaimsIdentity:
5353
""" Validate the incoming Auth Header
5454
5555
Validate the incoming Auth Header as a token sent from the Bot Framework Service.
@@ -69,7 +69,7 @@ async def authenticate_token(auth_header: str, credentials: CredentialProvider)
6969
Constants.ALLOWED_SIGNING_ALGORITHMS)
7070

7171
identity = await asyncio.ensure_future(
72-
token_extractor.get_identity_from_auth_header(auth_header))
72+
token_extractor.get_identity_from_auth_header(auth_header, channel_id))
7373
if not identity:
7474
# No valid identity. Not Authorized.
7575
raise Exception('Unauthorized. No valid identity.')

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def is_token_from_emulator(auth_header: str) -> bool:
8181
return True
8282

8383
@staticmethod
84-
async def authenticate_emulator_token(auth_header: str, credentials: CredentialProvider) -> ClaimsIdentity:
84+
async def authenticate_emulator_token(auth_header: str, credentials: CredentialProvider, channel_id: str) -> ClaimsIdentity:
8585
""" Validate the incoming Auth Header
8686
8787
< 67ED span class=pl-s> Validate the incoming Auth Header as a token sent from the Bot Framework Service.
@@ -101,7 +101,7 @@ async def authenticate_emulator_token(auth_header: str, credentials: CredentialP
101101
Constants.ALLOWED_SIGNING_ALGORITHMS)
102102

103103
identity = await asyncio.ensure_future(
104-
token_extractor.get_identity_from_auth_header(auth_header))
104+
token_extractor.get_identity_from_auth_header(auth_header, channel_id))
105105
if not identity:
106106
# No valid identity. Not Authorized.
107107
raise Exception('Unauthorized. No valid identity.')
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import List
2+
3+
class EndorsementsValidator():
4+
@staticmethod
5+
def validate(channel_id: str, endorsements: List[str]):
6+
# If the Activity came in and doesn't have a Channel ID then it's making no
7+
# assertions as to who endorses it. This means it should pass.
8+
if not channel_id:
9+
return True
10+
11+
if endorsements == None:
12+
raise ValueError('Argument endorsements is null.')
13+
14+
# The Call path to get here is:
15+
# JwtTokenValidation.AuthenticateRequest
16+
# ->
17+
# JwtTokenValidation.ValidateAuthHeader
18+
# ->
19+
# ChannelValidation.AuthenticateChannelToken
20+
# ->
21+
# JWTTokenExtractor
22+
23+
# Does the set of endorsements match the channelId that was passed in?
24+
25+
# ToDo: Consider moving this to a HashSet instead of a string
26+
# array, to make lookups O(1) instead of O(N). To give a sense
27+
# of scope, tokens from WebChat have about 10 endorsements, and
28+
# tokens coming from Teams have about 20.
29+
30+
endorsementPresent = channel_id in endorsements
31+
return endorsementPresent

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66
import jwt
77
from .claims_identity import ClaimsIdentity
88
from .verify_options import VerifyOptions
9+
from .endorsements_validator import EndorsementsValidator
910

1011
class JwtTokenExtractor:
1112
metadataCache = {}
1213

13-
def __init__(self, validationParams: VerifyOptions, metadata_url: str, allowedAlgorithms: list, validator=None):
14+
def __init__(self, validationParams: VerifyOptions, metadata_url: str, allowedAlgorithms: list):
1415
self.validation_parameters = validationParams
1516
self.validation_parameters.algorithms = allowedAlgorithms
1617
self.open_id_metadata = JwtTokenExtractor.get_open_id_metadata(metadata_url)
17-
self.validator = validator if validator is not None else lambda x: True
1818

1919
@staticmethod
2020
def get_open_id_metadata(metadata_url: str):
@@ -24,15 +24,15 @@ def get_open_id_metadata(metadata_url: str):
2424
JwtTokenExtractor.metadataCache.setdefault(metadata_url, metadata)
2525
return metadata
2626

27-
async def get_identity_from_auth_header(self, auth_header: str) -> ClaimsIdentity:
27+
async def get_identity_from_auth_header(self, auth_header: str, channel_id: str) -> ClaimsIdentity:
2828
if not auth_header:
2929
return None
3030
parts = auth_header.split(" ")
3131
if len(parts) == 2:
32-
return await self.get_identity(parts[0], parts[1])
32+
return await self.get_identity(parts[0], parts[1], channel_id)
3333
return None
3434

35-
async def get_identity(self, schema: str, parameter: str) -> ClaimsIdentity:
35+
async def get_identity(self, schema: str, parameter: str, channel_id) -> ClaimsIdentity:
3636
# No header in correct scheme or no token
3737
if schema != "Bearer" or not parameter:
3838
return None
@@ -42,7 +42,7 @@ async def get_identity(self, schema: str, parameter: str) -> ClaimsIdentity:
4242
return None
4343

4444
try:
45-
return await self._validate_token(parameter)
45+
return await self._validate_token(parameter, channel_id)
4646
except:
4747
raise
4848

@@ -54,7 +54,7 @@ def _has_allowed_issuer(self, jwt_token: str) -> bool:
5454

5555
return issuer is self.validation_parameters.issuer
5656

57-
async def _validate_token(self, jwt_token: str) -> ClaimsIdentity:
57+
async def _validate_token(self, jwt_token: str, channel_id: str) -> ClaimsIdentity:
5858
headers = jwt.get_unverified_header(jwt_token)
5959

6060
# Update the signing tokens from the last refresh
@@ -65,9 +65,8 @@ async def _validate_token(self, jwt_token: str) -> ClaimsIdentity:
6565
if headers.get("alg", None) not in self.validation_parameters.algorithms:
6666
raise Exception('Token signing algorithm not in allowed list')
6767

68-
if self.validator is not None:
69-
if not self.validator(metadata.endorsements):
70-
raise Exception('Could not validate endorsement key')
68+
if not EndorsementsValidator.validate(channel_id, metadata.endorsements):
69+
raise Exception('Could not validate endorsement key')
7170

7271
options = {
7372
'verify_aud': False,

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
from .channel_validation import ChannelValidation
55
from .microsoft_app_credentials import MicrosoftAppCredentials
66
from .credential_provider import CredentialProvider
7+
from .claims_identity import ClaimsIdentity
78

89
class JwtTokenValidation:
910

1011
@staticmethod
11-
async def assert_valid_activity(activity: Activity, auth_header: str, credentials: CredentialProvider):
12-
"""Validates the security tokens required by the Bot Framework Protocol. Throws on any exceptions.
12+
async def authenticate_request(activity: Activity, auth_header: str, credentials: CredentialProvider) -> ClaimsIdentity:
13+
"""Authenticates the request and sets the service url in the set of trusted urls.
1314
1415
:param activity: The incoming Activity from the Bot Framework or the Emulator
1516
:type activity: ~botframework.connector.models.Activity
@@ -30,12 +31,22 @@ async def assert_valid_activity(activity: Activity, auth_header: str, credential
3031
# No Auth Header. Auth is required. Request is not authorized.
3132
raise Exception('Unauthorized Access. Request is not authorized')
3233

33-
using_emulator = EmulatorValidation.is_token_from_emulator(auth_header)
34-
if using_emulator:
35-
await EmulatorValidation.authenticate_emulator_token(auth_header, credentials)
36-
else:
37-
await ChannelValidation.authenticate_token_service_url(
38-
auth_header, credentials, activity.service_url)
34+
claims_identity = await JwtTokenValidation.validate_auth_header(auth_header, credentials, activity.channel_id, activity.service_url)
3935

4036
# On the standard Auth path, we need to trust the URL that was incoming.
4137
MicrosoftAppCredentials.trust_service_url(activity.service_url)
38+
39+
return claims_identity
40+
41+
@staticmethod
42+
async def validate_auth_header(auth_header: str, credentials: CredentialProvider, channel_id: str, service_url: str = None) -> ClaimsIdentity:
43+
if not auth_header:
44+
raise ValueError('argument auth_header is null')
45+
using_emulator = EmulatorValidation.is_token_from_emulator(auth_header)
46+
if using_emulator:
47+
return await EmulatorValidation.authenticate_emulator_token(auth_header, credentials, channel_id)
48+
else:
49+
if service_url:
50+
return await ChannelValidation.authenticate_token_service_url(auth_header, credentials, service_url, channel_id)
51+
else:
52+
return await ChannelValidation.authenticate_token(auth_header, credentials, channel_id)

0 commit comments

Comments
 (0)
0