Tel: (+44) 1482 234021 | info@wearesauce.io

Accessing AWS IoT MQTT from React Native

We recently discovered that the AWS IoT Device SDK does not support React Native. It has a few platform dependencies that aren't available by default (fs particularly). One often mentioned solution was to use rn-nodeify (https://github.com/tradle/rn-nodeify), but we are using Yarn Workspaces to share dependencies between React and React Native apps and this would have really compromised that, so we went looking for something that would work for us.

The AWS IoT SDK is an extension on MQTT.js, and it handles the connection signing and ThingShadow while MQTT.js does all of the Pub/Sub work. Luckily for us we don't actually need the ThingShadow features at the moment, so only needed to meet the dependencies of MQTT.js and give it an AWS compatible URL.

MQTT.js still has a few unsupported platform requirements but they were easily met with the following dependencies and a shim (imported as the first file in the project). MQTT didn't error without the shim, but it didn't connect to a simple test broker.

The dependencies we imported were:

  • buffer

  • process

  • global

  • mqtt

  • websocket-stream

And the shim:

// shim.js

import global from 'global';
import { Buffer } from 'buffer';
import process from 'process';

global.Buffer = Buffer;
global.process = process;
if (!global.self) {
  global.self = global;
}

We then needed to make it work with an AWS Broker. AWS provide the code for signing the connection here (https://docs.aws.amazon.com/iot/latest/developerguide/protocols.html). We tided it up for our ESLint rules.

// sigV4Utils.js

import AWS from 'aws-sdk/dist/aws-sdk-react-native';

export default function SigV4Utils() {}

SigV4Utils.getSignatureKey = function (key, date, region, service) {
  const kDate = AWS.util.crypto.hmac(`AWS4${key}`, date, 'buffer');
  const kRegion = AWS.util.crypto.hmac(kDate, region, 'buffer');
  const kService = AWS.util.crypto.hmac(kRegion, service, 'buffer');
  const kCredentials = AWS.util.crypto.hmac(kService, 'aws4_request', 'buffer');
  return kCredentials;
};

SigV4Utils.getSignedUrl = function (host, region, credentials) {
  const datetime = AWS.util.date.iso8601(new Date()).replace(/[:-]|\.\d{3}/g, '');
  const date = datetime.substr(0, 8);

  const method = 'GET';
  const protocol = 'wss';
  const uri = '/mqtt';
  const service = 'iotdevicegateway';
  const algorithm = 'AWS4-HMAC-SHA256';

  const credentialScope = `${date}/${region}/${service}/aws4_request`;
  let canonicalQuerystring = `X-Amz-Algorithm=${algorithm}`;
  canonicalQuerystring += `&X-Amz-Credential=${encodeURIComponent(`${credentials.accessKeyId}/${credentialScope}`)}`;
  canonicalQuerystring += `&X-Amz-Date=${datetime}`;
  canonicalQuerystring += '&X-Amz-SignedHeaders=host';

  const canonicalHeaders = `host:${host}\n`;
  const payloadHash = AWS.util.crypto.sha256('', 'hex');
  const canonicalRequest = `${method}\n${uri}\n${canonicalQuerystring}\n${canonicalHeaders}\nhost\n${payloadHash}`;

  const stringToSign = `${algorithm}\n${datetime}\n${credentialScope}\n${AWS.util.crypto.sha256(canonicalRequest, 'hex')}`;
  const signingKey = SigV4Utils.getSignatureKey(credentials.secretAccessKey, date, region, service);
  const signature = AWS.util.crypto.hmac(signingKey, stringToSign, 'hex');

  canonicalQuerystring += `&X-Amz-Signature=${signature}`;
  if (credentials.sessionToken) {
    canonicalQuerystring += `&X-Amz-Security-Token=${encodeURIComponent(credentials.sessionToken)}`;
  }

const requestUrl = `${protocol}://${host}${uri}?${canonicalQuerystring}`;
  return requestUrl;
};

On its own this wasn't enough. Our connection was immediately rejected by AWS and we couldn't tell if the problem was with our credentials, code or back end.

We read the AWS IoT SDK to see if there was any more customisation to MQTT it provides. We discovered it massively simplified the connection builder for websockets to look like the following:

// mqttWs.js

const websocket = require('websocket-stream');

function buildBuilder(client, opts) {
  return websocket(opts.url, ['mqttv3.1'], opts.websocketOptions);
}

module.exports = buildBuilder;

Then to tie it all together to make a viable connection.

import mqtt from 'mqtt';

import SigV4Utils from './sigV4Utils';
import mqttWs from './mqttWs';

export default (endpoint, region, credentials) => {
  const ioturl = SigV4Utils.getSignedUrl(endpoint, region, credentials);

  function wrapper(client) {
    return mqttWs(client, { url: ioturl });
  }

  return mqtt.MqttClient(wrapper, { url: ioturl });
};

For our React web app we chose to stick with the full SDK as that just works out of the box (if using the new ATS endpoints).