ブラウザでAWS IoTのMQTTブローカーを署名バージョン4認証で購読する

# 概要

Firefoxなどのブラウザを用いてAWS IoTのMQTTブローカーに接続します。ブラウザからの接続の場合、WebSocketでの接続になります。AWS IoT Coreデベロッパーガイドの[デバイス通信プロトコル](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/protocols.html)の表によるとMQTT over WebSocketはPublisおよびSubscribeに対応しており、認証方法は署名V4(SigV4)とカスタム認証を使うことになっています。

> |プロトコル|オペレーション|認証|ポート|
> |:--|:--|:--|:--|
> |MQTT over WebSocket|発行, 購読|署名V4|443|
> |MQTT over WebSocket|発行, 購読|カスタム認証|443|

https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/protocols.html

独自の認証を使いたい場合はLambdaオーソライザーを使うようですが、この記事では「とにかくシンプルに使う」を目的にしているので、署名V4を使った接続方法を解説します。ここではカスタム認証の方法は扱いません。

# 準備

## IAMユーザーの作成

接続に使用するIAMユーザーを作成します。コンソール接続は不要でプログラムからのみ使用します。例えばポリシーは以下のようにすることで、購読(subscribe)のみ可能です。発行(publish)したい場合は `"iot:Publish"` を `Action` の項目に追加します。`{REGION}`および`{AWS ACCOUNT ID}`、`{TOPIC}`は適切なものに指定してください。

```json:インラインポリシーの例
    {
	  "Version": "2012-10-17",
	  "Statement": [
		{
          "Sid": "IoT-20240915",
          "Effect": "Allow",
          "Action": [
            "iot:Subscribe",
            "iot:Connect",
            "iot:Receive"
          ],
          "Resource": [
            "arn:aws:iot:{REGION}:{AWS ACCOUNT ID}:topicfilter/{TOPIC}",
            "arn:aws:iot:{REGION}:{AWS ACCOUNT ID}:client/*",
            "arn:aws:iot:{REGION}:{AWS ACCOUNT ID}:topic/{TOPIC}"
          ]
        }
      ]
    }
```

ユーザーを作成したらアクセスキーとシークレットアクセスキーをメモしておきます。

## ブラウザ用AWS IoT Device SDKのビルド

Node.jsの場合はnpmでインストールすればすぐに使えるみたいですが、ブラウザ向けのjsファイルは自分で作る必要があります。browserifyでビルドします。以下はRocky Linux 9.4, node.js 20.11.1, npm 10.2.4で実行しました。

```bash
$ npm install browserify
$ git clone https://github.com/aws/aws-iot-device-sdk-js.git
$ cd aws-iot-device-sdk-js
$ npm run-script browserize
```

これで、browserディレクトリに`aws-iot-sdk-browser-bundle.js`ファイルが生成されます。

# Webページの作成

ブラウザで開くWebページを作成します。ここではできるだけ単純に以下の構成にしました。`www`ディレクトリはWeb公開ディレクトリです。環境に合わせて調整してください。ひょっとしたらHTTPS接続が必要かもです(すみませんHTTPSでしか試していません)。
Windows 10 22H2のFirefox 130およびEdge 128で動作確認しました。

- www/
    - aws-iot-sdk-browser-bundle.js
    - mqttclient.html

以下がトピックを購読するための最低限のコードです。エラー処理などはしていません。

```html:mqttclient.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>MQTT Client Test</title>
  <script src="./aws-iot-sdk-browser-bundle.js"></script>
</head>
<body>
  <p id="txtMessage">0</p>

  <script>
    const awsIot = require("aws-iot-device-sdk");   // SDKを読み込む
    const IOT_TOPIC = "MyTopic/test";               // 対象とするトピック
    const params = {
      accessKeyId:  "AKEXAMPLEACCESSKEYID",
      secretKey:    "MyExampleSecretKeyabcdef0123456789000000",
      region:       "ap-northeast-1",
      host:         "xxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com",  // iot:Data-ATSエンドポイント
      protocol:     "wss",
      clientId:     self.crypto.randomUUID(),   // 一意のクライアントID(何でもよい)
    };

    deviceIot = awsIot.device(params);  // 接続開始

    deviceIot.on("connect", () => {     // 接続されたら購読する
      console.debug("Connected!!");
      deviceIot.subscribe(IOT_TOPIC, undefined, (err, granted) => {
        console.debug("Subscribed!!");
      });
    });

    deviceIot.on("message", (topic, payload) => {  // メッセージが届いた
      const message = new TextDecoder().decode(payload);
      document.querySelector("#txtMessage").textContent = message;
    });
  </script>
</body>
</html>
```

`mqttclient.html`をブラウザで開くとメッセージの購読が始まります。

## ポイント

- `params.clientId`は`randomUUID()`で求めていますが、適当な文字列が使えます。
- `awsIot.device(params)`のオプションで`sessionToken`も使えるので、高度な認証を行いたい場合はセッショントークンを使ってもいいかもしれません。

## メッセージの発行

今回のHTMLには実装していませんが、`publish()`メソッドでメッセージを発行できます。
この場合、IAMユーザーに`iot:Publish`権限が必要となります。

```javascript:Publish Example
deviceIot.publish("MyTopic/test", '{"message": "Hello, IoT World!!"}');
```

# 参考

1. AWS IoT Coreにブラウザから様々な認証方法で接続してみる
    - https://qiita.com/poruruba/items/3367c34b01c6abcb1ef9
2. ブラウザでAWS IoT CoreにMQTTでPub/Subしたい場合にやるべきことリスト
    - https://blog.maripo.org/2022/07/browser_aws_iot_pubsub/
3. AWS IoT Coreデベロッパーガイド -- デバイス通信プロトコル
    - https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/protocols.html
4. AWS IoT: awsIot.device()
    - https://github.com/aws/aws-iot-device-sdk-js?tab=readme-ov-file#device

# 余談

最低限の実装というのがなかなか見つからず、署名V4でシンプルに接続したいだけなのに結構苦労しました。
TypeScriptは使わないので、AWS SDKをビルドするのが面倒…というのでできるだけシンプルな方法を探していたのですが、AWS IoT Device SDKは必要なんですね(他のやり方もあるのかもですが)。
私はM5Stackからのセンサー値の通信に使いたいなと思って試しましたが、ほぼリアルタイムな通信が簡単に実装できるので、IoTだけではなくブラウザ間通信など、アイデア次第でいろいろ遊べそうな気がします。