From aa131038e81f529bfa2535735c62d57a2e618f2b Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Wed, 31 Jul 2019 10:44:15 -0700 Subject: [PATCH 1/5] Gov support with unit tests. Coveralls badge fixed to point correct branch. --- README.md | 2 +- .../botbuilder/core/bot_framework_adapter.py | 2 +- .../botframework/connector/auth/__init__.py | 1 + .../auth/authentication_constants.py | 110 +++++++++++ .../auth/enterprise_channel_validation.py | 90 +++++++++ .../auth/government_channel_validation.py | 88 +++++++++ .../connector/auth/jwt_token_extractor.py | 8 +- .../connector/auth/jwt_token_validation.py | 33 +++- .../auth/microsoft_app_credentials.py | 17 +- .../botframework-connector/tests/test_auth.py | 187 ++++++++++++++++-- 10 files changed, 503 insertions(+), 35 deletions(-) create mode 100644 libraries/botframework-connector/botframework/connector/auth/authentication_constants.py create mode 100644 libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py create mode 100644 libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py diff --git a/README.md b/README.md index 3ce5d93d1..5981ef94f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # Bot Framework SDK v4 for Python (Preview) [![Build status](https://fuselabs.visualstudio.com/SDK_v4/_apis/build/status/Python/SDK_v4-Python-CI)](https://fuselabs.visualstudio.com/SDK_v4/_build/latest?definitionId=431) [![roadmap badge](https://img.shields.io/badge/visit%20the-roadmap-blue.svg)](https://github.com/Microsoft/botbuilder-python/wiki/Roadmap) -[![Coverage Status](https://coveralls.io/repos/github/microsoft/botbuilder-python/badge.svg?branch=axsuarez/formatting-and-style)](https://coveralls.io/github/microsoft/botbuilder-python?branch=axsuarez/formatting-and-style) +[![Coverage Status](https://coveralls.io/repos/github/microsoft/botbuilder-python/badge.svg?branch=HEAD)](https://coveralls.io/github/microsoft/botbuilder-python?branch=HEAD) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) This repository contains code for the Python version of the [Microsoft Bot Framework SDK](https://github.com/Microsoft/botbuilder). The Bot Framework SDK v4 enables developers to model conversation and build sophisticated bot applications using Python. This Python version is in **Preview** state and is being actively developed. diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py index 9e5d8f8e6..5704e2c84 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py @@ -206,7 +206,7 @@ async def authenticate_request(self, request: Activity, auth_header: str): :return: """ claims = await JwtTokenValidation.authenticate_request( - request, auth_header, self._credential_provider + request, auth_header, self._credential_provider, self.settings.channel_service ) if not claims.is_authenticated: diff --git a/libraries/botframework-connector/botframework/connector/auth/__init__.py b/libraries/botframework-connector/botframework/connector/auth/__init__.py index fa90f7443..3dd269e1b 100644 --- a/libraries/botframework-connector/botframework/connector/auth/__init__.py +++ b/libraries/botframework-connector/botframework/connector/auth/__init__.py @@ -17,3 +17,4 @@ from .emulator_validation import * from .jwt_token_extractor import * from .government_constants import * +from .authentication_constants import * diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py new file mode 100644 index 000000000..ac2087612 --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py @@ -0,0 +1,110 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from abc import ABC + + +class AuthenticationConstants(ABC): + + # TO CHANNEL FROM BOT: Login URL + # + # DEPRECATED: DO NOT USE + TO_CHANNEL_FROM_BOT_LOGIN_URL = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token" + + # TO CHANNEL FROM BOT: Login URL prefix + TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX = "https://login.microsoftonline.com/" + + # TO CHANNEL FROM BOT: Login URL token endpoint path + TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH = "/oauth2/v2.0/token" + + # TO CHANNEL FROM BOT: Default tenant from which to obtain a token for bot to channel communication + DEFAULT_CHANNEL_AUTH_TENANT = "botframework.com" + + # TO CHANNEL FROM BOT: OAuth scope to request + TO_CHANNEL_FROM_BOT_OAUTH_SCOPE = "https://api.botframework.com/.default" + + # TO BOT FROM CHANNEL: Token issuer + TO_BOT_FROM_CHANNEL_TOKEN_ISSUER = "https://api.botframework.com" + + # Application Setting Key for the OpenIdMetadataUrl value. + BOT_OPEN_ID_METADATA_KEY = "BotOpenIdMetadata" + + # Application Setting Key for the ChannelService value. + CHANNEL_SERVICE = "ChannelService" + + # Application Setting Key for the OAuthUrl value. + OAUTH_URL_KEY = "OAuthApiEndpoint" + + # Application Settings Key for whether to emulate OAuthCards when using the emulator. + EMULATE_OAUTH_CARDS_KEY = "EmulateOAuthCards" + + # TO BOT FROM CHANNEL: OpenID metadata document for tokens coming from MSA + TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL = "https://login.botframework.com/v1/.well-known/openidconfiguration" + + # TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming from MSA + TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT = ( + "https://{channelService}.enterprisechannel.botframework.com/v1/.well-known/openidconfiguration" + ) + + # TO BOT FROM EMULATOR: OpenID metadata document for tokens coming from MSA + TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = ( + "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration" + ) + + # Allowed token signing algorithms. Tokens come from channels to the bot. The code + # that uses this also supports tokens coming from the emulator. + ALLOWED_SIGNING_ALGORITHMS = ["RS256", "RS384", "RS512"] + + # "azp" Claim. + # Authorized party - the party to which the ID Token was issued. + # This claim follows the general format set forth in the OpenID Spec. + # http://openid.net/specs/openid-connect-core-1_0.html#IDToken + AUTHORIZED_PARTY = "azp" + + """ + Audience Claim. From RFC 7519. + https://tools.ietf.org/html/rfc7519#section-4.1.3 + The "aud" (audience) claim identifies the recipients that the JWT is + intended for. Each principal intended to process the JWT MUST + identify itself with a value in the audience claim.If the principal + processing the claim does not identify itself with a value in the + "aud" claim when this claim is present, then the JWT MUST be + rejected.In the general case, the "aud" value is an array of case- + sensitive strings, each containing a StringOrURI value.In the + special case when the JWT has one audience, the "aud" value MAY be a + single case-sensitive string containing a StringOrURI value.The + interpretation of audience values is generally application specific. + Use of this claim is OPTIONAL. + """ + AUDIENCE_CLAIM = "aud" + + """ + Issuer Claim. From RFC 7519. + https://tools.ietf.org/html/rfc7519#section-4.1.1 + The "iss" (issuer) claim identifies the principal that issued the + JWT. The processing of this claim is generally application specific. + The "iss" value is a case-sensitive string containing a StringOrURI + value. Use of this claim is OPTIONAL. + """ + ISSUER_CLAIM = "iss" + + """ + From RFC 7515 + https://tools.ietf.org/html/rfc7515#section-4.1.4 + The "kid" (key ID) Header Parameter is a hint indicating which key + was used to secure the JWS. This parameter allows originators to + explicitly signal a change of key to recipients. The structure of + the "kid" value is unspecified. Its value MUST be a case-sensitive + string. Use of this Header Parameter is OPTIONAL. + When used with a JWK, the "kid" value is used to match a JWK "kid" + parameter value. + """ + KEY_ID_HEADER = "kid" + + # Token version claim name. As used in Microsoft AAD tokens. + VERSION_CLAIM = "ver" + + # App ID claim name. As used in Microsoft AAD 1.0 tokens. + APP_ID_CLAIM = "appid" + + # Service URL claim name. As used in Microsoft Bot Framework v3.1 auth. + SERVICE_URL_CLAIM = "serviceurl" diff --git a/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py new file mode 100644 index 000000000..83698ee4f --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py @@ -0,0 +1,90 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from abc import ABC + +from .authentication_constants import AuthenticationConstants +from .channel_validation import ChannelValidation +from .claims_identity import ClaimsIdentity +from .credential_provider import CredentialProvider +from .jwt_token_extractor import JwtTokenExtractor +from .verify_options import VerifyOptions + + +class EnterpriseChannelValidation(ABC): + + TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions( + issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER], + audience=None, + clock_tolerance=5 * 60, + ignore_expiration=False + ) + + @staticmethod + async def authenticate_channel_token( + auth_header: str, + credentials: CredentialProvider, + channel_id: str, + channel_service: str + ) -> ClaimsIdentity: + endpoint = (ChannelValidation.open_id_metadata_endpoint + if ChannelValidation.open_id_metadata_endpoint + else AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT.replace( + "{channelService}", channel_service + )) + token_extractor = JwtTokenExtractor( + EnterpriseChannelValidation.TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS, + endpoint, + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + ) + + identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(auth_header, channel_id) + return await EnterpriseChannelValidation.validate_identity(identity, credentials) + + @staticmethod + async def authenticate_channel_token_with_service_url( + auth_header: str, + credentials: CredentialProvider, + service_url: str, + channel_id: str, + channel_service: str + ) -> ClaimsIdentity: + identity: ClaimsIdentity = await EnterpriseChannelValidation.authenticate_channel_token( + auth_header, credentials, channel_id, channel_service + ) + + service_url_claim: str = identity.get_claim_value(AuthenticationConstants.SERVICE_URL_CLAIM) + if service_url_claim != service_url: + raise Exception("Unauthorized. service_url claim do not match.") + + return identity + + @staticmethod + async def validate_identity(identity: ClaimsIdentity, credentials: CredentialProvider) -> ClaimsIdentity: + if identity is None: + # No valid identity. Not Authorized. + raise Exception("Unauthorized. No valid identity.") + + if not identity.is_authenticated: + # The token is in some way invalid. Not Authorized. + raise Exception("Unauthorized. Is not authenticated.") + + # Now check that the AppID in the claim set matches + # what we're looking for. Note that in a multi-tenant bot, this value + # comes from developer code that may be reaching out to a service, hence the + # Async validation. + + # Look for the "aud" claim, but only if issued from the Bot Framework + if (identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM) != + AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER): + # The relevant Audience Claim MUST be present. Not Authorized. + raise Exception("Unauthorized. Issuer claim MUST be present.") + + # The AppId from the claim in the token must match the AppId specified by the developer. + # In this case, the token is destined for the app, so we find the app ID in the audience claim. + aud_claim: str = identity.get_claim_value(AuthenticationConstants.AUDIENCE_CLAIM) + if not await credentials.is_valid_appid(aud_claim or ""): + # The AppId is not valid or not present. Not Authorized. + raise Exception(f"Unauthorized. Invalid AppId passed on token: { aud_claim }") + + return identity diff --git a/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py new file mode 100644 index 000000000..f1c827241 --- /dev/null +++ b/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py @@ -0,0 +1,88 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from abc import ABC + +from .authentication_constants import AuthenticationConstants +from .claims_identity import ClaimsIdentity +from .credential_provider import CredentialProvider +from .government_constants import GovernmentConstants +from .jwt_token_extractor import JwtTokenExtractor +from .verify_options import VerifyOptions + + +class GovernmentChannelValidation(ABC): + + OPEN_ID_METADATA_ENDPOINT = "" + + TO_BOT_FROM_GOVERNMENT_CHANNEL_TOKEN_VALIDATION_PARAMETERS = VerifyOptions( + issuer=[GovernmentConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER], + audience=None, + clock_tolerance=5 * 60, + ignore_expiration=False + ) + + @staticmethod + async def authenticate_channel_token( + auth_header: str, + credentials: CredentialProvider, + channel_id: str + ) -> ClaimsIdentity: + endpoint = (GovernmentChannelValidation.open_id_metadata_endpoint + if GovernmentChannelValidation.open_id_metadata_endpoint + else GovernmentConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL) + token_extractor = JwtTokenExtractor( + GovernmentChannelValidation.TO_BOT_FROM_GOVERNMENT_CHANNEL_TOKEN_VALIDATION_PARAMETERS, + endpoint, + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + ) + + identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(auth_header, channel_id) + return await GovernmentChannelValidation.validate_identity(identity, credentials) + + @staticmethod + async def authenticate_channel_token_with_service_url( + auth_header: str, + credentials: CredentialProvider, + service_url: str, + channel_id: str + ) -> ClaimsIdentity: + identity: ClaimsIdentity = await GovernmentChannelValidation.authenticate_channel_token( + auth_header, credentials, channel_id + ) + + service_url_claim: str = identity.get_claim_value(AuthenticationConstants.SERVICE_URL_CLAIM) + if service_url_claim != service_url: + raise Exception("Unauthorized. service_url claim do not match.") + + return identity + + @staticmethod + async def validate_identity(identity: ClaimsIdentity, credentials: CredentialProvider) -> ClaimsIdentity: + if identity is None: + # No valid identity. Not Authorized. + raise Exception("Unauthorized. No valid identity.") + + if not identity.is_authenticated: + # The token is in some way invalid. Not Authorized. + raise Exception("Unauthorized. Is not authenticated.") + + # Now check that the AppID in the claim set matches + # what we're looking for. Note that in a multi-tenant bot, this value + # comes from developer code that may be reaching out to a service, hence the + # Async validation. + + # Look for the "aud" claim, but only if issued from the Bot Framework + if (identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM) != + GovernmentConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER): + # The relevant Audience Claim MUST be present. Not Authorized. + raise Exception("Unauthorized. Issuer claim MUST be present.") + + # The AppId from the claim in the token must match the AppId specified by the developer. + # In this case, the token is destined for the app, so we find the app ID in the audience claim. + aud_claim: str = identity.get_claim_value(AuthenticationConstants.AUDIENCE_CLAIM) + if not (await credentials.is_valid_appid(aud_claim or "")): + # The AppId is not valid or not present. Not Authorized. + raise Exception(f"Unauthorized. Invalid AppId passed on token: { aud_claim }") + + return identity diff --git a/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py b/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py index 4c58509a1..043c0eccb 100644 --- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py +++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_extractor.py @@ -16,12 +16,12 @@ class JwtTokenExtractor: def __init__( self, - validationParams: VerifyOptions, + validation_params: VerifyOptions, metadata_url: str, - allowedAlgorithms: list, + allowed_algorithms: list, ): - self.validation_parameters = validationParams - self.validation_parameters.algorithms = allowedAlgorithms + self.validation_parameters = validation_params + self.validation_parameters.algorithms = allowed_algorithms self.open_id_metadata = JwtTokenExtractor.get_open_id_metadata(metadata_url) @staticmethod 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 1a6813494..b67789a36 100644 --- a/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/jwt_token_validation.py @@ -1,11 +1,13 @@ from botbuilder.schema import Activity from .emulator_validation import EmulatorValidation +from .enterprise_channel_validation import EnterpriseChannelValidation from .channel_validation import ChannelValidation from .microsoft_app_credentials import MicrosoftAppCredentials from .credential_provider import CredentialProvider from .claims_identity import ClaimsIdentity from .government_constants import GovernmentConstants +from .government_channel_validation import GovernmentChannelValidation class JwtTokenValidation: @@ -70,14 +72,35 @@ async def validate_auth_header( auth_header, credentials, channel_service, channel_id ) - # Right now public Azure is the only supported scenario (Gov and Enterprise pending) + # 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 + ) + + return await ChannelValidation.authenticate_channel_token( + auth_header, credentials, channel_id + ) + + 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 + ) + + return await GovernmentChannelValidation.authenticate_channel_token( + auth_header, credentials, channel_id + ) + + # Otherwise use Enterprise Channel Validation if service_url: - return await ChannelValidation.authenticate_channel_token_with_service_url( - auth_header, credentials, service_url, channel_id + return await EnterpriseChannelValidation.authenticate_channel_token_with_service_url( + auth_header, credentials, service_url, channel_id, channel_service ) - return await ChannelValidation.authenticate_channel_token( - auth_header, credentials, channel_id + return await EnterpriseChannelValidation.authenticate_channel_token( + auth_header, credentials, channel_id, channel_service ) @staticmethod diff --git a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py index 66d17633f..05a4f1cd4 100644 --- a/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/microsoft_app_credentials.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + from datetime import datetime, timedelta from urllib.parse import urlparse from msrest.authentication import BasicTokenAuthentication, Authentication @@ -43,11 +46,16 @@ class MicrosoftAppCredentials(Authentication): MicrosoftAppCredentials auth implementation and cache. """ - refreshEndpoint = AUTH_SETTINGS["refreshEndpoint"] - refreshScope = AUTH_SETTINGS["refreshScope"] schema = "Bearer" - trustedHostNames = {} + trustedHostNames = { + "state.botframework.com": datetime.max, + "api.botframework.com": datetime.max, + "token.botframework.com": datetime.max, + "state.botframework.azure.us": datetime.max, + "api.botframework.azure.us": datetime.max, + "token.botframework.azure.us": datetime.max, + } cache = {} def __init__(self, app_id: str, password: str, channel_auth_tenant: str = None): @@ -71,6 +79,7 @@ def __init__(self, app_id: str, password: str, channel_auth_tenant: str = None): + tenant + Constants.TO_CHANNEL_FROM_BOT_TOKEN_ENDPOINT_PATH ) + self.oauth_scope = AUTH_SETTINGS["refreshScope"] self.token_cache_key = app_id + "-cache" def signed_session(self) -> requests.Session: # pylint: disable=arguments-differ @@ -123,7 +132,7 @@ def refresh_token(self) -> _OAuthResponse: "grant_type": "client_credentials", "client_id": self.microsoft_app_id, "client_secret": self.microsoft_app_password, - "scope": MicrosoftAppCredentials.refreshScope, + "scope": self.oauth_scope, } response = requests.post(self.oauth_endpoint, data=options) diff --git a/libraries/botframework-connector/tests/test_auth.py b/libraries/botframework-connector/tests/test_auth.py index 02016715f..a3a4606ba 100644 --- a/libraries/botframework-connector/tests/test_auth.py +++ b/libraries/botframework-connector/tests/test_auth.py @@ -7,9 +7,28 @@ from botframework.connector.auth import JwtTokenValidation from botframework.connector.auth import SimpleCredentialProvider from botframework.connector.auth import EmulatorValidation +from botframework.connector.auth import EnterpriseChannelValidation from botframework.connector.auth import ChannelValidation +from botframework.connector.auth import ClaimsIdentity from botframework.connector.auth import MicrosoftAppCredentials +from botframework.connector.auth import GovernmentConstants +from botframework.connector.auth import GovernmentChannelValidation +async def jwt_token_validation_validate_auth_header_with_channel_service_succeeds( + app_id: str, + pwd: str, + channel_service: str, + header: str = None +): + if header is None: + header = f"Bearer {MicrosoftAppCredentials(app_id, pwd).get_access_token()}" + + credentials = SimpleCredentialProvider(app_id, pwd) + result = await JwtTokenValidation.validate_auth_header( + header, credentials, channel_service, "", "https://webchat.botframework.com/" + ) + + assert result.is_authenticated class TestAuth: EmulatorValidation.TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS.ignore_expiration = ( @@ -73,7 +92,7 @@ async def test_connector_auth_header_and_no_credential_should_not_validate(self) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio - async def test_empty_header_and_no_credential_should_validate(self): + async def test_empty_header_and_no_credential_should_throw(self): header = "" credentials = SimpleCredentialProvider("", "") with pytest.raises(Exception) as excinfo: @@ -114,8 +133,8 @@ async def test_emulator_msa_header_and_no_credential_should_not_validate(self): await JwtTokenValidation.validate_auth_header(header, credentials, "", None) assert "Unauthorized" in excinfo - @pytest.mark.asyncio # Tests with a valid Token and service url; and ensures that Service url is added to Trusted service url list. + @pytest.mark.asyncio async def test_channel_msa_header_valid_service_url_should_be_trusted(self): activity = Activity( service_url="https://smba.trafficmanager.net/amer-client-ss.msg/" @@ -136,24 +155,6 @@ async def test_channel_msa_header_valid_service_url_should_be_trusted(self): "https://smba.trafficmanager.net/amer-client-ss.msg/" ) - @pytest.mark.asyncio - async def test_channel_msa_header_from_user_specified_tenant(self): - activity = Activity( - service_url="https://smba.trafficmanager.net/amer-client-ss.msg/" - ) - header = "Bearer " + MicrosoftAppCredentials( - "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F", "microsoft.com" - ).get_access_token(True) - credentials = SimpleCredentialProvider( - "2cd87869-38a0-4182-9251-d056e8f0ac24", "" - ) - - claims = await JwtTokenValidation.authenticate_request( - activity, header, credentials - ) - - assert claims.get_claim_value("tid") == "72f988bf-86f1-41af-91ab-2d7cd011db47" - @pytest.mark.asyncio # Tests with a valid Token and invalid service url and ensures that Service url is NOT added to # Trusted service url list. @@ -177,6 +178,24 @@ async def test_channel_msa_header_invalid_service_url_should_not_be_trusted(self "https://webchat.botframework.com/" ) + @pytest.mark.asyncio + async def test_channel_msa_header_from_user_specified_tenant(self): + activity = Activity( + service_url="https://smba.trafficmanager.net/amer-client-ss.msg/" + ) + header = "Bearer " + MicrosoftAppCredentials( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F", "microsoft.com" + ).get_access_token(True) + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "" + ) + + claims = await JwtTokenValidation.authenticate_request( + activity, header, credentials + ) + + assert claims.get_claim_value("tid") == "72f988bf-86f1-41af-91ab-2d7cd011db47" + @pytest.mark.asyncio # Tests with no authentication header and makes sure the service URL is not added to the trusted list. async def test_channel_authentication_disabled_should_be_anonymous(self): @@ -205,3 +224,131 @@ async def test_channel_authentication_disabled_service_url_should_not_be_trusted assert not MicrosoftAppCredentials.is_trusted_service( "https://webchat.botframework.com/" ) + + @pytest.mark.asyncio + async def test_emulator_auth_header_correct_app_id_and_service_url_with_gov_channel_service_should_validate(self): + await jwt_token_validation_validate_auth_header_with_channel_service_succeeds( + "2cd87869-38a0-4182-9251-d056e8f0ac24", # emulator creds + "2.30Vs3VQLKt974F", + GovernmentConstants.CHANNEL_SERVICE + ) + + @pytest.mark.asyncio + async def test_emulator_auth_header_correct_app_id_and_service_url_with_private_channel_service_should_validate( + self + ): + await jwt_token_validation_validate_auth_header_with_channel_service_succeeds( + "2cd87869-38a0-4182-9251-d056e8f0ac24", # emulator creds + "2.30Vs3VQLKt974F", + "TheChannel" + ) + + @pytest.mark.asyncio + async def test_government_channel_validation_succeeds(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + + await GovernmentChannelValidation.validate_identity( + ClaimsIdentity( + {"iss": "https://api.botframework.us", "aud": credentials.app_id}, + True + ), + credentials + ) + + @pytest.mark.asyncio + async def test_government_channel_validation_no_authentication_fails(self): + with pytest.raises(Exception) as excinfo: + await GovernmentChannelValidation.validate_identity(ClaimsIdentity({}, False), None) + assert "Unauthorized" in str(excinfo.value) + + @pytest.mark.asyncio + async def test_government_channel_validation_no_issuer_fails(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + with pytest.raises(Exception) as excinfo: + await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ + "peanut": "peanut" + }, True), credentials) + assert "Unauthorized" in str(excinfo.value) + + @pytest.mark.asyncio + async def test_government_channel_validation_wrong_issuer_fails(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + with pytest.raises(Exception) as excinfo: + await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ + "iss": "peanut" + }, True), credentials) + assert "Unauthorized" in str(excinfo.value) + + @pytest.mark.asyncio + async def test_government_channel_validation_no_audience_fails(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + with pytest.raises(Exception) as excinfo: + await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ + "iss": "https://api.botframework.us" + }, True), credentials) + assert "Unauthorized" in str(excinfo.value) + + @pytest.mark.asyncio + async def test_government_channel_validation_wrong_audience_fails(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + with pytest.raises(Exception) as excinfo: + await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ + "iss": "https://api.botframework.us", + "aud": "peanut" + }, True), credentials) + assert "Unauthorized" in str(excinfo.value) + + @pytest.mark.asyncio + async def test_enterprise_channel_validation_succeeds(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + + await EnterpriseChannelValidation.validate_identity( + ClaimsIdentity( + {"iss": "https://api.botframework.com", "aud": credentials.app_id}, + True + ), + credentials + ) + + @pytest.mark.asyncio + async def test_enterprise_channel_validation_no_authentication_fails(self): + with pytest.raises(Exception) as excinfo: + await EnterpriseChannelValidation.validate_identity(ClaimsIdentity({}, False), None) + assert "Unauthorized" in str(excinfo.value) + + @pytest.mark.asyncio + async def test_enterprise_channel_validation_no_issuer_fails(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + with pytest.raises(Exception) as excinfo: + await EnterpriseChannelValidation.validate_identity(ClaimsIdentity({ + "peanut": "peanut" + }, True), credentials) + assert "Unauthorized" in str(excinfo.value) + + @pytest.mark.asyncio + async def test_enterprise_channel_validation_wrong_issuer_fails(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + with pytest.raises(Exception) as excinfo: + await EnterpriseChannelValidation.validate_identity(ClaimsIdentity({ + "iss": "peanut" + }, True), credentials) + assert "Unauthorized" in str(excinfo.value) + + @pytest.mark.asyncio + async def test_enterprise_channel_validation_no_audience_fails(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + with pytest.raises(Exception) as excinfo: + await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ + "iss": "https://api.botframework.com" + }, True), credentials) + assert "Unauthorized" in str(excinfo.value) + + @pytest.mark.asyncio + async def test_enterprise_channel_validation_wrong_audience_fails(self): + credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + with pytest.raises(Exception) as excinfo: + await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ + "iss": "https://api.botframework.com", + "aud": "peanut" + }, True), credentials) + assert "Unauthorized" in str(excinfo.value) From 0adcb43fb51d20fcf551c601238753b7c8a83e54 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Wed, 31 Jul 2019 10:49:50 -0700 Subject: [PATCH 2/5] pylint and black compliance --- .../botbuilder/core/bot_framework_adapter.py | 5 +- .../auth/authentication_constants.py | 12 +- .../auth/enterprise_channel_validation.py | 50 ++++--- .../auth/government_channel_validation.py | 50 ++++--- .../botframework-connector/tests/test_auth.py | 131 +++++++++++------- 5 files changed, 156 insertions(+), 92 deletions(-) diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py index 5704e2c84..2557468d0 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py @@ -206,7 +206,10 @@ async def authenticate_request(self, request: Activity, auth_header: str): :return: """ claims = await JwtTokenValidation.authenticate_request( - request, auth_header, self._credential_provider, self.settings.channel_service + request, + auth_header, + self._credential_provider, + self.settings.channel_service, ) if not claims.is_authenticated: diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py index ac2087612..0024f5c8b 100644 --- a/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py +++ b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py @@ -8,7 +8,9 @@ class AuthenticationConstants(ABC): # TO CHANNEL FROM BOT: Login URL # # DEPRECATED: DO NOT USE - TO_CHANNEL_FROM_BOT_LOGIN_URL = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token" + TO_CHANNEL_FROM_BOT_LOGIN_URL = ( + "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token" + ) # TO CHANNEL FROM BOT: Login URL prefix TO_CHANNEL_FROM_BOT_LOGIN_URL_PREFIX = "https://login.microsoftonline.com/" @@ -38,12 +40,12 @@ class AuthenticationConstants(ABC): EMULATE_OAUTH_CARDS_KEY = "EmulateOAuthCards" # TO BOT FROM CHANNEL: OpenID metadata document for tokens coming from MSA - TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL = "https://login.botframework.com/v1/.well-known/openidconfiguration" + TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL = ( + "https://login.botframework.com/v1/.well-known/openidconfiguration" + ) # TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming from MSA - TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT = ( - "https://{channelService}.enterprisechannel.botframework.com/v1/.well-known/openidconfiguration" - ) + TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT = "https://{channelService}.enterprisechannel.botframework.com/v1/.well-known/openidconfiguration" # TO BOT FROM EMULATOR: OpenID metadata document for tokens coming from MSA TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = ( diff --git a/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py index 83698ee4f..5124b65ed 100644 --- a/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/enterprise_channel_validation.py @@ -17,7 +17,7 @@ class EnterpriseChannelValidation(ABC): issuer=[AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER], audience=None, clock_tolerance=5 * 60, - ignore_expiration=False + ignore_expiration=False, ) @staticmethod @@ -25,21 +25,27 @@ async def authenticate_channel_token( auth_header: str, credentials: CredentialProvider, channel_id: str, - channel_service: str + channel_service: str, ) -> ClaimsIdentity: - endpoint = (ChannelValidation.open_id_metadata_endpoint - if ChannelValidation.open_id_metadata_endpoint - else AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT.replace( - "{channelService}", channel_service - )) + endpoint = ( + ChannelValidation.open_id_metadata_endpoint + if ChannelValidation.open_id_metadata_endpoint + else AuthenticationConstants.TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT.replace( + "{channelService}", channel_service + ) + ) token_extractor = JwtTokenExtractor( EnterpriseChannelValidation.TO_BOT_FROM_ENTERPRISE_CHANNEL_TOKEN_VALIDATION_PARAMETERS, endpoint, - AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS, ) - identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(auth_header, channel_id) - return await EnterpriseChannelValidation.validate_identity(identity, credentials) + identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header( + auth_header, channel_id + ) + return await EnterpriseChannelValidation.validate_identity( + identity, credentials + ) @staticmethod async def authenticate_channel_token_with_service_url( @@ -47,20 +53,24 @@ async def authenticate_channel_token_with_service_url( credentials: CredentialProvider, service_url: str, channel_id: str, - channel_service: str + channel_service: str, ) -> ClaimsIdentity: identity: ClaimsIdentity = await EnterpriseChannelValidation.authenticate_channel_token( auth_header, credentials, channel_id, channel_service ) - service_url_claim: str = identity.get_claim_value(AuthenticationConstants.SERVICE_URL_CLAIM) + service_url_claim: str = identity.get_claim_value( + AuthenticationConstants.SERVICE_URL_CLAIM + ) if service_url_claim != service_url: raise Exception("Unauthorized. service_url claim do not match.") return identity @staticmethod - async def validate_identity(identity: ClaimsIdentity, credentials: CredentialProvider) -> ClaimsIdentity: + async def validate_identity( + identity: ClaimsIdentity, credentials: CredentialProvider + ) -> ClaimsIdentity: if identity is None: # No valid identity. Not Authorized. raise Exception("Unauthorized. No valid identity.") @@ -75,16 +85,22 @@ async def validate_identity(identity: ClaimsIdentity, credentials: CredentialPro # Async validation. # Look for the "aud" claim, but only if issued from the Bot Framework - if (identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM) != - AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER): + if ( + identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM) + != AuthenticationConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER + ): # The relevant Audience Claim MUST be present. Not Authorized. raise Exception("Unauthorized. Issuer claim MUST be present.") # The AppId from the claim in the token must match the AppId specified by the developer. # In this case, the token is destined for the app, so we find the app ID in the audience claim. - aud_claim: str = identity.get_claim_value(AuthenticationConstants.AUDIENCE_CLAIM) + aud_claim: str = identity.get_claim_value( + AuthenticationConstants.AUDIENCE_CLAIM + ) if not await credentials.is_valid_appid(aud_claim or ""): # The AppId is not valid or not present. Not Authorized. - raise Exception(f"Unauthorized. Invalid AppId passed on token: { aud_claim }") + raise Exception( + f"Unauthorized. Invalid AppId passed on token: { aud_claim }" + ) return identity diff --git a/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py b/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py index f1c827241..f4226be79 100644 --- a/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/government_channel_validation.py @@ -19,46 +19,54 @@ class GovernmentChannelValidation(ABC): issuer=[GovernmentConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER], audience=None, clock_tolerance=5 * 60, - ignore_expiration=False + ignore_expiration=False, ) @staticmethod async def authenticate_channel_token( - auth_header: str, - credentials: CredentialProvider, - channel_id: str + auth_header: str, credentials: CredentialProvider, channel_id: str ) -> ClaimsIdentity: - endpoint = (GovernmentChannelValidation.open_id_metadata_endpoint - if GovernmentChannelValidation.open_id_metadata_endpoint - else GovernmentConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL) + endpoint = ( + GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT + if GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT + else GovernmentConstants.TO_BOT_FROM_CHANNEL_OPEN_ID_METADATA_URL + ) token_extractor = JwtTokenExtractor( GovernmentChannelValidation.TO_BOT_FROM_GOVERNMENT_CHANNEL_TOKEN_VALIDATION_PARAMETERS, endpoint, - AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS + AuthenticationConstants.ALLOWED_SIGNING_ALGORITHMS, ) - identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header(auth_header, channel_id) - return await GovernmentChannelValidation.validate_identity(identity, credentials) + identity: ClaimsIdentity = await token_extractor.get_identity_from_auth_header( + auth_header, channel_id + ) + return await GovernmentChannelValidation.validate_identity( + identity, credentials + ) @staticmethod async def authenticate_channel_token_with_service_url( auth_header: str, credentials: CredentialProvider, service_url: str, - channel_id: str + channel_id: str, ) -> ClaimsIdentity: identity: ClaimsIdentity = await GovernmentChannelValidation.authenticate_channel_token( auth_header, credentials, channel_id ) - service_url_claim: str = identity.get_claim_value(AuthenticationConstants.SERVICE_URL_CLAIM) + service_url_claim: str = identity.get_claim_value( + AuthenticationConstants.SERVICE_URL_CLAIM + ) if service_url_claim != service_url: raise Exception("Unauthorized. service_url claim do not match.") return identity @staticmethod - async def validate_identity(identity: ClaimsIdentity, credentials: CredentialProvider) -> ClaimsIdentity: + async def validate_identity( + identity: ClaimsIdentity, credentials: CredentialProvider + ) -> ClaimsIdentity: if identity is None: # No valid identity. Not Authorized. raise Exception("Unauthorized. No valid identity.") @@ -73,16 +81,22 @@ async def validate_identity(identity: ClaimsIdentity, credentials: CredentialPro # Async validation. # Look for the "aud" claim, but only if issued from the Bot Framework - if (identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM) != - GovernmentConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER): + if ( + identity.get_claim_value(AuthenticationConstants.ISSUER_CLAIM) + != GovernmentConstants.TO_BOT_FROM_CHANNEL_TOKEN_ISSUER + ): # The relevant Audience Claim MUST be present. Not Authorized. raise Exception("Unauthorized. Issuer claim MUST be present.") # The AppId from the claim in the token must match the AppId specified by the developer. # In this case, the token is destined for the app, so we find the app ID in the audience claim. - aud_claim: str = identity.get_claim_value(AuthenticationConstants.AUDIENCE_CLAIM) - if not (await credentials.is_valid_appid(aud_claim or "")): + aud_claim: str = identity.get_claim_value( + AuthenticationConstants.AUDIENCE_CLAIM + ) + if not await credentials.is_valid_appid(aud_claim or ""): # The AppId is not valid or not present. Not Authorized. - raise Exception(f"Unauthorized. Invalid AppId passed on token: { aud_claim }") + raise Exception( + f"Unauthorized. Invalid AppId passed on token: { aud_claim }" + ) return identity diff --git a/libraries/botframework-connector/tests/test_auth.py b/libraries/botframework-connector/tests/test_auth.py index a3a4606ba..b3d904b4d 100644 --- a/libraries/botframework-connector/tests/test_auth.py +++ b/libraries/botframework-connector/tests/test_auth.py @@ -14,11 +14,9 @@ from botframework.connector.auth import GovernmentConstants from botframework.connector.auth import GovernmentChannelValidation + async def jwt_token_validation_validate_auth_header_with_channel_service_succeeds( - app_id: str, - pwd: str, - channel_service: str, - header: str = None + app_id: str, pwd: str, channel_service: str, header: str = None ): if header is None: header = f"Bearer {MicrosoftAppCredentials(app_id, pwd).get_access_token()}" @@ -30,6 +28,7 @@ async def jwt_token_validation_validate_auth_header_with_channel_service_succeed assert result.is_authenticated + class TestAuth: EmulatorValidation.TO_BOT_FROM_EMULATOR_TOKEN_VALIDATION_PARAMETERS.ignore_expiration = ( True @@ -226,11 +225,13 @@ async def test_channel_authentication_disabled_service_url_should_not_be_trusted ) @pytest.mark.asyncio - async def test_emulator_auth_header_correct_app_id_and_service_url_with_gov_channel_service_should_validate(self): + async def test_emulator_auth_header_correct_app_id_and_service_url_with_gov_channel_service_should_validate( + self + ): await jwt_token_validation_validate_auth_header_with_channel_service_succeeds( "2cd87869-38a0-4182-9251-d056e8f0ac24", # emulator creds "2.30Vs3VQLKt974F", - GovernmentConstants.CHANNEL_SERVICE + GovernmentConstants.CHANNEL_SERVICE, ) @pytest.mark.asyncio @@ -240,115 +241,143 @@ async def test_emulator_auth_header_correct_app_id_and_service_url_with_private_ await jwt_token_validation_validate_auth_header_with_channel_service_succeeds( "2cd87869-38a0-4182-9251-d056e8f0ac24", # emulator creds "2.30Vs3VQLKt974F", - "TheChannel" + "TheChannel", ) @pytest.mark.asyncio async def test_government_channel_validation_succeeds(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) await GovernmentChannelValidation.validate_identity( ClaimsIdentity( - {"iss": "https://api.botframework.us", "aud": credentials.app_id}, - True + {"iss": "https://api.botframework.us", "aud": credentials.app_id}, True ), - credentials + credentials, ) @pytest.mark.asyncio async def test_government_channel_validation_no_authentication_fails(self): with pytest.raises(Exception) as excinfo: - await GovernmentChannelValidation.validate_identity(ClaimsIdentity({}, False), None) + await GovernmentChannelValidation.validate_identity( + ClaimsIdentity({}, False), None + ) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio async def test_government_channel_validation_no_issuer_fails(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) with pytest.raises(Exception) as excinfo: - await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ - "peanut": "peanut" - }, True), credentials) + await GovernmentChannelValidation.validate_identity( + ClaimsIdentity({"peanut": "peanut"}, True), credentials + ) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio async def test_government_channel_validation_wrong_issuer_fails(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) with pytest.raises(Exception) as excinfo: - await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ - "iss": "peanut" - }, True), credentials) + await GovernmentChannelValidation.validate_identity( + ClaimsIdentity({"iss": "peanut"}, True), credentials + ) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio async def test_government_channel_validation_no_audience_fails(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) with pytest.raises(Exception) as excinfo: - await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ - "iss": "https://api.botframework.us" - }, True), credentials) + await GovernmentChannelValidation.validate_identity( + ClaimsIdentity({"iss": "https://api.botframework.us"}, True), + credentials, + ) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio async def test_government_channel_validation_wrong_audience_fails(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) with pytest.raises(Exception) as excinfo: - await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ - "iss": "https://api.botframework.us", - "aud": "peanut" - }, True), credentials) + await GovernmentChannelValidation.validate_identity( + ClaimsIdentity( + {"iss": "https://api.botframework.us", "aud": "peanut"}, True + ), + credentials, + ) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio async def test_enterprise_channel_validation_succeeds(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) await EnterpriseChannelValidation.validate_identity( ClaimsIdentity( - {"iss": "https://api.botframework.com", "aud": credentials.app_id}, - True + {"iss": "https://api.botframework.com", "aud": credentials.app_id}, True ), - credentials + credentials, ) @pytest.mark.asyncio async def test_enterprise_channel_validation_no_authentication_fails(self): with pytest.raises(Exception) as excinfo: - await EnterpriseChannelValidation.validate_identity(ClaimsIdentity({}, False), None) + await EnterpriseChannelValidation.validate_identity( + ClaimsIdentity({}, False), None + ) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio async def test_enterprise_channel_validation_no_issuer_fails(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) with pytest.raises(Exception) as excinfo: - await EnterpriseChannelValidation.validate_identity(ClaimsIdentity({ - "peanut": "peanut" - }, True), credentials) + await EnterpriseChannelValidation.validate_identity( + ClaimsIdentity({"peanut": "peanut"}, True), credentials + ) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio async def test_enterprise_channel_validation_wrong_issuer_fails(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) with pytest.raises(Exception) as excinfo: - await EnterpriseChannelValidation.validate_identity(ClaimsIdentity({ - "iss": "peanut" - }, True), credentials) + await EnterpriseChannelValidation.validate_identity( + ClaimsIdentity({"iss": "peanut"}, True), credentials + ) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio async def test_enterprise_channel_validation_no_audience_fails(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) with pytest.raises(Exception) as excinfo: - await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ - "iss": "https://api.botframework.com" - }, True), credentials) + await GovernmentChannelValidation.validate_identity( + ClaimsIdentity({"iss": "https://api.botframework.com"}, True), + credentials, + ) assert "Unauthorized" in str(excinfo.value) @pytest.mark.asyncio async def test_enterprise_channel_validation_wrong_audience_fails(self): - credentials = SimpleCredentialProvider("2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F") + credentials = SimpleCredentialProvider( + "2cd87869-38a0-4182-9251-d056e8f0ac24", "2.30Vs3VQLKt974F" + ) with pytest.raises(Exception) as excinfo: - await GovernmentChannelValidation.validate_identity(ClaimsIdentity({ - "iss": "https://api.botframework.com", - "aud": "peanut" - }, True), credentials) + await GovernmentChannelValidation.validate_identity( + ClaimsIdentity( + {"iss": "https://api.botframework.com", "aud": "peanut"}, True + ), + credentials, + ) assert "Unauthorized" in str(excinfo.value) From 7d5d0e75ce07d05b3a79db46a9145cd3196a570f Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Wed, 31 Jul 2019 10:51:25 -0700 Subject: [PATCH 3/5] pylint fix --- .../botframework/connector/auth/authentication_constants.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py index 0024f5c8b..429b7ccb6 100644 --- a/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py +++ b/libraries/botframework-connector/botframework/connector/auth/authentication_constants.py @@ -45,7 +45,10 @@ class AuthenticationConstants(ABC): ) # TO BOT FROM ENTERPRISE CHANNEL: OpenID metadata document for tokens coming from MSA - TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT = "https://{channelService}.enterprisechannel.botframework.com/v1/.well-known/openidconfiguration" + TO_BOT_FROM_ENTERPRISE_CHANNEL_OPEN_ID_METADATA_URL_FORMAT = ( + "https://{channelService}.enterprisechannel.botframework.com" + "/v1/.well-known/openidconfiguration" + ) # TO BOT FROM EMULATOR: OpenID metadata document for tokens coming from MSA TO_BOT_FROM_EMULATOR_OPEN_ID_METADATA_URL = ( From ae8ae8f0c42450e4a6474506ea35827dc34626e4 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 2 Aug 2019 11:05:56 -0700 Subject: [PATCH 4/5] Fixes in BF connector and emulator validation --- .../botbuilder/core/bot_framework_adapter.py | 17 +++++++++++++++++ .../connector/auth/emulator_validation.py | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py index 2557468d0..301f8ff2e 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py @@ -16,6 +16,9 @@ from botframework.connector import Channels, EmulatorApiClient from botframework.connector.aio import ConnectorClient from botframework.connector.auth import ( + ChannelValidation, + GovernmentChannelValidation, + GovernmentConstants, MicrosoftAppCredentials, JwtTokenValidation, SimpleCredentialProvider, @@ -91,6 +94,20 @@ def __init__(self, settings: BotFrameworkAdapterSettings): ) self._is_emulating_oauth_cards = False + if self.settings.open_id_metadata: + ChannelValidation.open_id_metadata_endpoint = self.settings.open_id_metadata + GovernmentChannelValidation.OPEN_ID_METADATA_ENDPOINT = ( + self.settings.open_id_metadata + ) + + if JwtTokenValidation.is_government(self.settings.channel_service): + self._credentials.oauth_endpoint = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_LOGIN_URL + ) + self._credentials.oauth_scope = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + ) + async def continue_conversation( self, bot_id: str, reference: ConversationReference, callback: Callable ): diff --git a/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py b/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py index 1d94135a8..bbe99ce1c 100644 --- a/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py +++ b/libraries/botframework-connector/botframework/connector/auth/emulator_validation.py @@ -25,6 +25,10 @@ class EmulatorValidation: "https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0", # ??? "https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/", + # Auth for US Gov, 1.0 token + "https://sts.windows.net/cab8a31a-1906-4287-a0d8-4eef66b95f6e/", + # Auth for US Gov, 2.0 token + "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0", ], audience=None, clock_tolerance=5 * 60, From ac2904d0610dbc913f698b5ee9f52c11a4322069 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Fri, 9 Aug 2019 09:58:31 -0700 Subject: [PATCH 5/5] Added check to env variables for BF Adapter --- .../botbuilder/core/bot_framework_adapter.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py index 301f8ff2e..00cc672cf 100644 --- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py +++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py @@ -4,6 +4,7 @@ import asyncio import base64 import json +import os from typing import List, Callable, Awaitable, Union, Dict from msrest.serialization import Model from botbuilder.schema import ( @@ -16,6 +17,7 @@ from botframework.connector import Channels, EmulatorApiClient from botframework.connector.aio import ConnectorClient from botframework.connector.auth import ( + AuthenticationConstants, ChannelValidation, GovernmentChannelValidation, GovernmentConstants, @@ -84,6 +86,13 @@ class BotFrameworkAdapter(BotAdapter, UserTokenProvider): def __init__(self, settings: BotFrameworkAdapterSettings): super(BotFrameworkAdapter, self).__init__() self.settings = settings or BotFrameworkAdapterSettings("", "") + self.settings.channel_service = self.settings.channel_service or os.environ.get( + AuthenticationConstants.CHANNEL_SERVICE + ) + self.settings.open_id_metadata = ( + self.settings.open_id_metadata + or os.environ.get(AuthenticationConstants.BOT_OPEN_ID_METADATA_KEY) + ) self._credentials = MicrosoftAppCredentials( self.settings.app_id, self.settings.app_password,