AWS CognitoとAWS IoTを連携させてみる

今回はAWS Cognitoで認証したユーザからAWS IoTのMQTTにPublishできるようにしたいと思います。

（過去を振り返ってみると、AWS Cognitoに集中して投稿してこれで記念すべき？！5つ目となりました。）

今回やることは、X.509でIoTのモノを認証するのではなく、Cognitoで認証したユーザをIoTのものとして認証し、Publish/Subscribeする、というものです。
触るAWSマネージドサービスは以下の通りです。

+ Amazon Cognito（フェデレーテッドアイデンティティ）
+ IAM（ロール）
+ AWS IoT（IoT Core）

特に、Cognitoおよびトークンについて知っていないと理解にはちょっと厳しいかも。
以下が参考になるかもです。

[AWS Cognitoにサインインしないと見れないLambdaを作る](https://qiita.com/poruruba/items/86668c9c63844e1afd32)

あと、AWS IoT SDK for Javascriptも使います。あと、AWS CLIも使うのでそのセットアップも完了している必要があります。
めげずに、行ってみましょう。

# フェデレーテッドアイデンティティのIDプールを作成する

まずは、AWS Cognitoで、フェデレーテッドアイデンティティのIDプールを作成するところから始めます。
AWS IoTとCognito連携するためには、ユーザープールだけではだめで、フェデレーテッドアイデンティティのIDプールが必要のようです。
AWS Cognitoのユーザープールの方は作成してある前提です。まだの方は以下をご参照ください。

[AWS CognitoにGoogleとLINEアカウントを連携させる](https://qiita.com/poruruba/items/189945dc64edfe1f2464)

AWS Cognitoの管理Webコンソールから入って、IDプールの管理を選択します。その後、「新しいIDプールの作成」ボタンを押下します。

https://ap-northeast-1.console.aws.amazon.com/cognito/home?region=ap-northeast-1

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/dd2d8b1a-4a7e-496b-d8a0-67694082c027.png)

以下のような画面が出てきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/405ff3b3-0634-3c52-c5a1-99181f0e8fe7.png)

IDプール名に適当な名前を入力します。例として、「TestIdPool」とします。
認証プロバイダを設定するのですが、Cognitoを選択し、ユーザープールIDとアプリクライアントIDを入力します。
ユーザープールIDとアプリクライアントIDは、前回以下で作ったものを流用します。AWS Cognitoの管理Webコンソールの先頭ページから、「ユーザープールの管理」を選択すると表示されるプールIDとアプリクライアントIDです。

[AWS CognitoにGoogleとLINEアカウントを連携させる](https://qiita.com/poruruba/items/189945dc64edfe1f2464)

入力したら、「IDプールの作成」ボタンを押下します。
そうすると、作成するIDプール用にロールを新規に作成するかを聞かれます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/5efad1ab-ee9b-d04f-f316-dd6cc02a9094.png)

ここは無難に新規作成しておきます。IDプールのほかに、IAMにも以下の名前のロールが自動的に作成されます。

* Cognito_TestIdPoolAuth_Role
* Cognito_TestIdPoolUnauth_Role

作成が完了すると、IDプール→ダッシュボード→IDプールの編集 と選択していくと、IDプールのIDが採番されているのがわかります。後でこのIDを使います。（ここらへんからIDだらけでわけわかんなくなります）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/dac5b8a3-eb5f-85e5-ff7c-c836fb431c16.png)

ここで一つやっておくべきことがあります。
認証プロバイダの設定です。
Cognitoタブのところに、ユーザプールIDとアプリクライアントIDを指定しておきます。
アプリクライアントIDは、のちほどのAWS IoT連携させるサインインユーザの認証を行うときに選択するものを選んでください。

# AWS IoTのMQTTを触ってみる

https://ap-northeast-1.console.aws.amazon.com/iot/home?region=ap-northeast-1

うーん、どっから手を付けてよいか迷うので、とりあえず、MQTTでPublish/Subscribeしてみます。

