AWS Security Blog

How to Use Your Own Identity and Access Management Systems to Control Access to AWS IoT Resources

AWS IoT is a managed cloud platform that lets connected devices easily and securely interact with cloud applications and other devices by using the Message Queuing Telemetry Transport (MQTT) protocol, HTTP, and the MQTT over the WebSocket protocol. Every connected device must authenticate to AWS IoT, and AWS IoT must authorize all requests to determine if access to the requested operations or resources is allowed. Until now, AWS IoT has supported two kinds of authentication techniques: the Transport Layer Security (TLS) mutual authentication protocol and the AWS Signature Version 4 algorithm. Callers must possess either an X.509 certificate or AWS security credentials to be able to authenticate their calls. The requests are authorized based on the policies attached to the certificate or the AWS security credentials.

However, many of our customers have their own systems that issue custom authorization tokens to their devices. These systems use different access control mechanisms such as OAuth over JWT or SAML tokens. AWS IoT now supports a custom authorizer to enable you to use custom authorization tokens for access control. You can now use custom tokens to authenticate and authorize HTTPS over the TLS server authentication protocol and MQTT over WebSocket connections to AWS IoT.

In this blog post, I explain the AWS IoT custom authorizer design and then demonstrate the end-to-end process of setting up a custom authorizer to authorize an HTTPS over TLS server authentication connection to AWS IoT using a custom authorization token. In this process, you configure an AWS Lambda function, which will validate the token and provide the policies necessary to control the access privileges on the connection.

Note: This post assumes you are familiar with AWS IoT and AWS Lambda enough to perform steps using the AWS CLI and OpenSSL. Make sure you are running the latest version of the AWS CLI.

Overview of the custom authorizer workflow

The following numbered diagram illustrates the custom authorizer workflow. The diagram is followed by explanations of the steps.

To explain the steps of the workflow as illustrated in the preceding diagram:

  1. The connected device uses the AWS SDK or custom client to make an HTTPS or MQTT over WebSocket request to the AWS IoT gateway. The request includes a custom authorization token and the name of a preconfigured authorizer Lambda function that is to be invoked to validate the authorization token.
  2. The AWS IoT gateway identifies the authorization token in the request and determines that it is a custom authorization request. The gateway checks if a Lambda authorization function is configured for the AWS account that owns the device. If yes, the gateway invokes the Lambda function by passing the authorization token.
  3. The Lambda function verifies the authorization token and returns to the AWS IoT gateway a principal that identifies the connection and a set of AWS IoT policies that determine permissions for the requested operation. The Lambda function also returns two time-to-live values that determine the validity (in seconds) of the connection and policy documents.
  4. The AWS IoT gateway invokes the AWS policy evaluation engine to authorize the requested operation against the set of policies that are received from the authorizer Lambda function.
  5. The AWS policy evaluation engine evaluates the policy documents and returns the result to the AWS IoT gateway. The gateway then caches the policy documents.
  6. If the policy evaluation allows the requested operation, the AWS IoT gateway serves the request. If the requested operation is denied, the AWS IoT gateway returns an AccessDenied exception with the status code of 403 to the device (the red line in the preceding diagram).

Outline of the steps to configure and use the custom authorizer

The following are the steps you will perform as part of the solution:

  1. Create a Lambda function: Start by creating a Lambda function. The function takes the authorization token in the input, verifies it, and returns authorization policies to determine the caller’s permissions for the requested operation.
  2. Create an authorizer: Create an authorizer in AWS IoT. An authorizer is an alternate data model pointing to a pre-created Lambda function. You can specify in the custom authorization request an authorizer name. AWS IoT invokes a corresponding Lambda function to verify the authorization token. You may update the authorizer to point to a different Lambda function and thus easily control which Lambda function to invoke to verify an authorization token.
  3. Designate the default authorizer: You may designate one of your authorizers as the default authorizer. AWS IoT invokes the default authorizer implicitly when a custom authorization request does not include a specific authorizer name.
  4. Add Lambda invocation permissions: AWS IoT needs to be able to call your Lambda function on your behalf to verify the token in the custom authorization request. You need to associate a resource policy with the Lambda function to allow this.
  5. Test the Lambda function: When the Lambda function and the custom authorizer are configured, use the test function to verify that they are functioning correctly.
  6. Invoke the custom authorizer: Finally, make an HTTPS request to the gateway that includes a custom authorization token. The request invokes the custom authorizer.

