AWS CognitoとAWS IoTを連携させてみる(パート2)


以前、以下の投稿で、AWS Cognitoで認証したユーザからAWS IoTのMQTTにPublishできるようにしました。

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

その時は、AWS Cliを使っていたため、自動化できていませんでした。
今回は、少し発展させて、ユーザがAWS Cognitoで認証してMQTTでPublishするまでを行うWebページを作成してみたいと思います。

AWS CognitoのユーザプールとフェデレーテッドアイデンティティのIDプールの作成

今回は、Node.jsによるサーバと、JavascriptによるWebページを作成することがメインです。
AWS Cognitoによるユーザ認証のためのユーザプールの作成と、AWS IoTと連携するためのつなぎとなるフェデレーテッドアイデンティティのIDプールは、作成済みである前提です。
以下の投稿を参考にして、まずはAWS Cliによる手動で、AWS CognitoとAWS IoTが連携できていることを確認してください。

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

目指す形

以下の状態となることを目指します。

  • ログインユーザごとにトピック名を割り当てます。これにより、どのユーザからのPublishなのかを区別できるようにします。
  • ログインユーザしか割り当てられたトピックにPublishできないようにするために、ログインユーザにThingを割り当て、ポリシでトピック名とThingを紐づけます。

以上を実現するために、以下の状態を作ります。

  • AWS Cognitoでのログインユーザの識別は、フェデレーテッドアイデンティティのIdentityIdです。
  • そして、ログインユーザに割り当てるThingのThing名を単純にIdentityIdにします。
  • ログインユーザごとに割り当てるトピック名は、/iot/IdentityId にします。ログインユーザ全員にPublishできるように、/iot/allというトピックも考えておきます。

AWS IoTポリシの作成

先に、AWS IoTに設定するポリシを示しておきます。
XXXXXXXXXXXXの部分は、AWSアカウントIDです。リージョンは、ap-northeast-1としています。

TestIotPolicy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect"
      ],
      "Resource": [
        "*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Publish"
      ],
      "Resource": [
        "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:topic//iot/${iot:Connection.Thing.ThingName}"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Subscribe"
      ],
      "Resource": [
        "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:topicfilter//iot/all"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "iot:Receive"
      ],
      "Resource": [
        "arn:aws:iot:ap-northeast-1:XXXXXXXXXXXX:topic//iot/all"
      ]
    }
  ]
}

4つの指定項目があります。上から順番に説明します。

① MQTTブローカに接続するための指定です。
② 自分の名前のトピックにPublishできるようにするための指定です。
③④ 全ユーザ共通のトピックにSubscribeできるようにするための指定です。

(参考)
https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/thing-policy-variables.html

このポリシを登録します。
AWS管理WebコンソールからAWS Iotを開き、左側のナビゲータから、「安全性」→ 「ポリシ」を選択し、右上の「作成」ボタンを押下します。

作成は、アドバンスモードでもベーシックモードでもどちらでもよいです。
ポリシの名前は、例えば「TestIotPolicy」とします。

フェデレーテッドアイデンティティで作成した「認証されたロール」にも同じ内容でIAMにポリシ作成して割り当てます。(とりあえず、「AWSIoTFullAccess」でもいいですが。。。)

AWS IoTにユーザ登録するためのサーバの作成

AWS IoTへのユーザ登録要求を受け付けるサーバを立ち上げます。
毎度のRESTfulサーバです。以下を参考にしてください。

SwaggerでRESTful環境を構築する

Swagger定義ファイルの該当部分を示します。

swagger.yaml
paths:
  /iotregister:
    post:
      x-swagger-router-controller: routing
      operationId: iotregister
      parameters:
        - in: body
          name: IotRegister
          required: true
          schema:
            $ref: '#/definitions/IotRegisterRequest'
      responses:
        200:
          description: Success
          schema:
            type: object

definitions:
  IotRegisterRequest:
    type: object
    required:
    - id_token
    properties:
      id_token:
        type: string

そして、以下がサーバ実装の本体です。Lambdaを想定しています。

index.js
const aws = require('aws-sdk');
aws.config.update({region: 'ap-northeast-1'});

var cognitoidentity = new aws.CognitoIdentity();
var iot = new aws.Iot();

const Response = require('../../helpers/response');

exports.handler = async (event, context, callback) => {
    var body = JSON.parse(event.body);

    var params = {
        IdentityPoolId: 【IDプールのID】,
        Logins: {
            'cognito-idp.ap-northeast-1.amazonaws.com/【プールID】' : body.id_token
        }
    };

    cognitoidentity.getId(params, (err, data) =>{
        if( err )
            return callback(err);

        console.log(data.IdentityId);

        var identityId = data.IdentityId;

        var params = {
            policyName: 'TestIotPolicy',
            target: identityId
        };

        iot.attachPolicy(params, (err, data) =>{
            if( err )
                return callback(err);

            console.log(data);

            var params = {
                thingName: identityId
            }

            iot.createThing(params, (err, data) =>{
                if( err )
                    return callback(err);

                console.log(data);

                var params = {
                    principal: identityId,
                    thingName: identityId
                };

                iot.attachThingPrincipal(params, (err, data) =>{
                    if( err )
                        return callback(err);

                    return callback( null, new Response({ data: data }));
                });
            });
        });
    });
};