左側のナビゲーションから「設定」を選択します。
カスタムエンドポイントのところに、「エンドポイント」が表示されています。これを覚えておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/b2991aed-17d2-7c86-c166-99221d8cadd6.png)

次に、左側のナビゲーションから「テスト」を選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/10fa4290-babf-e0b4-decc-c0f98425e089.png)

「トピックのサブスクリプション」に、適当なトピック名を入力して「トピックへのサブスクライブ」ボタンを押下します。例として、「test_sub」にします。
以下のように、誰かがPublishしてくれるのを待ち受けます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/327a9cb5-75d9-e890-1eb1-27e3cafd95fc.png)

早速、Publishしてみます。同じ画面からPublishしても面白くないので、Javascriptからやってみます。

その前に、まずPublishできる権利を持ったユーザをIAMから作成します。
例えば、「iot」という名前のユーザを作成し、「AWSIoTFullAccess」というポリシーを割り当てました。（今回は適当に上記の権限を割り当てましたが、実際にはきちんと絞った権限を与えるようにしてください）。
アクセスの種類として、「プログラムによるアクセス」を選択し、アクセスキーIDとシークレットアクセスキーをメモっておいてください。

それでは、Webページを作っていきます。
フォルダ構成は以下の通りです。またしても、以下の投稿で作ったものの再流用です。

[AWS CognitoにGoogleとLINEアカウントを連携させる](https://qiita.com/poruruba/items/189945dc64edfe1f2464)

```
.
├── cert
│   ├── server.crt
│   ├── server.csr
│   └── server.key
├── index.js
├── node_modules
├── package.json
└── public
    ├── auth.html
    ├── auth_custom.html
    ├── iot.html
    ├── dist
    │   └── js
    │       └── aws-iot-sdk-browser-bundle.js
    └── js
        ├── start.js
        ├── start_custom.js
        └── start_iot.js
```

今回新たに追加するのは、以下の3つです。

* aws-iot-sdk-browser-bundle.js
　　AWS IoTをJavascriptから呼び出すためのものです。おおもとはAWSが提供してくれてはいるのですが、ブラウザから使うには上記ファイルをご自身で作る必要がありまして、以下のURLの「Browser Applications」というところを制覇する必要があります。（なんだかんだ言ってこれが一番めんどいかも。。。）(https://github.com/aws/aws-iot-device-sdk-js)

* start_iot.js
　　以下に、コードを示します。

* iot.html
　　以下に、コードを示します。

```js
const MqttTopicSubscribe = 'test_sub';
const MqttHost = 'XXXXXXXXXX.iot.ap-northeast-1.amazonaws.com';

const MqttAccessKeyId = 'XXXXXXXXXXXX';
const MqttSecretKey = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXX';

var awsIot = require('aws-iot-device-sdk');
var deviceIot;

function subscribe_call(){
	try{
		var parent = this;

		deviceIot = awsIot.device({
			region: 'ap-northeast-1',
			clientId: 'clientid_1234',
			accessKeyId: MqttAccessKeyId,
			secretKey: MqttSecretKey,
			protocol: 'wss',
			port: 443,
			host: MqttHost
		});

		deviceIot.on('message', function(topic, payload) {
			console.log('deviceIot.on(message)');
			console.log(payload.toString());

			var elem = document.getElementById("message");
			elem.innerText = payload.toString();
		});

		deviceIot.subscribe(MqttTopicSubscribe, undefined, function (err, granted){
			if( err ){
				console.log('iot_subscribe error: ' + err);
				return;
			}
			console.log(granted);
		});
	}catch(error){
		console.log('iot_subscribe error: ' + error);
	}
}

var counter = 0;

function publish_call(){
	try{
		counter++;
		var payload = {
			message: 'Hello World(' + counter + ')!'
		};
		deviceIot.publish(MqttTopicSubscribe, JSON.stringify(payload), undefined, function(err){
			if( err ){
				console.log('iot_publish error: ' + err);
				return;
			}
		});
	}catch(error){
		console.log('iot_publish error: ' + error);
	}
}
```

```html
<HTML lang="ja">
<HEAD>
	<META http-equiv="Content-Type" content="text/html; charset=utf-8">	 
	<META http-equiv="Content-Security-Policy" content="default-src * 'unsafe-inline'; style-src * 'unsafe-inline'; media-src * blob:; img-src *; script-src * 'unsafe-inline';">
	<TITLE>テスト</TITLE>	 
</HEAD>
<BODY>
	<H1><a href="">テスト</a></H1>

	<BUTTON id="subscribe_call" onclick="subscribe_call()">subscribe呼び出し</BUTTON><br>
	<BUTTON id="publish_call" onclick="publish_call()">publish呼び出し</BUTTON><br>
	<DIV id="message"></DIV>

	<script src="dist/js/aws-iot-sdk-browser-bundle.js"></script>
	<script src="js/start_iot.js"></script>
</BODY>
</HTML>
```

書き換えていただく必要がある個所は、start_iot.jsの以下の変数定義の部分です。

* MqttHost
　　先ほどメモった、AWS IoTにおける、カスタムエンドポイントのエンドポイントのURLです。

* MqttAccessKeyId

* MqttSecretKey
　　これも先ほど作ったIAMユーザ「iot」のアクセスキーIDとアクセスシークレットです。

* clientIdは何でもいいです。

それでは早速実行してみます。
Webブラウザからiot.htmlを開いて、「subscribe呼び出し」ボタンを押下します。
ブラウザの開発コンソールログに、何やら以下のような表示がされましたでしょうか？表示されていれば成功です。以下は、Chromeの場合です。

```
[{…}]
 0:{topic: "test_sub", qos: 0}
 length:1
 __proto__:Array(0)
```

続けて、「publish呼び出し」ボタンを押下します。以下のように「{“message”:”Hello World(1)!”}」と表示されれば成功です。
publish_call()の中の変数payloadで指定した文字列が配信されました。AWS IoTの管理コンソールで待ち受けていたページにも、同じ文字列が追加されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/f5a17645-4ee6-c35f-9d80-7bb9ad1faebf.png)
 
![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/e41b47c1-3248-de8c-b6ec-8a6864878d91.png)

# AWS IoTにCognitoを設定する

さて、これからが本番です。先ほど触ったAWS IoTに、Cognitoを設定します。

まずは、ポリシーを作成します。ポリシーは、外部(Cognitoなど)から接続されたときにAWS IoTでできる範囲を制限するための設定です。

AWS IoT管理コンソールから、左側のナビゲーションの安全性を選択すると表示される「ポリシー」を選択します。そして、「作成」ボタンを押下します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/82574adf-32d8-3b45-1aab-940c6352aa77.png)