Deploy the solution

1.  Create a Lambda function

In this step, I show you how to create a Lambda function that runs your custom authorizer code and returns a set of essential attributes to authorize the request.

Sign in to your AWS account and create from the Lambda console a Lambda function that takes as input an authorization token and performs whichever actions are necessary to validate the token, as shown in the following code example. The output, in JSON format, must contain the following attributes:

  1. IsAuthenticated: This is a Boolean (true/false) attribute that indicates whether the request is authenticated.
  2. PrincipalId: This is an alphanumeric string; the minimum length is 1 character, and the maximum length is 128 characters. This string acts as an identifier associated with the token that is received in the custom authorization request.
  3. PolicyDocuments: This is a list of JSON formatted policy documents following the same conventions as an AWS IoT policy. The list contains at most 10 policy documents, each of which can be a maximum of 2,048 characters.
  4. DisconnectAfterInSeconds: This indicates the maximum duration (in seconds) of the connection to the AWS IoT gateway, after which it will be disconnected. The minimum value is 300 seconds, and the maximum value is 86,400 seconds.
  5. RefreshAfterInSeconds: This is the period between policy refreshes. When it lapses, the Lambda function is invoked again to allow for policy refreshes. The minimum value is 300 seconds, and the maximum value is 86,400 seconds.

The following code example is a Lambda function in Node.js 6.10 that authenticates a token.

// A simple authorizer Lambda function demonstrating
// how to parse auth token and generate response 

exports.handler = function(event, context, callback) { 
    var token = event.token; 
    switch (token.toLowerCase()) { 
        case 'allow': 
            callback(null, generateAuthResponse(token, 'Allow')); 
        case 'deny': 
            callback(null, generateAuthResponse(token, 'Deny')); 
        default: 
            callback("Error: Invalid token"); 
    }
};

// Helper function to generate authorization response 
var generateAuthResponse = function(token, effect) { 
    // Invoke your preferred identity provider 
    // to get the authN and authZ response. 
    // Following is just for simplicity sake 

    var authResponse = {}; 
    authResponse.isAuthenticated = true; 
    authResponse.principalId = 'principalId'; 
    
    var policyDocument = {}; 
    policyDocument.Version = '2012-10-17'; 
    policyDocument.Statement = []; 
    var statement = {}; 
    statement.Action = 'iot:Publish'; 
    statement.Effect = effect; 
    statement.Resource = "arn:aws:iot:us-east-1:<your_aws_account_id>:topic/customauthtesting"; 
    policyDocument.Statement[0] = statement; 
    authResponse.policyDocuments = [policyDocument]; 
    authResponse.disconnectAfterInSeconds = 3600; 
    authResponse.refreshAfterInSeconds = 600; 
    
    return authResponse; 
}

The preceding function takes an authorization token and returns an object containing the five attributes (a-e) described earlier in this step.

2.  Create an authorizer

Now that you have created the Lambda function, you will create an authorizer with AWS IoT pointing to the Lambda function. You do this so that you can easily control which Lambda function to invoke to verify an authorization token. The following attributes are required to create an authorizer:

  1. AuthorizerName: This is the name of the authorizer. It is a string; the minimum length is 1 character, and the maximum length is 128 characters.
  2. AuthorizerFunctionArn: This is the Amazon Resource Name (ARN) of the Lambda function that you created in the previous step.
  3. TokenKeyName: This specifies the key name that your device chooses, which indicates the token in the custom authorization HTTP request header. It is a string; the minimum length is 1 character, and the maximum length is 128 characters.
  4. TokenSigningPublicKeys: This is a map of one (minimum) and two (maximum) public keys. It is a 2,048-bit key at minimum. You need to generate a key pair, sign the custom authorization token and include the digital signature in the request in order to be able to use custom authorization successfully. AWS IoT needs the corresponding public key to verify the digital signature. Therefore, you must provide the public key while creating an authorizer.
  5. Status: This specifies the status (ACTIVE or INACTIVE) of the authorizer. It is optional. The default value is INACTIVE.

