From 62dce8a0943d6af6a83d3d17ccfccca33b016787 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 25 Nov 2019 16:55:35 -0800 Subject: [PATCH 1/3] added claims validator --- .../botframework/connector/auth/__init__.py | 2 + .../auth/authentication_configuration.py | 11 ++- .../connector/auth/jwt_token_validation.py | 91 +++++++++++-------- .../botframework-connector/tests/test_auth.py | 24 +++++ 4 files changed, 90 insertions(+), 38 deletions(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/__init__.py b/libraries/botframework-connector/botframework/connector/auth/__init__.py index 3dd269e1b..45b23659a 100644 --- a/libraries/botframework-connector/botframework/connector/auth/__init__.py +++ b/libraries/botframework-connector/botframework/connector/auth/__init__.py @@ -11,6 +11,7 @@ # pylint: disable=missing-docstring from .microsoft_app_credentials import * +from .claims_identity import * from .jwt_token_validation import * from .credential_provider import * from .channel_validation import * @@ -18,3 +19,4 @@ from .jwt_token_extractor import * from .government_constants import * from .authentication_constants import * +from .authentication_configuration import * diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py index f60cff190..6ba4ffd7a 100644 --- a/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py +++ b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py @@ -1,9 +1,16 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import List +from typing import Awaitable, Callable, List + +from .claims_identity import ClaimsIdentity class AuthenticationConfiguration: - def __init__(self, required_endorsements: List[str] = None): + def __init__( + self, + required_endorsements: List[str] = None, + claims_validator: Callable[[List[ClaimsIdentity]], Awaitable] = None, + ): self.required_endorsements = required_endorsements or [] + self.claims_validator = claims_validator diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py index 91035413c..02dd474a0 100644 --- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Dict +from typing import Dict, List from botbuilder.schema import Activity @@ -73,63 +73,82 @@ async def validate_auth_header( if not auth_header: raise ValueError("argument auth_header is null") - if SkillValidation.is_skill_token(auth_header): - return await SkillValidation.authenticate_channel_token( - auth_header, - credentials, - channel_service, - channel_id, - auth_configuration, - ) - - if EmulatorValidation.is_token_from_emulator(auth_header): - return await EmulatorValidation.authenticate_emulator_token( - auth_header, credentials, channel_service, channel_id - ) - - # If the channel is Public Azure - if not channel_service: - if service_url: - return await ChannelValidation.authenticate_channel_token_with_service_url( + async def get_claims(): + if SkillValidation.is_skill_token(auth_header): + return await SkillValidation.authenticate_channel_token( auth_header, credentials, - service_url, + channel_service, channel_id, auth_configuration, ) - return await ChannelValidation.authenticate_channel_token( - auth_header, credentials, channel_id, auth_configuration - ) + if EmulatorValidation.is_token_from_emulator(auth_header): + return await EmulatorValidation.authenticate_emulator_token( + auth_header, credentials, channel_service, channel_id + ) + + # If the channel is Public Azure + if not channel_service: + if service_url: + return await ChannelValidation.authenticate_channel_token_with_service_url( + auth_header, + credentials, + service_url, + channel_id, + auth_configuration, + ) + + return await ChannelValidation.authenticate_channel_token( + auth_header, credentials, channel_id, auth_configuration + ) - if JwtTokenValidation.is_government(channel_service): + if JwtTokenValidation.is_government(channel_service): + if service_url: + return await GovernmentChannelValidation.authenticate_channel_token_with_service_url( + auth_header, + credentials, + service_url, + channel_id, + auth_configuration, + ) + + return await GovernmentChannelValidation.authenticate_channel_token( + auth_header, credentials, channel_id, auth_configuration + ) + + # Otherwise use Enterprise Channel Validation if service_url: - return await GovernmentChannelValidation.authenticate_channel_token_with_service_url( + return await EnterpriseChannelValidation.authenticate_channel_token_with_service_url( auth_header, credentials, service_url, channel_id, + channel_service, auth_configuration, ) - return await GovernmentChannelValidation.authenticate_channel_token( - auth_header, credentials, channel_id, auth_configuration - ) - - # Otherwise use Enterprise Channel Validation - if service_url: - return await EnterpriseChannelValidation.authenticate_channel_token_with_service_url( + return await EnterpriseChannelValidation.authenticate_channel_token( auth_header, credentials, - service_url, channel_id, channel_service, auth_configuration, ) - return await EnterpriseChannelValidation.authenticate_channel_token( - auth_header, credentials, channel_id, channel_service, auth_configuration - ) + claims = await get_claims() + + if claims: + await JwtTokenValidation.validate_claims(auth_configuration, claims) + + return claims + + @staticmethod + async def validate_claims( + auth_config: AuthenticationConfiguration, claims: List[ClaimsIdentity] + ): + if auth_config and auth_config.claims_validator: + await auth_config.claims_validator(claims) @staticmethod def is_government(channel_service: str) -> bool: diff --git a/libraries/botframework-connector/tests/test_auth.py b/libraries/botframework-connector/tests/test_auth.py index 635f00c50..a73b8a439 100644 --- a/libraries/botframework-connector/tests/test_auth.py +++ b/libraries/botframework-connector/tests/test_auth.py @@ -1,10 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import uuid +from typing import List +from unittest.mock import Mock + import pytest from botbuilder.schema import Activity from botframework.connector.auth import ( + AuthenticationConfiguration, AuthenticationConstants, JwtTokenValidation, SimpleCredentialProvider, @@ -40,6 +44,26 @@ class TestAuth: True ) + @pytest.mark.asyncio + async def test_claims_validation(self): + claims: List[ClaimsIdentity] = [] + default_auth_config = AuthenticationConfiguration() + + # No validator should pass. + await JwtTokenValidation.validate_claims(default_auth_config, claims) + + mock_validator = Mock() + auth_with_validator = AuthenticationConfiguration( + claims_validator=mock_validator + ) + + # ClaimsValidator configured but no exception should pass. + mock_validator.side_effect = PermissionError("Invalid claims.") + with pytest.raises(PermissionError) as excinfo: + await JwtTokenValidation.validate_claims(auth_with_validator, claims) + + assert "Invalid claims." in str(excinfo.value) + @pytest.mark.asyncio async def test_connector_auth_header_correct_app_id_and_service_url_should_validate( self, From 6392a4885c73246afdaa78f6b2d716682d8ccb19 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 25 Nov 2019 17:09:26 -0800 Subject: [PATCH 2/3] Solved PR comments --- .../connector/auth/authentication_configuration.py | 4 ++-- .../botframework/connector/auth/jwt_token_validation.py | 8 ++++---- libraries/botframework-connector/tests/test_auth.py | 6 ++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py index 6ba4ffd7a..192ff5c4e 100644 --- a/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py +++ b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py @@ -3,14 +3,14 @@ from typing import Awaitable, Callable, List -from .claims_identity import ClaimsIdentity +from .claims_identity import Claim class AuthenticationConfiguration: def __init__( self, required_endorsements: List[str] = None, - claims_validator: Callable[[List[ClaimsIdentity]], Awaitable] = None, + claims_validator: Callable[[List[Claim]], Awaitable] = None, ): self.required_endorsements = required_endorsements or [] self.claims_validator = claims_validator diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py index 02dd474a0..ef53d9f29 100644 --- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py @@ -11,7 +11,7 @@ from .channel_validation import ChannelValidation from .microsoft_app_credentials import MicrosoftAppCredentials from .credential_provider import CredentialProvider -from .claims_identity import ClaimsIdentity +from .claims_identity import Claim, ClaimsIdentity from .government_constants import GovernmentConstants from .government_channel_validation import GovernmentChannelValidation from .skill_validation import SkillValidation @@ -73,7 +73,7 @@ async def validate_auth_header( if not auth_header: raise ValueError("argument auth_header is null") - async def get_claims(): + async def get_claims() -> ClaimsIdentity: if SkillValidation.is_skill_token(auth_header): return await SkillValidation.authenticate_channel_token( auth_header, @@ -139,13 +139,13 @@ async def get_claims(): claims = await get_claims() if claims: - await JwtTokenValidation.validate_claims(auth_configuration, claims) + await JwtTokenValidation.validate_claims(auth_configuration, claims.claims) return claims @staticmethod async def validate_claims( - auth_config: AuthenticationConfiguration, claims: List[ClaimsIdentity] + auth_config: AuthenticationConfiguration, claims: List[Claim] ): if auth_config and auth_config.claims_validator: await auth_config.claims_validator(claims) diff --git a/libraries/botframework-connector/tests/test_auth.py b/libraries/botframework-connector/tests/test_auth.py index a73b8a439..17f14fd81 100644 --- a/libraries/botframework-connector/tests/test_auth.py +++ b/libraries/botframework-connector/tests/test_auth.py @@ -16,6 +16,7 @@ EnterpriseChannelValidation, ChannelValidation, ClaimsIdentity, + Claim, MicrosoftAppCredentials, GovernmentConstants, GovernmentChannelValidation, @@ -46,18 +47,19 @@ class TestAuth: @pytest.mark.asyncio async def test_claims_validation(self): - claims: List[ClaimsIdentity] = [] + claims: List[Claim] = [] default_auth_config = AuthenticationConfiguration() # No validator should pass. await JwtTokenValidation.validate_claims(default_auth_config, claims) + # ClaimsValidator configured but no exception should pass. mock_validator = Mock() auth_with_validator = AuthenticationConfiguration( claims_validator=mock_validator ) - # ClaimsValidator configured but no exception should pass. + # Configure IClaimsValidator to fail mock_validator.side_effect = PermissionError("Invalid claims.") with pytest.raises(PermissionError) as excinfo: await JwtTokenValidation.validate_claims(auth_with_validator, claims) From 4e51ea61314e26f6acc962721c7c12b1cf1b9e91 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 25 Nov 2019 17:15:47 -0800 Subject: [PATCH 3/3] claims are dict not Claim --- .../connector/auth/authentication_configuration.py | 6 ++---- .../botframework/connector/auth/jwt_token_validation.py | 4 ++-- libraries/botframework-connector/tests/test_auth.py | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py index 192ff5c4e..59642d9ff 100644 --- a/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py +++ b/libraries/botframework-connector/botframework/connector/auth/authentication_configuration.py @@ -1,16 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. -from typing import Awaitable, Callable, List - -from .claims_identity import Claim +from typing import Awaitable, Callable, Dict, List class AuthenticationConfiguration: def __init__( self, required_endorsements: List[str] = None, - claims_validator: Callable[[List[Claim]], Awaitable] = None, + claims_validator: Callable[[List[Dict]], Awaitable] = None, ): self.required_endorsements = required_endorsements or [] self.claims_validator = claims_validator diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py index ef53d9f29..d3b1c86c3 100644 --- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py @@ -11,7 +11,7 @@ from .channel_validation import ChannelValidation from .microsoft_app_credentials import MicrosoftAppCredentials from .credential_provider import CredentialProvider -from .claims_identity import Claim, ClaimsIdentity +from .claims_identity import ClaimsIdentity from .government_constants import GovernmentConstants from .government_channel_validation import GovernmentChannelValidation from .skill_validation import SkillValidation @@ -145,7 +145,7 @@ async def get_claims() -> ClaimsIdentity: @staticmethod async def validate_claims( - auth_config: AuthenticationConfiguration, claims: List[Claim] + auth_config: AuthenticationConfiguration, claims: List[Dict] ): if auth_config and auth_config.claims_validator: await auth_config.claims_validator(claims) diff --git a/libraries/botframework-connector/tests/test_auth.py b/libraries/botframework-connector/tests/test_auth.py index 17f14fd81..83e88d985 100644 --- a/libraries/botframework-connector/tests/test_auth.py +++ b/libraries/botframework-connector/tests/test_auth.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import uuid -from typing import List +from typing import Dict, List from unittest.mock import Mock import pytest @@ -16,7 +16,6 @@ EnterpriseChannelValidation, ChannelValidation, ClaimsIdentity, - Claim, MicrosoftAppCredentials, GovernmentConstants, GovernmentChannelValidation, @@ -47,7 +46,7 @@ class TestAuth: @pytest.mark.asyncio async def test_claims_validation(self): - claims: List[Claim] = [] + claims: List[Dict] = [] default_auth_config = AuthenticationConfiguration() # No validator should pass.