名前には適当な名前を付けます。例えば、「TestIotPolicy」とします。
とりあえず、アクションには「iot:*」、リソースARNには「arn:aws:iot:ap-northeast-1:XXXXXXXX:topic/test_sub」とします。XXXXXXの部分にはすでに値が入力されているかと思いますが、AWSアカウントIDです。「test_sub」は、さっき使ったトピック名ですが、また使っちゃおうと思います。すなわちtest_subというトピックにIoTのいずれかの操作ができることを示したポリシーです。

(2018/12/21 追記)
なんか、最近ポリシの書き方が厳しくなったような気がします。
iot:Publishとiot:Subscribeのリソースの書き方が微妙に違うので、それぞれごとに分けて書かないといけないようです。(これにかなりはまりました。。。)

```:example
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect"
      ],
      "Resource": [
        "*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Subscribe"
      ],
      "Resource": [
        "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:topicfilter/test_sub"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Receive"
      ],
      "Resource": [
        "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:topic/test_sub"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Publish"
      ],
      "Resource": [
        "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:topic/test_sub"
      ]
    }
  ]
}
```

最後に、「作成」ボタンを押下して、作成を完了させます。

次に、このポリシーに、Cognitoのユーザを割り当てます。
ちょっとめんどいです。

[参考情報]
https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/thing-groups.html#group-attach-policy
https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/pub-sub-policy.html#pub-sub-policy-cognito

