9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-12-21

以前、以下の投稿で、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を開き、左側のナビゲータから、「安全性」→ 「ポリシ」を選択し、右上の「作成」ボタンを押下します。

image.png

作成は、アドバンスモードでもベーシックモードでもどちらでもよいです。
ポリシの名前は、例えば「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】:同上

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

image.png

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

image.png

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

image.png

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

image.png

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

image.png

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

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

image.png

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

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

image.png

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

image.png

image.png

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

image.png

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

image.png

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

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

以上です。

9
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?