aws / aws-iot-device-sdk-js

SDK for connecting to AWS IoT from a device using JavaScript/Node.js
Apache License 2.0
964 stars 384 forks source link

Bug report: Can Connect/Subscribe but not Publish (auth with Cognito) #139

Closed olgeorge closed 7 years ago

olgeorge commented 7 years ago

I am using Cognito identity pool together with Cognito user pool to authenticate my users. Both policies attached to the identity pool as well as each identity allow broad connect, subscribe, receive and publish. I'm able to connect to IoT with authenticated identities as well as subscribe to a topic and receive messages on it (published by AWS console). Moreover, I'm able to successfully use fine-grained permissions by using ${cognito-identity.amazonaws.com:sub} inside the policy attached to the cognito identity, and they get replaced by the identityId with the format eu-west-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (not shown in this code sample), so I'm pretty sure I'm doing things the right way with authentication and authorization.

However, after I call device.publish(topic, message) the client disconnects. The permissions are as broad as possible, they are the same for both policies for subscribe and publish. One works but the other doesn't. Please help!

IAM role attached to the identity pool:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iot:AttachPrincipalPolicy",
                "iot:AttachThingPrincipal",
                "iot:CreateKeysAndCertificate",
                "iot:CreatePolicy",
                "iot:CreateThing",
                "iot:DescribeEndpoint",
                "iot:GetPolicy"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "iot:Connect",
                "iot:Subscribe",
                "iot:Receive",
                "iot:Publish"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

MyPolicy attached to federated identities:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "iot:Connect",
                "iot:Subscribe",
                "iot:Receive",
                "iot:Publish"
            ],
            "Resource": ["*"]
        }
    ]
}

CloudWatch logs that clearly show the successful connect, subscribe and the disconnect that follows

2017-08-09 10:27:17.389 TRACEID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx PRINCIPALID:xxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials [INFO] EVENT:MQTT Client Connect MESSAGE:Connect Status: SUCCESS
2017-08-09 10:27:17.389 TRACEID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx PRINCIPALID:xxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials [INFO] EVENT:MQTT Client Connect MESSAGE: IpAddress: xxx.xxx.xxx.xxx SourcePort: 50509
2017-08-09 10:27:17.471 TRACEID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx PRINCIPALID:xxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials [INFO] EVENT:MQTTClient Subscribe TOPICNAME:test-topic MESSAGE:Subscribe Status: SUCCESS
2017-08-09 10:27:17.471 TRACEID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx PRINCIPALID:xxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials [INFO] EVENT:MQTTClient Subscribe MESSAGE: IpAddress: xxx.xxx.xxx.xxx SourcePort: 50509
2017-08-09 10:27:17.710 TRACEID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx PRINCIPALID:xxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials [INFO] EVENT:MQTT Client Disconnect MESSAGE:Disconnect Status: SUCCESS
2017-08-09 10:27:17.710 TRACEID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx PRINCIPALID:xxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials/xxxxxxxxxxxxxxxxxxxxx:CognitoIdentityCredentials [INFO] EVENT:MQTT Client Disconnect MESSAGE: IpAddress: xxx.xxx.xxx.xxx SourcePort: 50509

Here's the Javascript code:

const idToken = cognitoUserSession.getIdToken().getJwtToken();
AWS.config.region = 'eu-west-1';
if (AWS.config.credentials) {
    AWS.config.credentials.clearCachedId();
}
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityPoolId: 'eu-west-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
    Logins: {
        'cognito-idp.eu-west-1.amazonaws.com/eu-west-1_xxxxxxxxx': idToken,
    }
});

const cognitoIdentity = new AWS.CognitoIdentity();
AWS.config.credentials.get((error, response) => {
    if (error) { throw error; }
    const attachParams = {
        policyName: 'MyPolicy',
        principal: AWS.config.credentials.identityId,
    }

    const iot = new AWS.Iot();
    iot.attachPrincipalPolicy(attachParams, (error, response) => {
        if (error) { throw error; }
        const params = {
            IdentityId: AWS.config.credentials.identityId,
            Logins: {
                'cognito-idp.eu-west-1.amazonaws.com/eu-west-1_xxxxxxxxx': idToken,
            }
        };

        cognitoIdentity.getCredentialsForIdentity(params, (error, response) => {
            if (error) { throw error; }
            const device = awsIot.device({
                region: 'eu-west-1',
                host: 'xxxxxxxxxxxxxx.iot.eu-west-1.amazonaws.com',
                clientId: 'test',
                protocol: 'wss',
                maximumReconnectTimeMs: 2000,
                debug: true,
                accessKeyId: response.Credentials.AccessKeyId,
                secretKey: response.Credentials.SecretKey,
                sessionToken: response.Credentials.SessionToken,
            });
            device.on('connect', () => {
                // This succeeds
                console.log('Connect success');

                const topic = 'test-topic';
                console.log(`Subscribe to ` + topic);
                device.subscribe(topic); // This succeeds. The client receives messages published from AWS console

                console.log(`Publish to ` + topic);
                device.publish('test-topic', { test: "test" }) // After this line the client disconnects
            });
            device.on('close', () => {
                console.log('Disconnect'); // This happens after device.publish
                device.end(); // prevent reconnect for test purposes
            });
        })
    })
})
olgeorge commented 7 years ago

I've found the problem. The publish method expects a string, not an object. The following line fixes it:

device.publish('test-topic', '{ "test": "test" }')

I'm still leaving this issue open as a suggestion to add proper input validation and error messages inside the SDK. Afaik the client would disconnect when an unauthorized publish action is attempted, which adds confusion to finding the the source of the bug.

fengsongAWS commented 7 years ago

Hi @olgeorge , Thanks for your interest in aws-iot-device-sdk-js. It is good to see your problem was resolved. We will take this in our backlog.

blbradley commented 5 years ago

@olgeorge Thanks for your report. I know it's been a long time; how were you able to debug this? Just by CloudWatch IoT logs? I think I experienced something similar. The client showed no signs of disconnecting, but I'm not printing anything in a disconnect handler, either.

blbradley commented 5 years ago

I logged the connect, close, and reconnect events. Incorrect permissions turns into a reconnect loop. Very unforgiving.