PearceCodes

Using AWS Cognito to secure MQTT topics

2022-02-17 - Tags:

How to use AWS Cognito with AWS Iot Core policies to secure MQTT access

Summary

Asynchronous messaging and pub/sub message models are becoming increasingly popular. AWS Iot Core provides a managed MQTT broker with policy based access control. This blog demonstrates how Iot Core Policies work with AWS Cognito Identities and Iot Core Certificates to control access to Iot resources.

This blog walks through a sample application that supports automated creation of users and devices, but maintains the principal of least privilege. In this application one user might have many devices. I hope this helps make the interaction between Cognito Identities and Iot Core Policies more clear. I had the following requirements:

Application diagram

Application Diagram

Prerequisites

This blog post assumes that you:

Deploy Infrastructure

To start, install AWS CDK and setup an IAM user with permissions to deploy this stack to your aws account. There is a great guide on the cdk workshop.

Download the repo: [https://github.com/Pearcekieser/mqtt-auth-poc] This repo has two parts ./infrastructure and ./app. The ./infrastructure directory contains a CDK package that sets up all the necessary infrastructure for the demo. The ./app directory contains a kotlin library with integration tests that demonstrates how to use Congnito with Iot Core Policies to restrict user permissions.

Go to the infrastructure folder cd ./infrastructure

Run cdk bootstrap to prepare your account.

Run cdk deploy to deploy the infrastructure stack for this demo.

This stack automatically configures a Cognito UserPool, Cognito UserPoolClient, an IdentityPool, and an IAM Role for the IdentityPool to issue to authenticated users.

Demo application

The code is kotlin library that wraps the AWS Java SDK. Under the hood this application uses the following libraries:

implementation(platform("software.amazon.awssdk:bom:2.17.88"))
implementation("software.amazon.awssdk:iot") // iot control plane client
implementation("software.amazon.awssdk:sts") // sts client to get admin credentials for the test
implementation("software.amazon.awssdk:cognitoidentity") // identityPoolClient
implementation("software.amazon.awssdk:cognitoidentityprovider") // userPoolClient

implementation("com.amazonaws:aws-iot-device-sdk-java:1.3.9") // iot device sdk v1

This application is a script that uses the CognitoIdentityProviderClient, CognitoIdentityClient, IotClient, and Iot Java Device SDK v1. The application has a library that uses these clients to preform high level actions (e.g. createUser, signIn, etc.). It has two integration tests (TODO add links) that verify our security requirements.

To run the application:

  1. Change into the app directory: cd ./app
  2. Initialize the gradle environment: gradle wrapper
  3. Run gradle build to finish the job: ./gradlew build

User registration

To create a user, I use the CognitoIdentityProviderClient AdminCreateUserRequest to generate a user. This creates a user with a temporary password in our Cognito UserPool.

We also create an IoT Policy that will eventually allow the user to connect to the MQTT broker and send messages to their devices. The policy restricts the user to only attach with a client that matches the username, it can publish and subscribe to topics username/*.

The policy looks like this:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iot:Connect"
            ],
            "Resource": [
                "arn:aws:iot:us-west-2:111111111111:client/${userInfo.username}"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iot:Publish"
            ],
            "Resource": [
                "arn:aws:iot:us-west-2:111111111111:topic/${userInfo.username}/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iot:Subscribe"
            ],
            "Resource": [
                "arn:aws:iot:us-west-2:111111111111:topicfilter/${userInfo.username}/*"
            ]
        }
    ]
}

Authentication

When the user signs in, we use the ADMIN_USER_PASSWORD_AUTH flow (CognitoIdentityProviderClient.adminInitiateAuth) and automatically update the password if a new password is required (CognitoIdentityProviderClient.adminRespondToAuthChallenge). This return an authentication result, which contains the user’s idToken for this session. We use that idToken to get the user’s Cognito Identity ID (CognitoIdentityClient.getId) and to generate session credentials (CognitoIdentityClient.getCredentialsForIdentity).

Even though the Cognito Identity’s authenticated user role allows IoTFullAccess. That user still does not have access, we need to attach an IoT Policy which will allow the user to connect, publish, and subscribe to MQTT topics.

After the user authenticates we need to make sure the IoT Policy we created with the user is attached to the user’s Cognito Identity. In this sample app we just assume the policy always needs to be attached and attempt to attach it. In a real application you might want to try your IoT operation and only attempt to attach the certificate if you get a permissions issue on the IoT call. We cannot attach the policy at user creation time because when a user is created we do not know its Identity Pool IdentityID. We use the IotClient.attachPolicy:

val request = AttachPolicyRequest.builder()
    .target(authenticatedUser.identityId)
    .policyName(authenticatedUser.username)
    .build()
val response = iotClient.attachPolicy(request)

Device registration

Device registration works similar to the user registration. This registration workflow follows:

  1. create certificate (IotClient.createKeysAndCertificate)
  2. create the policy for the iot thing, like the user it allows one client (thingName) and pub/sub access to the topic username/thingname:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iot:Connect"
            ],
            "Resource": [
                "arn:aws:iot:$region:$awsAccount:client/${deviceInfo.thingName}"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iot:Publish"
            ],
            "Resource": [
                "arn:aws:iot:$region:$awsAccount:topic/${deviceInfo.username}/${deviceInfo.deviceName}/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iot:Subscribe"
            ],
            "Resource": [
                "arn:aws:iot:$region:$awsAccount:topicfilter/${deviceInfo.username}/${deviceInfo.deviceName}/*"
            ]
        }
    ]
}
  1. Attach the policy to the certificate (IotClient.attachPolicy)
  2. Activate the certificate (IotClient.updateCertificate)
  3. Create the IoT Thing (IotClient.createThing)
  4. Attach the policy to the IoT thing (IotClient.attachThingPrincipal)

Using the credentials

The IoT Device SDK (Java in this case but they also support other languages) supports both certificate and role based authentication. My MqttGateway has examples of using:

  1. STS and my local admin credentials from the aws cli config to authenticate
  2. role credentials from the Cognito Identity to support a user
  3. certificate credentials to support a device.

Once the AWSIotMqttClient is created we can connect and test the capabilities of our role.

The tests

There are two main test suites, one verifies the access controls of the Users (authenticated by Cognito Identities) and the other, Devices (authenticated by IoT Certificates).

Cleanup

The library and tests use cleanup methods to automatically delete the Certificates, Things, Policies, Identities, and Users that it took to make this work.

You will need delete the cloud formation stack to remove the infrastructure for this demo.

  1. Change into the infrastructure directory: cd ./infrastructure
  2. Run cdk destroy to tear delete the stack: cdk destroy
  3. Enter y when prompted.