From 13ca9a0c9408e9c4a37ff1d3de168f031983b15c Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Wed, 13 Dec 2023 15:29:20 -0800 Subject: [PATCH] Custom auth polish (#537) * Test driven development hur * Conditional signature encoding * Doc string updates --- .github/workflows/ci.yml | 2 +- awsiot/mqtt5_client_builder.py | 20 +++++++++---- awsiot/mqtt_connection_builder.py | 13 +++++---- test/test_mqtt.py | 41 ++++++++++++++++++++++++++- test/test_mqtt5.py | 47 ++++++++++++++++++++++++++++++- 5 files changed, 109 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index baad5074..e349c504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: - 'docs' env: - BUILDER_VERSION: v0.9.21 + BUILDER_VERSION: v0.9.53 BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: aws-iot-device-sdk-python-v2 diff --git a/awsiot/mqtt5_client_builder.py b/awsiot/mqtt5_client_builder.py index 2742d0c8..6f8adda4 100644 --- a/awsiot/mqtt5_client_builder.py +++ b/awsiot/mqtt5_client_builder.py @@ -179,6 +179,8 @@ import awscrt.auth import awscrt.io import awscrt.mqtt5 +import urllib.parse + DEFAULT_WEBSOCKET_MQTT_PORT = 443 DEFAULT_DIRECT_MQTT_PORT = 8883 @@ -629,8 +631,7 @@ def direct_with_custom_authorizer( auth_authorizer_signature (`str`): The digital signature of the token value in the `auth_token_value` parameter. The signature must be based on the private key associated with the custom authorizer. The signature must be base64 encoded. - Required if the custom authorizer has signing enabled. It is strongly suggested to URL-encode this value; - the SDK will not do so for you. + Required if the custom authorizer has signing enabled. auth_token_key_name (`str`): Key used to extract the custom authorizer token from MQTT username query-string properties. @@ -656,8 +657,12 @@ def direct_with_custom_authorizer( username_string, auth_authorizer_name, "x-amz-customauthorizer-name=") if auth_authorizer_signature is not None: + encoded_signature = auth_authorizer_signature + if "%" not in encoded_signature: + encoded_signature = urllib.parse.quote(encoded_signature) + username_string = _add_to_username_parameter( - username_string, auth_authorizer_signature, "x-amz-customauthorizer-signature=") + username_string, encoded_signature, "x-amz-customauthorizer-signature=") if auth_token_key_name is not None and auth_token_value is not None: username_string = _add_to_username_parameter(username_string, auth_token_value, auth_token_key_name + "=") @@ -707,8 +712,7 @@ def websockets_with_custom_authorizer( auth_authorizer_signature (`str`): The digital signature of the token value in the `auth_token_value` parameter. The signature must be based on the private key associated with the custom authorizer. The signature must be base64 encoded. - Required if the custom authorizer has signing enabled. It is strongly suggested to URL-encode this value; - the SDK will not do so for you. + Required if the custom authorizer has signing enabled. auth_token_key_name (`str`): Key used to extract the custom authorizer token from MQTT username query-string properties. @@ -738,8 +742,12 @@ def websockets_with_custom_authorizer( username_string, auth_authorizer_name, "x-amz-customauthorizer-name=") if auth_authorizer_signature is not None: + encoded_signature = auth_authorizer_signature + if "%" not in encoded_signature: + encoded_signature = urllib.parse.quote(encoded_signature) + username_string = _add_to_username_parameter( - username_string, auth_authorizer_signature, "x-amz-customauthorizer-signature=") + username_string, encoded_signature, "x-amz-customauthorizer-signature=") if auth_token_key_name is not None and auth_token_value is not None: username_string = _add_to_username_parameter(username_string, auth_token_value, auth_token_key_name + "=") diff --git a/awsiot/mqtt_connection_builder.py b/awsiot/mqtt_connection_builder.py index a78a1850..1790098a 100644 --- a/awsiot/mqtt_connection_builder.py +++ b/awsiot/mqtt_connection_builder.py @@ -123,6 +123,7 @@ import awscrt.auth import awscrt.io import awscrt.mqtt +import urllib.parse def _check_required_kwargs(**kwargs): @@ -529,8 +530,7 @@ def direct_with_custom_authorizer( auth_authorizer_signature (`str`): The digital signature of the token value in the `auth_token_value` parameter. The signature must be based on the private key associated with the custom authorizer. The signature must be base64 encoded. - Required if the custom authorizer has signing enabled. It is strongly suggested to URL-encode this value; - the SDK will not do so for you. + Required if the custom authorizer has signing enabled. auth_token_key_name (`str`): Key used to extract the custom authorizer token from MQTT username query-string properties. @@ -590,8 +590,7 @@ def websockets_with_custom_authorizer( auth_authorizer_signature (`str`): The digital signature of the token value in the `auth_token_value` parameter. The signature must be based on the private key associated with the custom authorizer. The signature must be base64 encoded. - Required if the custom authorizer has signing enabled. It is strongly suggested to URL-encode this value; - the SDK will not do so for you. + Required if the custom authorizer has signing enabled. auth_token_key_name (`str`): Key used to extract the custom authorizer token from MQTT username query-string properties. @@ -644,8 +643,12 @@ def _with_custom_authorizer(auth_username=None, username_string, auth_authorizer_name, "x-amz-customauthorizer-name=") if auth_authorizer_signature is not None: + encoded_signature = auth_authorizer_signature + if "%" not in encoded_signature: + encoded_signature = urllib.parse.quote(encoded_signature) + username_string = _add_to_username_parameter( - username_string, auth_authorizer_signature, "x-amz-customauthorizer-signature=") + username_string, encoded_signature, "x-amz-customauthorizer-signature=") if auth_token_key_name is not None and auth_token_value is not None: username_string = _add_to_username_parameter(username_string, auth_token_value, auth_token_key_name + "=") diff --git a/test/test_mqtt.py b/test/test_mqtt.py index 6df40e26..2b13dd37 100644 --- a/test/test_mqtt.py +++ b/test/test_mqtt.py @@ -19,6 +19,7 @@ CUSTOM_AUTHORIZER_NAME_UNSIGNED = os.environ.get("CUSTOM_AUTHORIZER_NAME_UNSIGNED") CUSTOM_AUTHORIZER_PASSWORD = os.environ.get("CUSTOM_AUTHORIZER_PASSWORD") CUSTOM_AUTHORIZER_SIGNATURE = os.environ.get("CUSTOM_AUTHORIZER_SIGNATURE") +CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED = os.environ.get("CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED") CUSTOM_AUTHORIZER_TOKEN_KEY_NAME = os.environ.get("CUSTOM_AUTHORIZER_TOKEN_KEY_NAME") CUSTOM_AUTHORIZER_TOKEN_VALUE = os.environ.get("CUSTOM_AUTHORIZER_TOKEN_VALUE") @@ -27,7 +28,7 @@ def has_custom_auth_environment(): return (CUSTOM_AUTHORIZER_ENDPOINT is not None) and (CUSTOM_AUTHORIZER_NAME_SIGNED is not None) and \ (CUSTOM_AUTHORIZER_NAME_UNSIGNED is not None) and (CUSTOM_AUTHORIZER_PASSWORD is not None) and \ (CUSTOM_AUTHORIZER_SIGNATURE is not None) and (CUSTOM_AUTHORIZER_TOKEN_KEY_NAME is not None) and \ - (CUSTOM_AUTHORIZER_TOKEN_VALUE is not None) + (CUSTOM_AUTHORIZER_TOKEN_VALUE is not None) and (CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED is not None) class Config: cache = None @@ -193,6 +194,25 @@ def test_mqtt311_builder_direct_signed_custom_authorizer(self): self._test_connection(connection) + @unittest.skipIf(not has_custom_auth_environment(), 'requires custom authentication env vars') + def test_mqtt311_builder_direct_signed_custom_authorizer_unencoded(self): + elg = EventLoopGroup() + resolver = DefaultHostResolver(elg) + bootstrap = ClientBootstrap(elg, resolver) + + connection = mqtt_connection_builder.direct_with_custom_authorizer( + auth_username="", + auth_authorizer_name=CUSTOM_AUTHORIZER_NAME_SIGNED, + auth_authorizer_signature=CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED, + auth_password=CUSTOM_AUTHORIZER_PASSWORD, + auth_token_key_name=CUSTOM_AUTHORIZER_TOKEN_KEY_NAME, + auth_token_value=CUSTOM_AUTHORIZER_TOKEN_VALUE, + endpoint=CUSTOM_AUTHORIZER_ENDPOINT, + client_id=create_client_id(), + client_bootstrap=bootstrap) + + self._test_connection(connection) + @unittest.skipIf(not has_custom_auth_environment(), 'requires custom authentication env vars') def test_mqtt311_builder_direct_unsigned_custom_authorizer(self): elg = EventLoopGroup() @@ -244,3 +264,22 @@ def test_mqtt311_builder_websocket_signed_custom_authorizer(self): self._test_connection(connection) + @unittest.skipIf(not has_custom_auth_environment(), 'requires custom authentication env vars') + def test_mqtt311_builder_websocket_signed_custom_authorizer_unencoded(self): + elg = EventLoopGroup() + resolver = DefaultHostResolver(elg) + bootstrap = ClientBootstrap(elg, resolver) + + connection = mqtt_connection_builder.websockets_with_custom_authorizer( + auth_username="", + auth_authorizer_name=CUSTOM_AUTHORIZER_NAME_SIGNED, + auth_authorizer_signature=CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED, + auth_password=CUSTOM_AUTHORIZER_PASSWORD, + auth_token_key_name=CUSTOM_AUTHORIZER_TOKEN_KEY_NAME, + auth_token_value=CUSTOM_AUTHORIZER_TOKEN_VALUE, + endpoint=CUSTOM_AUTHORIZER_ENDPOINT, + client_id=create_client_id(), + client_bootstrap=bootstrap) + + self._test_connection(connection) + diff --git a/test/test_mqtt5.py b/test/test_mqtt5.py index d4e4bf37..a6adfbc8 100644 --- a/test/test_mqtt5.py +++ b/test/test_mqtt5.py @@ -21,6 +21,7 @@ CUSTOM_AUTHORIZER_NAME_UNSIGNED = os.environ.get("CUSTOM_AUTHORIZER_NAME_UNSIGNED") CUSTOM_AUTHORIZER_PASSWORD = os.environ.get("CUSTOM_AUTHORIZER_PASSWORD") CUSTOM_AUTHORIZER_SIGNATURE = os.environ.get("CUSTOM_AUTHORIZER_SIGNATURE") +CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED = os.environ.get("CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED") CUSTOM_AUTHORIZER_TOKEN_KEY_NAME = os.environ.get("CUSTOM_AUTHORIZER_TOKEN_KEY_NAME") CUSTOM_AUTHORIZER_TOKEN_VALUE = os.environ.get("CUSTOM_AUTHORIZER_TOKEN_VALUE") @@ -29,7 +30,7 @@ def has_custom_auth_environment(): return (CUSTOM_AUTHORIZER_ENDPOINT is not None) and (CUSTOM_AUTHORIZER_NAME_SIGNED is not None) and \ (CUSTOM_AUTHORIZER_NAME_UNSIGNED is not None) and (CUSTOM_AUTHORIZER_PASSWORD is not None) and \ (CUSTOM_AUTHORIZER_SIGNATURE is not None) and (CUSTOM_AUTHORIZER_TOKEN_KEY_NAME is not None) and \ - (CUSTOM_AUTHORIZER_TOKEN_VALUE is not None) + (CUSTOM_AUTHORIZER_TOKEN_VALUE is not None) and (CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED is not None) class Config: cache = None @@ -236,6 +237,28 @@ def test_mqtt5_builder_direct_signed_custom_authorizer(self): self._test_connection(client, callbacks) + @unittest.skipIf(not has_custom_auth_environment(), 'requires custom authentication env vars') + def test_mqtt5_builder_direct_signed_custom_authorizer_unencoded(self): + elg = EventLoopGroup() + resolver = DefaultHostResolver(elg) + bootstrap = ClientBootstrap(elg, resolver) + callbacks = Mqtt5TestCallbacks() + + client = mqtt5_client_builder.direct_with_custom_authorizer( + auth_username="", + auth_authorizer_name=CUSTOM_AUTHORIZER_NAME_SIGNED, + auth_authorizer_signature=CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED, + auth_password=CUSTOM_AUTHORIZER_PASSWORD, + auth_token_key_name=CUSTOM_AUTHORIZER_TOKEN_KEY_NAME, + auth_token_value=CUSTOM_AUTHORIZER_TOKEN_VALUE, + endpoint=CUSTOM_AUTHORIZER_ENDPOINT, + client_id=create_client_id(), + client_bootstrap=bootstrap, + on_lifecycle_connection_success=callbacks.on_lifecycle_connection_success, + on_lifecycle_stopped=callbacks.on_lifecycle_stopped) + + self._test_connection(client, callbacks) + @unittest.skipIf(not has_custom_auth_environment(), 'requires custom authentication env vars') def test_mqtt5_builder_direct_unsigned_custom_authorizer(self): elg = EventLoopGroup() @@ -277,6 +300,28 @@ def test_mqtt5_builder_websocket_signed_custom_authorizer(self): self._test_connection(client, callbacks) + @unittest.skipIf(not has_custom_auth_environment(), 'requires custom authentication env vars') + def test_mqtt5_builder_websocket_signed_custom_authorizer_unencoded(self): + elg = EventLoopGroup() + resolver = DefaultHostResolver(elg) + bootstrap = ClientBootstrap(elg, resolver) + callbacks = Mqtt5TestCallbacks() + + client = mqtt5_client_builder.websockets_with_custom_authorizer( + auth_username="", + auth_authorizer_name=CUSTOM_AUTHORIZER_NAME_SIGNED, + auth_authorizer_signature=CUSTOM_AUTHORIZER_SIGNATURE_UNENCODED, + auth_password=CUSTOM_AUTHORIZER_PASSWORD, + auth_token_key_name=CUSTOM_AUTHORIZER_TOKEN_KEY_NAME, + auth_token_value=CUSTOM_AUTHORIZER_TOKEN_VALUE, + endpoint=CUSTOM_AUTHORIZER_ENDPOINT, + client_id=create_client_id(), + client_bootstrap=bootstrap, + on_lifecycle_connection_success=callbacks.on_lifecycle_connection_success, + on_lifecycle_stopped=callbacks.on_lifecycle_stopped) + + self._test_connection(client, callbacks) + @unittest.skipIf(not has_custom_auth_environment(), 'requires custom authentication env vars') def test_mqtt5_builder_websocket_unsigned_custom_authorizer(self): elg = EventLoopGroup()