Skip to content

Commit 1381b43

Browse files
Added custom authorizer sample (#315)
Adds custom authorizer support and sample to the Python V2 SDK Commit log: * Added custom authorizer sample * Fixed documentation generation error and fixed custom authorizer password not being set correctly in CI * Added custom authorizer sample to README * Code review changes: fixed naming and cleaned up code
1 parent 7cb3272 commit 1381b43

7 files changed

+231
-14
lines changed

awsiot/mqtt_connection_builder.py

Lines changed: 107 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,26 +127,38 @@ def _get(kwargs, name, default=None):
127127
_metrics_str = None
128128

129129

130-
def _get_metrics_str():
130+
def _get_metrics_str(current_username=""):
131131
global _metrics_str
132+
133+
username_has_query = False
134+
if not current_username.find("?") is -1:
135+
username_has_query = True
136+
132137
if _metrics_str is None:
133138
try:
134139
import pkg_resources
135140
try:
136141
version = pkg_resources.get_distribution("awsiotsdk").version
137-
_metrics_str = "?SDK=PythonV2&Version={}".format(version)
142+
_metrics_str = "SDK=PythonV2&Version={}".format(version)
138143
except pkg_resources.DistributionNotFound:
139-
_metrics_str = "?SDK=PythonV2&Version=dev"
144+
_metrics_str = "SDK=PythonV2&Version=dev"
140145
except BaseException:
141146
_metrics_str = ""
142147

143-
return _metrics_str
148+
if not _metrics_str == "":
149+
if username_has_query:
150+
return "&" + _metrics_str
151+
else:
152+
return "?" + _metrics_str
153+
else:
154+
return ""
144155

145156

146157
def _builder(
147158
tls_ctx_options,
148159
use_websockets=False,
149160
websocket_handshake_transform=None,
161+
use_custom_authorizer=False,
150162
**kwargs):
151163

152164
ca_bytes = _get(kwargs, 'ca_bytes')
@@ -165,7 +177,7 @@ def _builder(
165177
else:
166178
port = 8883
167179

168-
if port == 443 and awscrt.io.is_alpn_available():
180+
if port == 443 and awscrt.io.is_alpn_available() and use_custom_authorizer is False:
169181
tls_ctx_options.alpn_list = ['http/1.1'] if use_websockets else ['x-amzn-mqtt-ca']
170182

171183
socket_options = awscrt.io.SocketOptions()
@@ -185,7 +197,7 @@ def _builder(
185197

186198
username = _get(kwargs, 'username', '')
187199
if _get(kwargs, 'enable_metrics_collection', True):
188-
username += _get_metrics_str()
200+
username += _get_metrics_str(username)
189201

190202
client_bootstrap = _get(kwargs, 'client_bootstrap')
191203
if client_bootstrap is None:
@@ -414,3 +426,92 @@ def websockets_with_custom_handshake(
414426
websocket_handshake_transform=websocket_handshake_transform,
415427
websocket_proxy_options=websocket_proxy_options,
416428
**kwargs)
429+
430+
def _add_to_username_parameter(input_string, parameter_value, parameter_pretext):
431+
"""
432+
Helper function to add parameters to the username in the direct_with_custom_authorizer function
433+
"""
434+
return_string = input_string
435+
436+
if not return_string.find("?") is -1:
437+
return_string += "&"
438+
else:
439+
return_string += "?"
440+
441+
if not parameter_value.find(parameter_pretext) is -1:
442+
return return_string + parameter_value
443+
else:
444+
return return_string + parameter_pretext + parameter_value
445+
446+
def direct_with_custom_authorizer(
447+
auth_username=None,
448+
auth_authorizer_name=None,
449+
auth_authorizer_signature=None,
450+
auth_password=None,
451+
**kwargs) -> awscrt.mqtt.Connection:
452+
"""
453+
This builder creates an :class:`awscrt.mqtt.Connection`, configured for an MQTT connection using a custom
454+
authorizer. This function will set the username, port, and TLS options.
455+
456+
This function takes all :mod:`common arguments<awsiot.mqtt_connection_builder>`
457+
described at the top of this doc, as well as...
458+
459+
Keyword Args:
460+
auth_username (`str`): The username to use with the custom authorizer.
461+
If provided, the username given will be passed when connecting to the custom authorizer.
462+
If not provided, it will check to see if a username has already been set (via username="example")
463+
and will use that instead.
464+
If no username has been set then no username will be sent with the MQTT connection.
465+
466+
auth_authorizer_name (`str`): The name of the custom authorizer.
467+
If not provided, then "x-amz-customauthorizer-name" will not be added with the MQTT connection.
468+
469+
auth_authorizer_signature (`str`): The signature of the custom authorizer.
470+
If not provided, then "x-amz-customauthorizer-name" will not be added with the MQTT connection.
471+
472+
auth_password (`str`): The password to use with the custom authorizer.
473+
If not provided, then no passord will be set.
474+
"""
475+
476+
_check_required_kwargs(**kwargs)
477+
username_string = ""
478+
479+
if auth_username is None:
480+
if not _get(kwargs, "username") is None:
481+
username_string += _get(kwargs, "username")
482+
else:
483+
username_string += auth_username
484+
485+
if not auth_authorizer_name is None:
486+
username_string = _add_to_username_parameter(
487+
username_string, auth_authorizer_name, "x-amz-customauthorizer-name=")
488+
if not auth_authorizer_signature is None:
489+
username_string = _add_to_username_parameter(
490+
username_string, auth_authorizer_signature, "x-amz-customauthorizer-signature=")
491+
492+
kwargs["username"] = username_string
493+
kwargs["password"] = auth_password
494+
kwargs["port"] = 443
495+
496+
tls_ctx_options = awscrt.io.TlsContextOptions()
497+
tls_ctx_options.alpn_list = ["mqtt"]
498+
499+
return _builder(tls_ctx_options=tls_ctx_options,
500+
use_websockets=False,
501+
use_custom_authorizer=True,
502+
**kwargs)
503+
504+
505+
def new_default_builder(**kwargs) -> awscrt.mqtt.Connection:
506+
"""
507+
This builder creates an :class:`awscrt.mqtt.Connection`, without any configuration besides the default TLS context options.
508+
509+
This requires setting the connection details manually by passing all the necessary data
510+
in :mod:`common arguments<awsiot.mqtt_connection_builder>` to make a connection
511+
"""
512+
_check_required_kwargs(kwargs)
513+
tls_ctx_options = awscrt.io.TlsContextOptions()
514+
515+
return _builder(tls_ctx_options=tls_ctx_options,
516+
use_websockets=False,
517+
kwargs=kwargs)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
env
6+
7+
pushd $CODEBUILD_SRC_DIR/samples/
8+
9+
ENDPOINT=$(aws secretsmanager get-secret-value --secret-id "unit-test/endpoint" --query "SecretString" | cut -f2 -d":" | sed -e 's/[\\\"\}]//g')
10+
AUTH_NAME=$(aws secretsmanager get-secret-value --secret-id "unit-test/authorizer-name" --query "SecretString" | cut -f2 -d":" | sed -e 's/[\\\"\}]//g')
11+
AUTH_PASSWORD=$(aws secretsmanager get-secret-value --secret-id "unit-test/authorizer-password" --query "SecretString" | cut -f2 -d":" | sed -e 's/[\\\"\}]//g')
12+
13+
echo "Custom authorizer connect test"
14+
python3 custom_authorizer_connect.py --endpoint $ENDPOINT --custom_auth_authorizer_name $AUTH_NAME --custom_auth_password $AUTH_PASSWORD
15+
16+
popd

codebuild/samples/linux-smoke-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ phases:
1212
- $CODEBUILD_SRC_DIR/codebuild/samples/connect-linux.sh
1313
- $CODEBUILD_SRC_DIR/codebuild/samples/pkcs11-connect-linux.sh
1414
- $CODEBUILD_SRC_DIR/codebuild/samples/pubsub-linux.sh
15+
- $CODEBUILD_SRC_DIR/codebuild/samples/connect-auth-linux.sh
1516
post_build:
1617
commands:
1718
- echo Build completed on `date`

samples/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* [Websocket Connect](#websocket-connect)
66
* [PKCS#11 Connect](#pkcs11-connect)
77
* [Windows Certificate Connect](#windows-certificate-connect)
8+
* [Custom Authorizer Connect](#custom-authorizer-connect)
89
* [Shadow](#shadow)
910
* [Jobs](#jobs)
1011
* [Fleet Provisioning](#fleet-provisioning)
@@ -324,6 +325,39 @@ To run this sample with a basic certificate from AWS IoT Core:
324325
python3 windows_cert_connect.py --endpoint <endpoint> --ca_file <path to root CA> --cert <path to certificate>
325326
```
326327
328+
## Custom Authorizer Connect
329+
330+
This sample makes an MQTT connection and connects through a [Custom Authorizer](https://docs.aws.amazon.com/iot/latest/developerguide/custom-authentication.html). On startup, the device connects to the server and then disconnects. This sample is for reference on connecting using a custom authorizer.
331+
332+
Your Thing's [Policy](https://docs.aws.amazon.com/iot/latest/developerguide/iot-policies.html) must provide privileges for this sample to connect.
333+
334+
<details>
335+
<summary>(see sample policy)</summary>
336+
<pre>
337+
{
338+
"Version": "2012-10-17",
339+
"Statement": [
340+
{
341+
"Effect": "Allow",
342+
"Action": [
343+
"iot:Connect"
344+
],
345+
"Resource": [
346+
"arn:aws:iot:<b>region</b>:<b>account</b>:client/test-*"
347+
]
348+
}
349+
]
350+
}
351+
</pre>
352+
</details>
353+
354+
Run the sample like this:
355+
``` sh
356+
python3 custom_authorizer_connect.py --endpoint <endpoint> --ca_file <path to root CA> --custom_auth_authorizer_name <authorizer name>
357+
```
358+
359+
You will need to setup your Custom Authorizer so that the lambda function returns a policy document. See [this page on the documentation](https://docs.aws.amazon.com/iot/latest/developerguide/config-custom-auth.html) for more details and example return result.
360+
327361
## Shadow
328362

329363
This sample uses the AWS IoT

samples/command_line_utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ def add_common_topic_message_commands(self):
7777
def add_common_logging_commands(self):
7878
self.register_command(self.m_cmd_verbosity, "<Log Level>", "Logging level.", default=io.LogLevel.NoLogs.name, choices=[x.name for x in io.LogLevel])
7979

80+
def add_common_custom_authorizer_commands(self):
81+
self.register_command(self.m_cmd_custom_auth_username, "<str>", "The name to send when connecting through the custom authorizer (optional)")
82+
self.register_command(self.m_cmd_custom_auth_authorizer_name, "<str>", "The name of the custom authorizer to connect to (optional but required for everything but custom domains)")
83+
self.register_command(self.m_cmd_custom_auth_username, "<str>", "The signature to send when connecting through a custom authorizer (optional)")
84+
self.register_command(self.m_cmd_custom_auth_password, "<str>", "The password to send when connecting through a custom authorizer (optional)")
85+
8086
"""
8187
Returns the command if it exists and has been passed to the console, otherwise it will print the help for the sample and exit the application.
8288
"""
@@ -191,3 +197,7 @@ def get_proxy_options_for_mqtt_connection(self):
191197
m_cmd_message = "message"
192198
m_cmd_topic = "topic"
193199
m_cmd_verbosity = "verbosity"
200+
m_cmd_custom_auth_username = "custom_auth_username"
201+
m_cmd_custom_auth_authorizer_name = "custom_auth_authorizer_name"
202+
m_cmd_custom_auth_authorizer_signature = "custom_auth_authorizer_signature"
203+
m_cmd_custom_auth_password = "custom_auth_password"

samples/custom_authorizer_connect.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0.
3+
4+
from awsiot import mqtt_connection_builder
5+
from uuid import uuid4
6+
7+
# This sample is similar to `samples/basic_connect.py` but it connects
8+
# through a custom authorizer rather than using a key and certificate.
9+
10+
# Parse arguments
11+
import command_line_utils
12+
cmdUtils = command_line_utils.CommandLineUtils(
13+
"Custom Authorizer Connect - Make a MQTT connection using a custom authorizer.")
14+
cmdUtils.add_common_mqtt_commands()
15+
cmdUtils.add_common_logging_commands()
16+
cmdUtils.add_common_custom_authorizer_commands()
17+
cmdUtils.register_command("client_id", "<str>",
18+
"Client ID to use for MQTT connection (optional, default='test-*').",
19+
default="test-" + str(uuid4()))
20+
# Needs to be called so the command utils parse the commands
21+
cmdUtils.get_args()
22+
23+
24+
def on_connection_interrupted(connection, error, **kwargs):
25+
# Callback when connection is accidentally lost.
26+
print("Connection interrupted. error: {}".format(error))
27+
28+
29+
def on_connection_resumed(connection, return_code, session_present, **kwargs):
30+
# Callback when an interrupted connection is re-established.
31+
print("Connection resumed. return_code: {} session_present: {}".format(return_code, session_present))
32+
33+
34+
if __name__ == '__main__':
35+
36+
# Create MQTT connection with a custom authorizer
37+
mqtt_connection = mqtt_connection_builder.direct_with_custom_authorizer(
38+
endpoint=cmdUtils.get_command_required(cmdUtils.m_cmd_endpoint),
39+
ca_filepath=cmdUtils.get_command(cmdUtils.m_cmd_ca_file),
40+
auth_username=cmdUtils.get_command(cmdUtils.m_cmd_custom_auth_username),
41+
auth_authorizer_name=cmdUtils.get_command(cmdUtils.m_cmd_custom_auth_authorizer_name),
42+
auth_authorizer_signature=cmdUtils.get_command(cmdUtils.m_cmd_custom_auth_authorizer_signature),
43+
auth_password=cmdUtils.get_command(cmdUtils.m_cmd_custom_auth_password),
44+
on_connection_interrupted=on_connection_interrupted,
45+
on_connection_resumed=on_connection_resumed,
46+
client_id=cmdUtils.get_command("client_id"),
47+
clean_session=False,
48+
keep_alive_secs=30)
49+
50+
print("Connecting to {} with client ID '{}'...".format(
51+
cmdUtils.get_command(cmdUtils.m_cmd_endpoint), cmdUtils.get_command("client_id")))
52+
53+
connect_future = mqtt_connection.connect()
54+
55+
# Future.result() waits until a result is available
56+
connect_future.result()
57+
print("Connected!")
58+
59+
# Disconnect
60+
print("Disconnecting...")
61+
disconnect_future = mqtt_connection.disconnect()
62+
disconnect_future.result()
63+
print("Disconnected!")

samples/windows_cert_connect.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,9 @@
1717
cmdUtils = command_line_utils.CommandLineUtils("Windows Cert Connect - Make a MQTT connection using Windows Store Certificates.")
1818
cmdUtils.add_common_mqtt_commands()
1919
cmdUtils.add_common_logging_commands()
20-
cmdUtils.register_command("port", "<int>",
21-
"Connection port for direct connection. " +
22-
"AWS IoT supports 433 and 8883 (optional, default=8883).",
23-
False, int)
2420
cmdUtils.register_command("client_id", "<str>",
2521
"Client ID to use for MQTT connection (optional, default='test-*').",
2622
default="test-" + str(uuid4()))
27-
cmdUtils.register_command("cert", "<path>",
28-
"Path to certificate in Windows certificate store. " +
29-
"e.g. \"CurrentUser\\MY\\6ac133ac58f0a88b83e9c794eba156a98da39b4c\"",
30-
True, str)
3123
# Needs to be called so the command utils parse the commands
3224
cmdUtils.get_args()
3325

0 commit comments

Comments
 (0)