毎度のユーティリティです。

response.js
class Response{
    constructor(context){
        this.statusCode = 200;
        this.headers = {'Access-Control-Allow-Origin' : '*'};
        if( context )
            this.set_body(context);
        else
            this.body = "";
    }

    set_error(error){
        this.body = JSON.stringify({"err": error});
    }

    set_body(content){
        this.body = JSON.stringify(content);        
    }

    get_body(){
        return JSON.parse(this.body);
    }
}

module.exports = Response;

一部、環境に合わせて変更が必要です。

【IDプールのID】:AWS CognitoのフェデレーテッドアイデンティティのIDプールのIDです。
【プールID】:AWS CognitoのユーザプールのプールIDです。

このRESTfulサーバに、ユーザがサインインしたときに取得したIDトークンを引数にして渡すと、MQTTにPublishできるように登録をしてくれます。
もう少し具体的に見ていきます。

cognitoidentity.getId
 これで、ユーザ認証したIDトークンから、フェデレーテッドアイデンティティのIdentityIdを取得します。

iot.attachPolicy
 これで、IdentityIdのユーザに、AWS IoTのポリシを割り当てます。

iot.createThing
 これで、IdentityIdという名前でIoT Thingを作成します。

iot.attachThingPrincipal
 これで、IdentityIdという名前のIoT Thingと、フェデレーテッドアイデンティティのIdentityIdが紐づきます。

Webページの作成

ユーザ向けのWebページを作成します。
このWebページでは、以下のことができるようにします。

  • ユーザがAWS Cognitoにログインできるリンクを用意します。
  • ログイン後に、さきほどのRESTful環境に対して、AWS IoTへのユーザ登録要求を行うボタンを用意します。
  • AWS IoTに接続し、トピック/iot/all をSubscribeするボタンを用意します。
  • AWS IoTに対して、トピック/iot/IdentityId でPublishするボタンを用意します。
index.html
<!DOCTYPE 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 * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
  <meta name="format-detection" content="telephone=no">
  <meta name="msapplication-tap-highlight" content="no">
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">

  <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

  <title>AWS IoT テスト</title>

  <script src="./dist/js/aws-iot-sdk-browser-bundle.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
  <script src="https://unpkg.com/vue"></script>
</head>
<body>
    <div id="top" class="container">
        <h1>AWS IoT テスト</h1>
        <label>IdentityId:</label> {{identityId}}<br>
        <label>Connected:</label> {{connected}}<br>
        <br>

        <a href="https://【ドメイン名】.auth.ap-northeast-1.amazoncognito.com/login?client_id=【アプリクライアントID】&redirect_uri=【このWebページのURL】&response_type=token&scope=openid">サインイン</a><br>
        <br>
        <button class="btn btn-primary" v-on:click="mqtt_register()">Mqtt登録</button>
        <button class="btn btn-primary" v-on:click="mqtt_connect()">Mqtt接続</button>
        <br>
        <br>
        <div class="form-group">
            <label>Message</label>
            <input type="text" class="form-control" v-model="publish_message">
            <button class="btn btn-primary" v-on:click="mqtt_publish()">MqttPublish</button>
        </div>
        <br>
        <div class="form-group">
            <label>Received Message</label>
            <div class="panel panel-default" v-for="(message, index) in message_list">
                <div class="panel-heading">
                    {{message.topic}}
                </div>
                <div class="panel-body">
                    {{message.payload}}
                </div>
            </div>
        </div>
    </div>

    <script src="js/start.js"></script>
</body>

以下の部分を書き換える必要があります。

【ドメイン名】:AWS Cognitoのユーザプールのアプリ統合で指定したドメイン名です。
【アプリクライアントID】:フェデレーテットアイデンティティの認証プロバイダのCognitoタブで指定したアプリクライアントIDに書き換えてください。
【このWebページのURL】:このHTMLファイルを配置しアクセスするときのURLにします。アプリクライアントIDのコールバックURLにこのURLが指定されている必要があります。

Javascript部分です。

start.js
var aws = require('aws-sdk');
aws.config.update({region: 'ap-northeast-1'});
var cognitoidentity = new aws.CognitoIdentity();

var iotDevice = require('aws-iot-device-sdk');
var iot = new aws.Iot();