まずは、Cognitoのユーザプールのユーザでログインし、IDトークンを取得します。クライアントIDには、IDプールの認証プロバイダに指定したものを使います。
前回作成したWebページを使います。

[AWS CognitoにGoogleとLINEアカウントを連携させる](https://qiita.com/poruruba/items/189945dc64edfe1f2464)

IDトークンが取得出来たら、次に、AWS CLIからの作業になります。Cognitoのサインインユーザに対してアイデンティティIDを割り当てます。

> aws cognito-identity get-id --identity-pool-id 【IDプールのID】--logins 【プールID】=【IDトークン】

以下が得られます。

```
{
    "IdentityId": 【アイデンティティID】
}
```

続けざまに、生成したアイデンティティIDを使って以下を実行します。アイデンティティIDにポリシーをアタッチします。

> aws iot attach-policy --policy-name "TestIotPolicy" --target 【アイデンティティID】

これで割り当て完了です。
IDだらけで紛らわしいので、以下補足しておきます。

+ IDプールのID：AWS CognitoのフェデレーテッドアイデンティティのIDプールのIDです。
+ プールID：AWS CognitoのユーザープールのプールIDです。
+ IDトークン：先ほどサインインして取得したIDトークンです。
+ アイデンティティID：ユーザープールのユーザIDに対して、フェデレーテッドアイデンティティが新規にユニークに払い出したIDです。

ふと、AWS IoTの管理Webコンソールから、ポリシーの証明書を見てみると、COGNITO IDとラベルづけられたものが追加されています。
これまでAWS IoTを使ったことがある方はお気づきかと思いますが、Thingsの証明書にポリシを割り当てても、COGNITO IDといったラベルなんて付いていなかったはずです！
で、COGNITO IDとラベルづけられたものにも16進数文字列が書かれている(見づらい！！！)のですが、これはアイデンティティIDのようです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/261826/442ddd75-690c-9556-47f1-8df11735e676.png)

ここでちょっとフェデレーテッドアイデンティティについて補足すると、フェデレーテッドアイデンティティは、AWS Cognitoの一機能であり、たくさんのID(たとえば複数のユーザープールIDや外部のSNSユーザアカウントのユーザIDなど)をまとめ、それぞれごとにユニークなIDを払い出してくれるものです。認証の機能自体は持っておらず、オリジナルのIDを払い出した側(今回はユーザープール)がその機能の役割を担います。

この２つのCLIコマンドにより、ユーザープールのユーザに対して、ポリシーTestIotPolicyに割り当てた操作が実行できるようになりました。

関係性は以下の通りです。

```
AWS CognitoのユーザープールでのユーザーID
｜
AWS CognitoのフェデレーテッドアイデンティティでのアイデンティティID
｜
AWS IoTのポリシ「TestIotPolicy」
｜
AWS IoTのポリシに設定したアクションとリソースARN
```

ちなみに、CLIのコマンド「aws iot attach-policy」は、現時点では管理Webコンソールからは実施できないようです。IoTのThings(モノ)に対してはWebから設定可能なのですが、AWS CognitoのフェデレーテッドアイデンティティのアタッチはCLIで実施するしかなさそうです。

最後に、もう一つ準備しておくべきことがあります。
このままでは、ユーザプールのIDトークンではAWS IoTの機能を呼び出すことができません。AWS IoT側は受け入れ準備ができたのですが、呼び出す側(フェデレーテッドアイデンティティ側)が呼び出す権限を持っていません。

その権限はどこで設定するかというと、作成したフェデレーテッドアイデンティティに割り当てたロールにその権限を設定します。
覚えていますでしょうか？IAMのロール「Cognito_TestIdPoolAuth_Role」です。（Cognito_TestIdPoolUnauth_Roleの方ではないです！）

このロールに、IoTの呼び出し権限を与えます。
とりあえず手っ取り早く「AWSIoTFullAccess」を割り当てておきます。正しくは、AWS IoTで設定したポリシ「TestIotPolicy」と同じ内容をIAMでポリシを作成してそれを割り当てた方がよいかもしれせん。

