Explained: AWS IoT Custom Authorizer with Cryptographic Token Validation

6 minute read
Content level: Advanced
3

This article gives a brief explanation of the supported authentication options when using AWS IoT Core and demonstrate how setup Custom Authentication with the token validation feature to prevent unwanted traffic to trigger the Lambda Custom Authorizer.

Introduction

Signature Version 4 is the signing protocol that AWS uses to authenticate requests to AWS services. This is basically a hash-based message authentication code (or HMAC). In December 2015 AWS launched AWS IoT Core. This is one of the first services that in addition to SigV4, it supported Mutual-TLS using x509 client certificates. Later, AWS added support for Custom Authentication giving users maximum flexibility when choosing how to authenticate and authorise incoming requests. This feature can be useful if for example you have devices that use a custom bearer token or MQTT username and password to authenticate.

AWS IoT Core supported protocols and authentication

ProtocolOperations SupportedAuthentication
MQTT over WebSocketPublish, SubscribeSignature Version 4
MQTT over WebSocketPublish, SubscribeCustom authentication
MQTTPublish, SubscribeX.509 client certificate
MQTTPublish, SubscribeX.509 client certificate
MQTTPublish, SubscribeCustom authentication
HTTPSPublish onlySignature Version 4
HTTPSPublish onlyX.509 client certificate
HTTPSPublish onlyCustom authentication

The way Custom Authentication works is that AWS passes the incoming request to a Lambda Function. The Lambda function is responsible of AuthZ/N. You are probably wondering – “That’s a lot of potential Lambdas that I’ll need to pay for if not used properly! (i.e. abused) ” But do not worry, the service comes with a feature called Token Validation to ensure traffic is valid before passing the request to the Authorizer Lambda function. The service validates a token you create using your own private key. AWS does not participate in the signing process. All you have to do is share the respective public key with AWS and the arbitrary string you intend to sign. The Lambda won’t get executed at all if the signature check fails. The best thing is that checking if the token is valid or not does not incur charges.

Enter image description here

Setting up Token Validation - A minimal example

First, let’s create a private key. The service supports RSA keys that are at least 2048 bits in size.

openssl genrsa -out private_key.pem 2048

Then, generate the corresponding public key pair using the private key we just generated. We will need to upload the public key file contents to AWS at a later stage.

openssl rsa -in private_key.pem -pubout > public_key.pem

Choose an arbitrary string. I am using MyStringThatNeedsToBeSigned then I get its digest using the SHA256 algorithm.

echo -n "MyStringThatNeedsToBeSigned" | shasum -a 256
# 8de411c918c5ed2896a7dfdd8f838fdc441c36a22938b024958b2546f8e7bd6a

I should get the following 256 bit string as a result from the above operation 8de411c918c5ed2896a7dfdd8f838fdc441c36a22938b024958b2546f8e7bd6a. Now take those bits and sign them using your private key. The final step is to base64 encode the signature.

echo -n "8de411c918c5ed2896a7dfdd8f838fdc441c36a22938b024958b2546f8e7bd6a" \
| openssl pkeyutl -sign  -inkey private_key.pem -pkeyopt digest:sha256 \
| base64 
# Zq3D/…du8F421wVrH2iAkF40SQYNxpXG6KJybD9Am99rA9iWfAtJw9uV37sWxnUIWTD3fpnr60NeCimrp2mx3w==

Here is a simpler command that produces the same result:

echo -n "MyStringThatNeedsToBeSigned" \
| openssl dgst -sha256 -sign private_key.pem \
| base64

The expected result is a base64 encoded RSA signature. This is the string the devices will need to have in order to get past the token validation stage and trigger the Lambda that will do additional checks. For example, checking if the user name and password are valid or if an application-level bearer token is valid.

We need to create a Lambda before creating the Custom Authorizer. Here is a minimum lambda allows anyone with a valid signed token to get permissions to publish to the topic customAuhtTestTopic.

Warning: this is just an example. Typically the Lambda would check for other things like user name and passwords, etc.

import json

