以前、以下の投稿で、AWS Cognitoで認証したユーザからAWS IoTのMQTTにPublishできるようにしました。
その時は、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が連携できていることを確認してください。
目指す形
以下の状態となることを目指します。
- ログインユーザごとにトピック名を割り当てます。これにより、どのユーザからの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としています。
{
"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定義ファイルの該当部分を示します。
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を想定しています。
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 }));
});
});
});
});
};
毎度のユーティリティです。
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するボタンを用意します。
<!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部分です。
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を使いこなすうえでの一助になればと思います。
以上です。