Skip to content

Commit 6f81404

Browse files
authored
Auth changes for skills (#420)
* auth changes for skills, tests pending * Moving imports due to circular dependencies * unit tests for oauth changes * Moving test dependency to correct requirement file * Removed useless todo note * solved PR comments
1 parent 73ff35a commit 6f81404

15 files changed

+558
-61
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
from typing import List
5+
6+
7+
class AuthenticationConfiguration:
8+
def __init__(self, required_endorsements: List[str] = None):
9+
self.required_endorsements = required_endorsements or []

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22

3+
from .authentication_configuration import AuthenticationConfiguration
34
from .verify_options import VerifyOptions
45
from .constants import Constants
56
from .jwt_token_extractor import JwtTokenExtractor
@@ -30,6 +31,7 @@ async def authenticate_channel_token_with_service_url(
3031
credentials: CredentialProvider,
3132
service_url: str,
3233
channel_id: str,
34+
auth_configuration: AuthenticationConfiguration = None,
3335
) -> ClaimsIdentity:
3436
""" Validate the incoming Auth Header
3537
@@ -48,7 +50,7 @@ async def authenticate_channel_token_with_service_url(
4850
"""
4951
identity = await asyncio.ensure_future(
5052
ChannelValidation.authenticate_channel_token(
51-
auth_header, credentials, channel_id
53+
auth_header, credentials, channel_id, auth_configuration
5254
)
5355
)
5456

@@ -63,7 +65,10 @@ async def authenticate_channel_token_with_service_url(
6365

6466
@staticmethod
6567
async def authenticate_channel_token(
66-
auth_header: str, credentials: CredentialProvider, channel_id: str
68+
auth_header: str,
69+
credentials: CredentialProvider,
70+
channel_id: str,
71+
auth_configuration: AuthenticationConfiguration = None,
6772
) -> ClaimsIdentity:
6873
""" Validate the incoming Auth Header
6974
@@ -78,6 +83,7 @@ async def authenticate_channel_token(
7883
:return: A valid ClaimsIdentity.
7984
:raises Exception:
8085
"""
86+
auth_configuration = auth_configuration or AuthenticationConfiguration()
8187
metadata_endpoint = (
8288
ChannelValidation.open_id_metadata_endpoint
8389
if ChannelValidation.open_id_metadata_endpoint
@@ -91,7 +97,9 @@ async def authenticate_channel_token(
9197
)
9298

9399
identity = await asyncio.ensure_future(
94-
token_extractor.get_identity_from_auth_header(auth_header, channel_id)
100+
token_extractor.get_identity_from_auth_header(
101+
auth_header, channel_id, auth_configuration.required_endorsements
102+
)
95103
)
96104

97105
return await ChannelValidation.validate_identity(identity, credentials)

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

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
14
import asyncio
25
import jwt
36

@@ -44,27 +47,14 @@ def is_token_from_emulator(auth_header: str) -> bool:
4447
4548
:return: True, if the token was issued by the Emulator. Otherwise, false.
4649
"""
47-
# The Auth Header generally looks like this:
48-
# "Bearer eyJ0e[...Big Long String...]XAiO"
49-
if not auth_header:
50-
# No token. Can't be an emulator token.
51-
return False
50+
from .jwt_token_validation import ( # pylint: disable=import-outside-toplevel
51+
JwtTokenValidation,
52+
)
5253

53-
parts = auth_header.split(" ")
54-
if len(parts) != 2:
55-
# Emulator tokens MUST have exactly 2 parts.
56-
# If we don't have 2 parts, it's not an emulator token
54+
if not JwtTokenValidation.is_valid_token_format(auth_header):
5755
return False
5856

59-
auth_scheme = parts[0]
60-
bearer_token = parts[1]
61-
62-
# We now have an array that should be:
63-
# [0] = "Bearer"
64-
# [1] = "[Big Long String]"
65-
if auth_scheme != "Bearer":
66-
# The scheme from the emulator MUST be "Bearer"
67-
return False
57+
bearer_token = auth_header.split(" ")[1]
6858

6959
# Parse the Big Long String into an actual token.
7060
token = jwt.decode(bearer_token, verify=False)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66

77
class EndorsementsValidator:
88
@staticmethod
9-
def validate(channel_id: str, endorsements: List[str]):
9+
def validate(expected_endorsement: str, endorsements: List[str]):
1010
# If the Activity came in and doesn't have a Channel ID then it's making no
1111
# assertions as to who endorses it. This means it should pass.
12-
if not channel_id:
12+
if not expected_endorsement:
1313
return True
1414

1515
if endorsements is None:
@@ -31,5 +31,5 @@ def validate(channel_id: str, endorsements: List[str]):
3131
# of scope, tokens from WebChat have about 10 endorsements, and
3232
# tokens coming from Teams have about 20.
3333

34-
endorsement_present = channel_id in endorsements
34+
endorsement_present = expected_endorsement in endorsements
3535
return endorsement_present

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from abc import ABC
55

6+
from .authentication_configuration import AuthenticationConfiguration
67
from .authentication_constants import AuthenticationConstants
78
from .channel_validation import ChannelValidation
89
from .claims_identity import ClaimsIdentity
@@ -26,6 +27,7 @@ async def authenticate_channel_token(
2627
credentials: CredentialProvider,
2728
channel_id: str,
2829
channel_service: str,
30+
auth_configuration: AuthenticationConfiguration = None,
2931
) -> ClaimsIdentity:
3032
endpoint = (
3133
ChannelValidation.open_id_metadata_endpoint
@@ -41,7 +43,7 @@ async def authenticate_channel_token(
4143
)
4244

4345
identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(
44-
auth_header, channel_id
46+
auth_header, channel_id, auth_configuration.required_endorsements
4547
)
4648
return await EnterpriseChannelValidation.validate_identity(
4749
identity, credentials
@@ -54,9 +56,10 @@ async def authenticate_channel_token_with_service_url(
5456
service_url: str,
5557
channel_id: str,
5658
channel_service: str,
59+
auth_configuration: AuthenticationConfiguration = None,
5760
) -> ClaimsIdentity:
5861
identity: ClaimsIdentity = await EnterpriseChannelValidation.authenticate_channel_token(
59-
auth_header, credentials, channel_id, channel_service
62+
auth_header, credentials, channel_id, channel_service, auth_configuration
6063
)
6164

6265
service_url_claim: str = identity.get_claim_value(

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from abc import ABC
55

6+
from .authentication_configuration import AuthenticationConfiguration
67
from .authentication_constants import AuthenticationConstants
78
from .claims_identity import ClaimsIdentity
89
from .credential_provider import CredentialProvider
@@ -24,8 +25,12 @@ class GovernmentChannelValidation(ABC):
2425

2526
@staticmethod
2627
async def authenticate_channel_token(
27-
auth_header: str, credentials: CredentialProvider, channel_id: str
28+
auth_header: str,
29+
credentials: CredentialProvider,
30+
channel_id: str,
31+
auth_configuration: AuthenticationConfiguration = None,
2832
) -> ClaimsIdentity:
33+
auth_configuration = auth_configuration or AuthenticationConfiguration()
2934
endpoint = (
3035
GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT
3136
if GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT
@@ -38,7 +43,7 @@ async def authenticate_channel_token(
3843
)
3944

4045
identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(
41-
auth_header, channel_id
46+
auth_header, channel_id, auth_configuration.required_endorsements
4247
)
4348
return await GovernmentChannelValidation.validate_identity(
4449
identity, credentials
@@ -50,9 +55,10 @@ async def authenticate_channel_token_with_service_url(
5055
credentials: CredentialProvider,
5156
service_url: str,
5257
channel_id: str,
58+
auth_configuration: AuthenticationConfiguration = None,
5359
) -> ClaimsIdentity:
5460
identity: ClaimsIdentity = await GovernmentChannelValidation.authenticate_channel_token(
55-
auth_header, credentials, channel_id
61+
auth_header, credentials, channel_id, auth_configuration
5662
)
5763

5864
service_url_claim: str = identity.get_claim_value(

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import json
55
from datetime import datetime, timedelta
6+
from typing import List
67
import requests
78
from jwt.algorithms import RSAAlgorithm
89
import jwt
@@ -33,17 +34,23 @@ def get_open_id_metadata(metadata_url: str):
3334
return metadata
3435

3536
async def get_identity_from_auth_header(
36-
self, auth_header: str, channel_id: str
37+
self, auth_header: str, channel_id: str, required_endorsements: List[str] = None
3738
) -> ClaimsIdentity:
3839
if not auth_header:
3940
return None
4041
parts = auth_header.split(" ")
4142
if len(parts) == 2:
42-
return await self.get_identity(parts[0], parts[1], channel_id)
43+
return await self.get_identity(
44+
parts[0], parts[1], channel_id, required_endorsements
45+
)
4346
return None
4447

4548
async def get_identity(
46-
self, schema: str, parameter: str, channel_id
49+
self,
50+
schema: str,
51+
parameter: str,
52+
channel_id: str,
53+
required_endorsements: List[str] = None,
4754
) -> ClaimsIdentity:
4855
# No header in correct scheme or no token
4956
if schema != "Bearer" or not parameter:
@@ -54,7 +61,9 @@ async def get_identity(
5461
return None
5562

5663
try:
57-
return await self._validate_token(parameter, channel_id)
64+
return await self._validate_token(
65+
parameter, channel_id, required_endorsements
66+
)
5867
except Exception as error:
5968
raise error
6069

@@ -64,27 +73,46 @@ def _has_allowed_issuer(self, jwt_token: str) -> bool:
6473
if issuer in self.validation_parameters.issuer:
6574
return True
6675

67-
return issuer is self.validation_parameters.issuer
76+
return issuer == self.validation_parameters.issuer
6877

69-
async def _validate_token(self, jwt_token: str, channel_id: str) -> ClaimsIdentity:
78+
async def _validate_token(
79+
self, jwt_token: str, channel_id: str, required_endorsements: List[str] = None
80+
) -> ClaimsIdentity:
81+
required_endorsements = required_endorsements or []
7082
headers = jwt.get_unverified_header(jwt_token)
7183

7284
# Update the signing tokens from the last refresh
7385
key_id = headers.get("kid", None)
7486
metadata = await self.open_id_metadata.get(key_id)
7587

7688
if key_id and metadata.endorsements:
89+
# Verify that channelId is included in endorsements
7790
if not EndorsementsValidator.validate(channel_id, metadata.endorsements):
7891
raise Exception("Could not validate endorsement key")
7992

93+
# Verify that additional endorsements are satisfied.
94+
# If no additional endorsements are expected, the requirement is satisfied as well
95+
for endorsement in required_endorsements:
96+
if not EndorsementsValidator.validate(
97+
endorsement, metadata.endorsements
98+
):
99+
raise Exception("Could not validate endorsement key")
100+
80101
if headers.get("alg", None) not in self.validation_parameters.algorithms:
81102
raise Exception("Token signing algorithm not in allowed list")
82103

83104
options = {
84105
"verify_aud": False,
85106
"verify_exp": not self.validation_parameters.ignore_expiration,
86107
}
87-
decoded_payload = jwt.decode(jwt_token, metadata.public_key, options=options)
108+
109+
decoded_payload = jwt.decode(
110+
jwt_token,
111+
metadata.public_key,
112+
leeway=self.validation_parameters.clock_tolerance,
113+
options=options,
114+
)
115+
88116
claims = ClaimsIdentity(decoded_payload, True)
89117

90118
return claims

0 commit comments

Comments
 (0)