これでようやく準備ができました。


# ログインユーザでMQTTを呼び出してみる

以下の順番で行います。

 1. ユーザープールにログインしてIDトークンを取得する。
 2. IDトークンからアイデンティティIDを取得する。
 3. IDトークンからIoT呼び出しのためのセッショントークンを取得する。
 4. AWS IoT SDK for Javascriptを使ってSubscribe/Publishする。

さきほど、ユーザープールにログインしてIDトークンを取得しましたが、期限がきれているかもしれないので、もう一度実行して取得しておきます。これでログインユーザを証明してくれる証ができました。
アイデンティティIDももう一度取得してみてください。IDトークンが異なっても、前回と同じIDが取得できたと思います。

それでは、IDトークンからIoT呼び出しのためのセッショントークンを取得しましょう。
またAWS CLIを使います。（今回は随所でAWS CLIを使っていますが、実際に運用されるときにはLambda等で実行することになるかと思います。今回は手っ取り早く試してみたかったのでCLIを採用しました）

フェデレーテッドアイデンティティはユニークなIDを生成する機能に加えて、(バックグラウンドでAWS STSと連携して)AWSの機能を呼び出すためのセッショントークンを生成する機能も担っています。

> aws cognito-identity get-credentials-for-identity --identity-id 【アイデンティティID】--logins 【プールID】=【IDトークン】

以下が得られます。

```
{
    "Credentials": {
        "SecretKey": 【シークレットキー】,
        "SessionToken": 【セッショントークン】,
        "Expiration": 【有効期限】
        "AccessKeyId": 【アクセスキーID】
    },
    "IdentityId": 【アイデンティティID】
}
```

それでは実際にJavascriptからPublishしてみましょう。
start_iot.jsに少し手を入れます。
MqttAccessKeyId、MqttSecretKeyに加えて、MqttSessionTokenを新たに追加しました。
さきほどのget-credentials-for-identityで取得したものに置き換えます。

```js
const MqttTopicSubscribe = 'test_sub';
const MqttHost = 'XXXXXXXXXX.iot.ap-northeast-1.amazonaws.com';

const MqttAccessKeyId = 'XXXXXXXXXXXX';  //★ここを書き換えます。
const MqttSecretKey = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXX';  //★ここを書き換えます。
const MqttSessionToken = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXX';  //★ここを追加します。

var awsIot = require('aws-iot-device-sdk');
var deviceIot;

function subscribe_call(){
	try{
		var parent = this;

		deviceIot = awsIot.device({
			region: 'ap-northeast-1',
			clientId: 'clientid_1234',
			accessKeyId: MqttAccessKeyId,
			secretKey: MqttSecretKey,
			sessionToken : MqttSessionToken,  //★ここを追加します。
			protocol: 'wss',
			port: 443,
			host: MqttHost
		});

		deviceIot.on('message', function(topic, payload) {
			console.log('deviceIot.on(message)');
			console.log(payload.toString());

			var elem = document.getElementById("message");
			elem.innerText = payload.toString();
		});

		deviceIot.subscribe(MqttTopicSubscribe, undefined, function (err, granted){
			if( err ){
				console.log('iot_subscribe error: ' + err);
				return;
			}
			console.log(granted);
		});
	}catch(error){
		console.log('iot_subscribe error: ' + error);
	}
}
```

(注意点)
同じClientIDで同時にブローカに接続できるのは1つだけです。もし接続したらもともとの接続が切断されてしまいます。

同じようにPublish実行できましたでしょうか？かなりややこしかったのではないでしょうか。
IAMで作成したユーザ「iot」を使うとすぐにMQTTにPublishできたのですが、ログインユーザからPublishさせようとするとこれだけいろいろな準備をする必要がありました。ただ準備してしまえばいろいろ応用ができそうです。

(2018/12/22 追記)
この続きとして、AWS Cliを使わずにNode.jsとJavascriptで実現することも可能です。
[AWS CognitoとAWS IoTを連携させてみる(パート2)](https://qiita.com/poruruba/items/1277f322778c1ac03410)

以上です。