Run the following command in OpenSSL to create an RSA key pair that will be used to generate a token signature.

openssl genrsa -out private.pem 2048

Run the following command to create the public key out of the key pair generated in the previous step.

openssl rsa -in private.pem -outform PEM -pubout -out public.pem

You need to store the key pair securely and pass the public key in the TokenSigningPublicKeys parameter when creating the authorizer in AWS IoT. You will use the private key in the key pair to sign the authorization token and include the signature in the custom authorization request.

Run the following command in the AWS CLI to create an authorizer.

aws iot create-authorizer --authorizer-name <authorizer_name> --authorizer-function-arn <Lambda_function_arn> --token-key-name <token_key_name> --token-signing-public-keys FIRST_KEY="<public_key_pem>" --status ACTIVE

The following is sample output of the create-authorizer command. It contains the authorizerName and authorizerArn.

{
    "authorizerName": "MyAuthorizer", 
    "authorizerArn": "arn:aws:iot:us-east-1:<your_aws_account_id>:authorizer/MyAuthorizer"
}

You must set the authorizer in the ACTIVE status to be invoked. The describe-authorizer API returns the attributes of an existing authorizer. You can use the following command to verify if all the attributes in the authorizer are set correctly.

aws iot describe-authorizer --authorizer-name <authorizer_name>

The following is sample output of the describe-authorizer command.

{
  "authorizerDescription": {
    "status": "ACTIVE",
    "lastModifiedDate": 1515351241.568,
    "authorizerName": "<authorizer_name>",
    "tokenKeyName": "<token_key_name>",
    "authorizerArn": "arn:aws:iot:us-east-1:<your_aws_account_id>:authorizer/MyAuthorizer",
    "tokenSigningPublicKeys": {
        FIRST_KEY": "<public_key_pem>"
    },
    "creationDate": 1515351241.568,
    "authorizerFunctionArn": "arn:aws:lambda:us-east-1:<your_aws_account_id>:function:MyCustomAuthFunction"
  }
}

3.  Designate the default authorizer (optional)

You can have multiple authorizers in your account. You can designate one of them as the default so that AWS IoT invokes it if the custom authorization request does not specify an authorizer name. Run the following command in the AWS CLI to designate a default authorizer.

aws iot set-default-authorizer --authorizer-name <authorizer_name>

The following is sample output of the set-default-authorizer command. It contains the authorizerName and authorizerArn.

{
    "authorizerName": "MyAuthorizer", 
    "authorizerArn": "arn:aws:iot:us-east-1:<your_aws_account_id>:authorizer/MyAuthorizer"
}

4. Add Lambda invocation permissions

AWS IoT needs to invoke your authorizer Lambda function to evaluate the custom authorizer token. You need to provide AWS IoT appropriate permissions to invoke your Lambda function when a custom authorization request is made. Use the AWS CLI with the AddPermission command to grant the AWS IoT service principal permission to call lambda:InvokeFunction, as shown in the following command.

aws lambda add-permission --function-name <lambda_function_name> --principal iot.amazonaws.com --source-arn <authorizer_arn> --statement-id Id-123 --action "lambda:InvokeFunction"

The following is sample output of the add-permission command.

{
    "Statement": "{\"Sid\":\"Id-123\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"iot.amazonaws.com\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:us-east-1:<your_aws_account_id>:function:MyCustomAuthFunction\",\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:iot:us-east-1:<your_aws_account_id>:authorizer/MyAuthorizer\"}}}"
}

Note that you are using the precreated AuthorizerArn as the SourceArn while granting the permission. The Lambda function gets triggered only if the source ARN provided by AWS IoT during the invocation matches with the SourceArn that you have given permission. Even if your Lambda function ARN is put in an authorizer owned by someone else, they cannot trigger the function causing illegitimate charge to you.

5. Test the Lambda function

To verify the configuration, test to see if AWS IoT can invoke the Lambda function and get the correct output. You can do this by using the TestInvokeAuthorizer API. The API takes the following input:

  1. AuthorizerName: This is the name of the authorizer. It is a string; the minimum length is 1 character, and the maximum length is 128 characters.
  2. Token: This specifies the custom authorization token to authorize the request to the AWS IoT gateway. It is a string; the minimum length is 1 character, and the maximum length is 1,024 characters.
  3. TokenSignature: This is the token signature generated by using the private key with the SHA256withRSA algorithm. This is a string with a maximum length 2,560 characters. You must Base64-encode the signature while passing it as an input (the command follows).