var id_token = null;
var device;

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',
        identityId: '',
        message_list: [],
        connected: false,
        publish_message: ''
    },
    computed: {
    },
    methods: {
        mqtt_publish: function(){
            device.publish('/iot/' + this.identityId, this.publish_message);
        },
        mqtt_register: function(){
            var body = {
                'id_token' : id_token
            };
            do_post("【RESTfulサーバのURL】/iotregister", body)
            .then(response =>{
                console.log(response);
                alert('登録しました。');
            });
        },
        mqtt_connect: function(){
            var params = {
                IdentityId: this.identityId,
                Logins: {
                    'cognito-idp.ap-northeast-1.amazonaws.com/【プールID】' : id_token
                }
            };
            cognitoidentity.getCredentialsForIdentity(params, (err, data) =>{
                if( err )
                    return callback(err);

                var credential = data.Credentials;

                device = iotDevice.device({
                    region: 'ap-northeast-1',
                    clientId: this.identityId,
                    accessKeyId: credential.AccessKeyId,
                    secretKey: credential.SecretKey,
                    sessionToken: credential.SessionToken,
                    protocol: 'wss',
                    port: 443,
                    host: '【AWS IoTのエンドポイント】',
                    maximumReconnectTimeMs: 8000,
                });

                device.on('connect', () =>{
                    console.log('device connect');
                    this.connected = true;
                });
                device.on('close', () =>{
                    console.log('device close');
                    this.connected = false;
                });
                device.on('reconnect', () =>{
                    console.log('device reconnect');
                });
                device.on('offline', () =>{
                    console.log('device offline');
                });
                device.on('error', (error) =>{
                    console.log('device error');
                });
                device.on('message', (topic, payload) =>{
                    console.log('device message');
                    console.log('topic ', topic);
                    console.log('payload ', payload.toString('utf-8'));

                    this.message_list.push({ topic: topic, payload: payload.toString('utf-8')});
                });

                device.subscribe('/iot/all');
            });
        }
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
        history.replaceState(null, null, '.');

        if( hashs.id_token ){
            id_token = hashs.id_token;
            Cookies.set("id_token", id_token, { expires: 7 });
        }else{
            id_token = Cookies.get("id_token");
        }

        if( id_token ){
            var params = {
                IdentityPoolId: 【IDプールのID】,
                Logins: {
                    'cognito-idp.ap-northeast-1.amazonaws.com/【プールID】' : id_token
                }
            };

            cognitoidentity.getId( params, (err, data) =>{
                if( err ){
                    console.log(err);
                    return;
                }

                this.identityId = data.IdentityId;
            });
        }
    }
};
var vue = new Vue( vue_options );

var hashs = {};
var searchs = {};

function proc_load() {
  hashs = parse_url_vars(location.hash);
  searchs = parse_url_vars(location.search);
}

function parse_url_vars(param){
  if( param.length < 1 )
      return {};

  var hash = param;
  if( hash.slice(0, 1) == '#' || hash.slice(0, 1) == '?' )
      hash = hash.slice(1);
  var hashs  = hash.split('&');
  var vars = {};
  for( var i = 0 ; i < hashs.length ; i++ ){
      var array = hashs[i].split('=');
      vars[array[0]] = array[1];
  }

  return vars;
}

function do_post(url, body){
    const headers = new Headers( { "Content-Type" : "application/json; charset=utf-8" } );

    return fetch(url, {
        method : 'POST',
        body : JSON.stringify(body),
        headers: headers
    })
    .then((response) => {
        return response.json();
    });
}

以下を書き換えます。

【RESTfulサーバのURL】:先ほど立ち上げたRESTfulサーバのURLです。
【AWS IoTのエンドポイント】:AWS IoTのエンドポイントです。AWS IoTのWeb管理ページの設定にあります。
【IDプールのID】:同上
【プールID】:同上

それでは、さっそくブラウザで開いてみましょう。

まずは、サインインのリンクをクリックして、サインインします。
そうするとログイン画面が表示されます。(以下の画面は、AWS Cognitoの設定によって見え方は異なります。)

ログインに成功すると、IdentityIdが表示されます。

次に、ユーザをAWS IoTに登録するために、「Mqtt登録」ボタンを押下します。
特に問題がなければ、「登録しました」というダイアログが表示さます。
この作業はユーザごとに1回だけでよいです。

これで、AWS Iotに接続する準備ができました。
「Mqtt接続」ボタンを押下します。そうすると、Connectedがtrue に表示が変わります。

この状態で、トピック「/iot/all」をSubscribeしている状態になっています。

AWS IoTコンソールから、Publishしてみます。
発行のところのエディットボックスに/iot/allと入力して、「トピックに発行」ボタンを押下します。

以下の文字列がブラウザに表示されましたでしょうか?

/iot/all
{ "message": "Hello from AWS IoT console" }

今度は、ブラウザ側からPublishしてみます。
ログインユーザのIdentityIdがブラウザに表示されていますので、それをAWS IoTコンソールに指定します。
トピックのサブスクリプションのところに /iot/IdentityId という感じで指定します。

ブラウザから、messageのところに適当な文字列を入れて、「MqttPublish」ボタンを押下します。

AWS IoTコンソールにその文字列が表示されれば成功です。

AWS IoTの開発者ガイドもあるのですが、なかなか理解が難しく、手を動かしてみると、ようやくその意味が分かってきました。

AWS IoTを使いこなすうえでの一助になればと思います。

以上です。