def lambda_handler(event, context):

    # Check for stuff (i.e. login / password, bearer token etc)

    return {
        "isAuthenticated": True, 
        "principalId": "myPrincipalDevice123",
        "disconnectAfterInSeconds": 86400, 
        "refreshAfterInSeconds": 300, 
         "policyDocuments": [
              {
                "Version": "2012-10-17",
                "Statement": [
                   {
                      "Action": "iot:Publish",
                      "Effect": "Allow",
                      "Resource": "arn:aws:iot:*:*:topic/customAuhtTestTopic"
                    }
                 ]
               }
            ]
    }

The next step is to create the actual Custom Authorizer. This setting is configured by going the AWS IoT Console then under Security -> Authorizers find the Create authorizer button. When creating the custom authorizer we need to specify the key-value pair we would like to use for our authentication scheme. I am using X-My-App-custom-key as the key, MyStringThatNeedsToBeSigned as the value (also referred as the token key name) and the contents of public_key.pem as the token signing key. Note that MyStringThatNeedsToBeSigned is not the value that the IoT device needs to send, instead the value an IoT device needs to send the base64 encoded signature of the sha256 digest of that string. And of course, the signature must be created using the corresponding private key.

Enter image description here

We also need to choose a Lambda. You must also ensure that it has the Lambda has enough resource-level permissions to allow custom the AWS IoT Custom authorizer to invoke it. Here is an AWS CLI command to add permissions to Lambda to be invoked by AWS IoT Core:

aws lambda add-permission \
--function-name arn:aws:lambda:eu-west-1:123456789012:function:TestIotCustomAuthorizer \
--principal iot.amazonaws.com \
--source-arn arn:aws:iot:eu-west-1:123456789012:authorizer/MyCustomAuthorizer \
--statement-id test1 \
--action "lambda:InvokeFunction"

We are done with all the setup steps. Now it’s time to test. The AWS CLI comes with a tool to test the authorizer.

aws iot test-invoke-authorizer \
--authorizer-name MyCustomAuthorizer \
--token MyStringThatNeedsToBeSigned \
--token-signature Zq3D/…du8F421wVrH2iAkF40SQYNxpXG6KJybD9Am99rA9iWfAtJw9uV37sWxnUIWTD3fpnr60NeCimrp2mx3w==

The expected result is something like this:

{
    "isAuthenticated": true,
    "principalId": "myPrincipalDevice123",
    "policyDocuments": [
        "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"iot:Publish\",\"Effect\":\"Allow\",\"Resource\":\"arn:aws:iot:*:*:topic/customAuhtTestTopic\"}]}"
    ],
    "refreshAfterInSeconds": 300,
    "disconnectAfterInSeconds": 86400
}

If this is all good, now we are ready to test using the data plane. I am using the HTTP protocol but the same credentials can be used with any of the other supported protocols like MQTT over TCP or MQTT over WSS.

curl \
--header "x-amz-customauthorizer-name: MyCustomAuthorizer" \
--header "X-My-App-custom-key: MyStringThatNeedsToBeSigned " \
--header "x-amz-customauthorizer-signature: Zq3D/…du8F421wVrH2iAkF40SQYNxpXG6KJybD9Am99rA9iWfAtJw9uV37sWxnUIWTD3fpnr60NeCimrp2mx3w==" \
--request POST \
--data "Hello, world" \
"https://example-ats.iot.eu-west-1.amazonaws.com/topics/customAuhtTestTopic?qos=1"
# {"message":"OK","traceId":"7b3d120b-b057-356b-f092-EXAMPLE"}
3 Comments

For a deployable solution you can refer to https://github.com/massi-ang/aws-iot-custom-authorizer-sample/

AWS
EXPERT
replied 7 months ago

does this sample work for MQTT over WSS? by following this article, Im trying to run code in a browser (mqtt via websocket ) but it fails to connect.

Suresh
replied 17 days ago

Hi, sorry for the delayed response.

This example uses the HTTP protocol. Unlike MQTT or MQTT over WebSockets, there is no concept of an IoT connection when using the HTTP protocol, hence no specific connection related permissions needed to publish messages.

You should be able to connect using MQTT over Websockets using the custom a custom authorizer if you modify the Lambda function to output a policy that contain an "allow" action for "iot:Connect". Take a look at some example policies here: https://docs.aws.amazon.com/iot/latest/developerguide/connect-policy.html

profile pictureAWS
EXPERT
Paco
replied 3 days ago