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:
This blog post assumes that you:
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.
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:
cd ./app
gradle wrapper
./gradlew build
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}/*"
]
}
]
}
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 works similar to the user registration. This registration workflow follows:
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}/*"
]
}
]
}
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:
Once the AWSIotMqttClient is created we can connect and test the capabilities of our role.
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).
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.
cd ./infrastructure
cdk destroy
y
when prompted.