Run the following command in OpenSSL to generate a signature for string, allow.

echo -n allow > token.txt
openssl dgst -sha256 -sign private.pem -out token.sign token.txt

Run the following command to Base64-encode the string.

base64 token.sign > token.sign.b64

Now, run the following command in the AWS CLI to test your authorizer.

aws iot test-invoke-authorizer --authorizer-name <authorizer_name> --token allow --token-signature <token_signature>

If AWS IoT is able to invoke the Lambda function successfully, the output of the TestInvokeAuthorizer API will be exactly the same as the output of the Lambda function. The following is sample output of the test-invoke-authorizer command for the Lambda function you created in Step 1 earlier in this post.

{
    "isAuthenticated": true,
    "refreshAfterInSeconds": 600,
    "policyDocuments": [
        {\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"iot:Publish\",\"Effect\":\"Allow\",\"Resource\":\"arn:aws:iot:us-east-1:<your_aws_account_id>:topic/customauthtesting\"}]}"
    ],
    "disconnectAfterInSeconds": 3600,
    "principalId": "principalId"
}

6. Invoke the custom authorizer

You can now make a custom authorization request to the AWS IoT gateway. Custom authorization is supported for HTTPS over TLS server authentication and MQTT over WebSocket connections. Regardless of the protocol, requests must include the following attributes in the header. Note that supplying these attributes through query parameters is not allowed for security reasons.

  1. Token: This specifies the authorization token. The header name must be the same as the TokenKeyName of the authorizer.
  2. TokenSignature: This specifies the Base64-encoded digital signature of the token string. The header name must be x-amz-customauthorizer-signature.
  3. AuthorizerName: This specifies the name of one of the ACTIVE authorizers preconfigured in your account. The header name must be x-amz-customauthorizer-name. If this field is not present and you have preconfigured a default custom authorizer for your account, the AWS IoT gateway invokes the default authorizer to authenticate and authorize the request.

Run the following command in the AWS CLI to obtain your AWS account-specific AWS IoT endpoint. See the DescribeEndpoint API documentation for further details. You need to specify the endpoint when making requests to the AWS IoT gateway.

aws iot describe-endpoint

The following is sample output of the describe-endpoint command. It contains the endpointAddress.

{
    "endpointAddress": "<your_aws_account_specific_prefix>.iot.us-east-1.amazonaws.com"
}

Now, make an HTTPS request to the AWS IoT gateway to post a message. You may use your preferred HTTP client for the request. I use curl in the following example, which posts the message, “Hello from custom auth,” to an MQTT topic, customauthtesting, by using the token, allow.

curl -X POST -k -i -N -H "Host: <your_aws_iot_endpoint>" -H "X-Amz-CustomAuthorizer-Name: <authorizer_name>" -H "X-Amz-CustomAuthorizer-Signature: <token_signature>" -H "<token_key_name>: allow" -H "User-Agent: aws-cli/1.10.65 Python/2.7.11 Darwin/16.1.0 botocore/1.4.55" --data 'Hello from custom auth' https://<your_aws_iot_endpoint>/topics/customauthtesting

If the command succeeds, you will see the following output.

HTTP/1.1 200 OK
content-type: application/json
content-length: 65
date: Sun, 07 Jan 2018 19:56:12 GMT
x-amzn-RequestId: e7c13873-6c61-a12c-1216-9a935901c130
connection: keep-alive
{"message":"OK","traceId":"e7c13873-6c61-a12c-1216-9a935901c130"}

Conclusion

In this blog post, I have shown how to configure a custom authorizer in your AWS account and use it to authorize an HTTPS over TLS server authentication connection and publish a message to a specific MQTT topic by using a custom authorization token. Similarly, you can use custom authorizers to authorize MQTT over WebSocket requests to the AWS IoT gateway to publish messages to a specific topic or subscribe to a topic filter.

If you have comments about this blog post, submit them in the “Comments” section below. If you have questions about or issues implementing this solution, start a new thread in the AWS IoT forum.

– Ramkishore