LoginSignup
3
1

More than 1 year has passed since last update.

AWS IoT Coreにブラウザから様々な認証方法で接続してみる

Last updated at Posted at 2022-06-18

AWS IoT Coreとして、MQTT ブローカが実装されており、自身でブローカを立てる必要がないですし、ブラウザ向けのクライアントライブラリもあるので、楽です。

AWS IoT Coreの認証方法にはいくつかの種類がありますので、それらを実際に試してみます。状況に合わせて使い分けてみてください。

ちなみに、今回扱うのは認証方法であって、MQTT/WebSocket/Httpといったプロトコルの種類ではないです。プロトコルは、AWS IoT Core側は特に違いは意識する必要はないですし、クライアントライブラリも違いを最低限にしてくれています。

以下の認証方法を扱います。

① (まずはNode.jsから)AWS IoT証明書を使って認証する。
② IAM認証
③ IAM+AssumeRole認証
④ Cognito認証
⑤ カスタムオーソライザ

<やること>
・AWS IoT Coreに認証して接続後、トピック名「test_sub」に対して、Subscribeします。
・いずれかのクライアントから、トピック名「test_sub」に対してPublishします。

ソースコードもろもろはgithubにあげてあります。

①(まずはNode.jsから)AWS IoT証明書を使って認証する。

まずは、AWS IoT Coreの環境設定を行いながら、一番簡単なX.509証明書を使って認証します。
X.509証明書を使っての認証は、ブラウザではあまりやらないと思いますし、ブラウザで、X.509証明書のファイルを扱うのは手間なので、Node.jsで実行します。

まず先にポリシを作成します。IAMにもポリシがありますが、ここで作成するのはAWS IoT Coreのポリシです。間違わないでください。

image.png

ポリシ名は適当に「iot_limitedaccess_policy」とでもしておきます。

ポリシードキュメントには、以下を追加しました。

許可 iot:Connect arn:aws:iot:ap-northest-1:[AWSアカウントID]:client/*
許可 iot:Subscribe arn:aws:iot:ap-northest-1:[AWSアカウントID]:topicfilter/test_sub
許可 iot:Receive arn:aws:iot:ap-northest-1:[AWSアカウントID]:topic/test_sub
許可 iot:Publish arn:aws:iot:ap-northest-1:[AWSアカウントID]:topic/test_sub

test_subがまさしく今回Subscribe/Publishしたいトピック名です。
要は、このルールが適用されれば、任意のクライアントIDで接続でき、トピック「test_sub」にSubscribeやPublishができるということです。

次に、AWS IoT Coreの管理Webコンソールに入って、モノを作成しましょう。

image.png

モノの名前は、例えば適当に「test_thing」とでもしておきます。

image.png

このモノに対して、認証に使うAWS IoT証明書を作成します。「新しい証明書を自動生成(推奨)」を選択して次へボタンを押下します。

image.png

次に、ポリシの選択画面となりますが、先ほど作成した、「iot_limitedaccess_policy」を選択します。

image.png

これで、モノが作成されました。自動生成された証明書をダウンロードしておきます。以下の4つのファイルです。

・デバイス証明書
・パブリックキーファイル
・プライベートキーファイル
・ルートCA証明書(Amazon 信頼サービスエンドポイント RSA 2048 ビットキー: Amazon ルート CA 1)

image.png

この作業で、先ほど作成したポリシにAWS IoT証明書が関連付けられました。それにより、このAWS IoT証明書を使ってAWS IoT Coreと認証が完了すると、ポリシに書かれたことができるようになります。

また、トップに戻って「設定」のところにある「デバイスデータエンドポイント」をメモしておいてください。こんな感じのやつです。

XXXXXXXXXXXXX-ats.iot.ap-northeast-1.amazonaws.com

image.png

さあ、Node.jsで接続してみます。
適当なフォルダを作成して、

> npm init -y
> npm install aws-iot-device-sdk
> mkdir keys

作成したkeysフォルダに、先ほどダウンロードした証明書ファイルを配置します。
ちなみに、パブリックキーファイルは使わないです。

\nodejs\index.js
var awsIot = require('aws-iot-device-sdk');

const AWSIOT_ENDPOINT = 'XXXXXXXXXXXXX-ats.iot.ap-northeast-1.amazonaws.com';
const AWSIOT_CLIENT_ID = "test00";
const AWSIOT_TOPIC = "test_sub";

var deviceIot = awsIot.device({
   keyPath: './keys/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-private.pem.key',
  certPath: './keys/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-certificate.pem.crt',
    caPath: './keys/AmazonRootCA1.pem',
  clientId: AWSIOT_CLIENT_ID,
      host: AWSIOT_ENDPOINT
});

deviceIot.on('connect', function() {
    console.log('connected');
    deviceIot.subscribe(AWSIOT_TOPIC, undefined, (err, granted) =>{
      if( err ){
          console.error(err);
          return;
      }
      console.log("deviceIot subscribe ok");
  });
});

deviceIot.on('closed', function() {
  console.log('closed');
});

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

後は、以下のように実行するだけです。

> node index.js

これで、トピック「test_sub」にSubscribeして、Publish待ちになっています。
「MQTTテストクライアント」を選択すると、簡単にPublishできます。

image.png

トピック名に「test_sub」を入力し、メッセージペイロードに適当なJSONを入力して、「発行」ボタンを押下するだけです。
Node.jsプログラムの方に届いているはずです。

ブラウザ向けライブラリの生成

さきほどNode.jsの場合には、npmによりライブラリは取得できていたのですが、ブラウザ向けのJavascriptライブラリは自分で作る必要があります。

まずは、browserifyが実行できる環境である必要があります。

> npm install -g browserify

次に以下を取得します。
https://github.com/aws/aws-iot-device-sdk-js

> git clone https://github.com/aws/aws-iot-device-sdk-js.git
> cd aws-iot-device-sdk-js

ここで、実は一部不備があるようで、以下のファイルを修正します。
.\browser\package.json

変更前

package.json
{
  "name": "aws-iot-sdk-browser-bundle",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"bundle exists\" && exit 0"
  },
  "author": "",
  "license": "Apache-2.0",
  "dependencies": {
    "aws-iot-device-sdk": "^1.0.11", ★この部分を書き換えます。
    "aws-sdk": "^2.3.0"
  }
}

変更後

package.json
{
  "name": "aws-iot-sdk-browser-bundle",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"bundle exists\" && exit 0"
  },
  "author": "",
  "license": "Apache-2.0",
  "dependencies": {
    "aws-iot-device-sdk": "^2.2.12",
    "aws-sdk": "^2.3.0"
  }
}

これを修正しないと、後述するカスタムオーソライザで接続できないんです。
後は以下を実行するだけです。

//Linuxの場合
> npm run-script browserize

//Windowsの場合
> .\scripts\windows-browserize.bat

これで、browserフォルダに、「aws-iot-sdk-browser-bundle.js」が出来上がりました。後で使います。

②IAM認証

それでは、さっそくブラウザからIAM認証で認証してみましょう。
正確に言うと、IAM認証先はAWS IoT Coreではなく、AWS本体です。
なので、ちょっとわかりにくいのですが、権限は、先ほど作ったAWS IoT Coreのポリシではなく、IAMのポリシが使われます。

IAMポリシを作成しましょう。名前は適当に「iot_limitedaccess」としました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iot:Receive",
                "iot:Subscribe",
                "iot:Connect",
                "iot:Publish"
            ],
            "Resource": [
                "arn:aws:iot:ap-northeast-1:[AWSアカウントID]:topicfilter/test_sub",
                "arn:aws:iot:ap-northeast-1:[AWSアカウントID]:client/*",
                "arn:aws:iot:ap-northeast-1:[AWSアカウントID]:topic/test_sub"
            ]
        }
    ]
}

それでは、ユーザを作ります。
名前は、「iot_limitedaccess_user」としてみました。「AWS 認証情報タイプを選択として、「アクセスキー - プログラムによるアクセス」をOnにします。

image.png

先ほど作成したポリシ「iot_limitedaccess」を選択します。

image.png

これで、アクセスキーIDとシークレットアクセスキーが生成されました。メモりましょう。

image.png

これで準備ができました。

それでは、ブラウザから実行するために、Javascriptを作っていきます。

まずは、HTMLに以下を追記します。

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

上記に示すところに、先ほどのaws-iot-sdk-browser-bundle.jsを置きます。

そして、以下の部分にIAMで払い出されたアクセスキーIDとシークレットアクセスキーを指定します。また、デバイスデータエンドポイントも指定します。

\public\js\start.js
const IAM_ACCESSKEY_ID = '[アクセスキーID]';
const IAM_SECRET_KEY = '[シークレットアクセスキー]';
const AWSIOT_ENDPOINT = 'XXXXXXXXXXXXX-ats.iot.ap-northeast-1.amazonaws.com';
const AWSIOT_TOPIC = "test_sub";
const AWSIOT_CLIENT_ID = "test01";

後は、以下のパラメータをawsIot.device()に渡せばよいです。

{
region: 'ap-northeast-1',
host: AWSIOT_ENDPOINT,
clientId: AWSIOT_CLIENT_ID,
protocol: 'wss',
accessKeyId: IAM_ACCESSKEY_ID,
secretKey: IAM_SECRET_KEY,
};

以下、抜粋です。

\public\js\start.js
            var credential = {
                protocol: 'wss',
                accessKeyId: IAM_ACCESSKEY_ID,
                secretKey: IAM_SECRET_KEY,
            };
            await this.mqtt_subscribe(AWSIOT_TOPIC, AWSIOT_CLIENT_ID, credential);

・・・

        mqtt_subscribe: async function(topic, client_id, credential){
            console.log('start mqtt_subscribe');
            try{
                var params = {
                    region: 'ap-northeast-1',
                    host: AWSIOT_ENDPOINT,
                    clientId: client_id,
                };
                params = Object.assign(params, credential);

                if( deviceIot ){
                    deviceIot.end();
                    deviceIot = null;
                    this.is_connected = false;
                    this.is_subscribed = false;
                }
                deviceIot = awsIot.device(params);

                deviceIot.on('connect', () => {
                    this.is_connected = true;
                    console.log('connected');
                    deviceIot.subscribe(topic, undefined, (err, granted) =>{
                        if( err ){
                            console.error(err);
                            return;
                        }
                        this.is_subscribed = true;
                        console.log("deviceIot subscribe ok");
                    });
                });
                
                deviceIot.on('close', () =>{
                    this.is_connected = false;
                    this.is_subscribed = false;
                    console.log('closed');
                }),

                deviceIot.on('message', (topic, payload) => {
                    console.log('message (' + topic + ')');
                    console.log(payload.toString());
                    this.logmessage += (new Date().toLocaleString( 'ja-JP', {} )) + ": (" + topic + ") " + payload.toString() + '\n';
                });
            }catch(error){
                console.log('mqtt_subscribe error: ' + error);
            }
        }

これにより、Subscribeした状態となります。Publishすると、受信できることがわかります。
関数「mqtt_subscribe」は後でも使います。

③IAM+AssumeRole認証

②IAM認証では、ユーザのクレデンシャルをそのままソースコードに貼り付けていました。
このIAMのクレデンシャルは常に有効なものです。
もちろん、ポリシによる実行権限は限定的にしているのでそれでもよいですが、できれば、時限的にしたいと思うはずです。そこで、AssumeRoleの出番です。
IAM認証のクレデンシャルをそのまま渡すのではなく、少しの時間だけ使えて、それを過ぎると以降は使えない無意味なクレデンシャルにすることができます。
AssumeRoleをするためにもIAMのクレデンシャルが必要なのですが、それがばれてしまっては意味がないので、サーバ側でAssumeRoleを実行するようにします。

AssumeRoleする対象のロールを作成します。名前は適当に「iot_limitedaccess_assumerole」としました。それに、ポリシ「iot_limitedaccess」をアタッチしてあげましょう。
作成が完了したら、このポリシのARNをメモしておきます。

次は、AssumeRoleできるようになるためのIAMのポリシを作成します。名前は適当に「limited」とします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::[AWSアカウントID]:role/*"
        }
    ]
}

できれば、AssumeRoleする対象のロールは「iot_limitedaccess_assumerole」限定にするようにした方がよいですが。

次にユーザを作成します。こちらも名前は適当でよいのですが、「limited」としました。
そのユーザに、先ほどのポリシ「limited」を割り当てて、アクセスキーIDとシークレットアクセスキーを生成します。
AssumeRoleするサーバがこのユーザになります。

\api\controllers\make-credential\index.js
const HELPER_BASE = process.env.HELPER_BASE || "/opt/";
const Response = require(HELPER_BASE + 'response');

const TARGET_ASSUMEROLE_ARN = 'arn:aws:iam::[AWSアカウントID]:role/iot_limitedaccess_assumerole';

const AWS = require('aws-sdk');
AWS.config.update({
	region: 'ap-northeast-1'
});
const sts = new AWS.STS({
	apiVersion: '2011-06-15',
});

exports.handler = async (event, context, callback) => {
	if( event.path == '/make-credential-assumerole' ){
		var body = JSON.parse(event.body);

		var data = await sts.assumeRole({
			RoleArn: TARGET_ASSUMEROLE_ARN,
			RoleSessionName: body.client_id
		}).promise();

		return new Response({ credential: data.Credentials });
	}else

これにより、クライアント側からリクエストを受け付けると、誰でもAssumeRoleを呼び出して結果を返してあげています。

あとは、このNode.jsプログラムを、さきほどのユーザ「limited」で実行すればよいです。
いくつかのやり方がありますが、AWSの認証情報ファイル(~/.aws/credential)に以下を追記します。

[limited]
aws_access_key_id = [アクセスキーID]
aws_secret_access_key = [シークレットアクセスキー]

そして環境変数「AWS_PROFILE」にlimitedを指定します。

//Windowsの場合
> set AWS_PROFILE=limited
//Linuxの場合
> export AWS_PROFILE=limited

あとは、Node.jsプログラムを実行すれば、上記のユーザ「limited」で実行されます。

> git clone https://github.com/poruruba/AwsIot_Test.git
> cd AWsIot_Test
> npm install
> node app.js

クライアント側は、ほぼ同じです。
以下のパラメータをawsIot.device()に渡せばよいです。

{
region: 'ap-northeast-1',
host: AWSIOT_ENDPOINT,
clientId: AWSIOT_CLIENT_ID,
protocol: 'wss',
accessKeyId: [サーバ側から取得した値],
secretKey: [サーバ側から取得した値],
sessionToken: [サーバ側から取得した値]
};

以下抜粋です。

\public\js\start.js
const base_url = 'https://[立ち上げたNode.jsのホスト名]';

・・・

            var params = {
                client_id: AWSIOT_CLIENT_ID
            };
            var result = await do_post(base_url + "/make-credential-assumerole", params );
            console.log(result);
            var credential = {
                protocol: 'wss',
                accessKeyId: result.credential.AccessKeyId,
                secretKey: result.credential.SecretAccessKey,
                sessionToken: result.credential.SessionToken
            };
            await this.mqtt_subscribe(AWSIOT_TOPIC, AWSIOT_CLIENT_ID, credential);

④Cognito認証

ちょっとややこしいです。

詳細は、こちらをご参照ください。

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

Cognitoでユーザ認証した結果からAWS IoT Coreに認証させます。

Cognitoユーザプールが既に作成されている前提で進めます。
まずはCognitoのIDプールを作成します。IDプール名は「IoTLimitedAccess」としてみました。

image.png

このとき、指定する認証プロバイダとして、Cognitoの欄にユーザプールIDとアプリクライアントIDを指定します。後程実装するCognitoへのログイン時に利用するユーザプールIDとアプリクライアントIDと同じにする必要があります。

Cognito_IoTLimitedAccessAuth_RoleとCognito_IoTLimitedAccessUnauth_Roleという2つのロールができているかと思います。
次に、ロールCognito_IoTLimitedAccessAuth_Roleに、ポリシ「iot_limitedaccess」を追加タッチします。

image.png

それでは実装に入っていくのですが、まず実装する必要があるのは、Cognitoにユーザログインして認可コードを取得するところです。

実装の詳細は、以下をご参照ください。

AWS Cognitoの画面遷移しないサインインページを作る

認可コードを取得するところから、今回の動作に合わせます。
以下に、ブラウザのJavascriptの認可コードを受け取ってからの処理を示します。
取得した認可コードと、認可コードを取得するときに使ったリダイレクトURLを使って次の処理を行います。

\public\js\start.js
const COGNITO_REDIRECT_URL = 'https://[立ち上げたNode.jsのホスト名]/auth_dialog/index.html';

・・・

        do_token: async function(message){
            if( this.state != message.state ){
                alert('state is mismatch');
                return;
            }
            console.log(message);

            var params = {
                code: message.code,
                redirect_uri: COGNITO_REDIRECT_URL
            };
            var result = await do_post(base_url + "/make-credential-cognito", params );
            console.log(result);

            var credential = {
                protocol: 'wss',
                accessKeyId: result.credential.AccessKeyId,
                secretKey: result.credential.SecretKey,
                sessionToken: result.credential.SessionToken
            };
            console.log(credential);

            await this.mqtt_subscribe(AWSIOT_TOPIC, AWSIOT_CLIENT_ID, credential);            
        },

以下が、次の処理です。

以下の部分を書き換えていただく必要があります。
・COGNITO_FEDERATED_ID
・COGNITO_URL
・COGNITO_CLIENT_ID
・COGNITO_CLIENT_SECRET
・COGNITO_USERPOOL_ID
・AWSIOT_POLICY_NAME

COGNITO_USERPOOL_ID とCOGNITO_CLIENT_IDは、IDブールの作成時に指定した認証プロバイダの情報と同じにします。

\api\controllers\make-credential\index.js
const COGNITO_FEDERATED_ID = '[IDプールのID]';  //ap-northeast-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX';
const COGNITO_URL = '[Cognitoドメイン]'; // https://XXXXXX.auth.ap-northeast-1.amazoncognito.com';
const COGNITO_CLIENT_ID = '[CognitoクライアントID]';
const COGNITO_CLIENT_SECRET = '[クライアントのシークレット]';
const COGNITO_USERPOOL_ID = '[ユーザプールID]'; //ap-northeast-1_XXXXXXXXX';
const AWSIOT_POLICY_NAME = "iot_limitedaccess_policy";

exports.handler = async (event, context, callback) => {

	if( event.path == '/make-credential-cognito' ){
		var body = JSON.parse(event.body);
		var params = {
			grant_type: 'authorization_code',
			client_id: COGNITO_CLIENT_ID,
			redirect_uri: body.redirect_uri,
			code: body.code
		};
		console.log(params);
		var result = await do_post_basic(COGNITO_URL + '/oauth2/token', params, COGNITO_CLIENT_ID, COGNITO_CLIENT_SECRET);
		console.log(result);
		var id_token = result.id_token;

		var params = {
			IdentityPoolId: COGNITO_FEDERATED_ID,
			Logins: {
				["cognito-idp.ap-northeast-1.amazonaws.com/" + COGNITO_USERPOOL_ID]: id_token
			}
		};
		var result = await cognitoidentity.getId(params).promise();
		console.log(result);

		var identityId = result.IdentityId;

		var params = {
			target: identityId
		};
		var result = await iot.listAttachedPolicies(params).promise();
		console.log(result);
		
		var item = result.policies.find(item => item.policyName == AWSIOT_POLICY_NAME);
		if( !item ){
			console.log("policy not found");
			var params = {
				policyName: AWSIOT_POLICY_NAME,
				target: identityId,
			};
			await iot.attachPolicy(params).promise();
		}

		var params = {
			IdentityId: identityId,
			Logins: {
				["cognito-idp.ap-northeast-1.amazonaws.com/" + COGNITO_USERPOOL_ID]: id_token
			}
		};
		var result = await cognitoidentity.getCredentialsForIdentity(params).promise();
		console.log(result);
		return new Response({ credential: result.Credentials });
	}else{

上記の中で、AWS IoT CoreのAttachPolicyやListAttachedPoliciesを使っていますので、IAMポリシの「limited」に以下のように追加しておきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iot:AttachPolicy",
                "iot:ListAttachedPolicies"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::626605254622:role/*"
        }
    ]
}

簡単に処理の内容を説明すると
・認可コードからIDトークンを取得します。
・IDトークンからIDプールにおけるアイデンティティIDを取得します。
・アイデンティティIDにアタッチされているAWS IoTのポリシのリストを取得し、まだポリシがアタッチされていない場合はアタッチします。
・IDトークンから一時的な認証情報を取得します。

少し補足すると、IDプールのロールCognito_IoTLimitedAccessAuth_Roleに、IAMのポリシ「iot_limitedaccess」を割り当て、また似たようなAWS IoTのポリシ「iot_limitedaccess_policy」をアイデンティティIDに割り当てています。これは、AWS IoT CoreがCognitoのユーザを認証しているのではなく、それに割り当てられたアイデンティティIDを認識しているためです。
IAMのポリシ「iot_limitedaccess」はAWS IoT CoreのAPIを呼び出すための権利設定であり、AWS IoTのポリシ「iot_limitedaccess_policy」は呼び出しを受け付けて実施ための権利設定と思ってください。

上記のレスポンスとして、一時的な認証情報であるアクセスキーID、シークレットアクセスキー、セッショントークンが返ります。
以降は、IAM+AssumeRole認証の時と同じです。

⑤カスタムオーソライザ

Cognitoにもカスタムオーソライザがあるように、AWS IoT Coreにもカスタムオーソライザがあります。
カスタムオーソライザにより、認証処理を自由に実装することができるようになります。
今回は、非常に単純に「actionToken=allow」というパラメータが渡ってきたら、認証OKとするようにします。

まずは、カスタムオーソライザの処理の中身であるLambdaを作成します。
とりあえず、特に特別な権限は不要です。名前は、「test-iot-custom-authorizer」としてみました。
処理の前半が入力パラメータのチェックで、後半で許可する範囲をポリシの形式で返しています。

\lambda\test-iot-custom-authorizer.js
const HTTP_PARAM_NAME = 'actionToken';
const ALLOW_ACTION = 'Allow';
const DENY_ACTION = 'Deny';

exports.handler = function(event, context, callback) {
    console.log("test-iot-custom-authorizer called");
    console.log(event);
    
    var protocolData = event.protocolData;
    var ACCOUNT_ID = context.invokedFunctionArn.split(":")[4];
    if (!protocolData) {
        console.log('Using the test-invoke-authorizer cli for testing only');
        callback(null, generateAuthResponse(DENY_ACTION, ACCOUNT_ID));
    } else{
        var queryString = event.protocolData.http.queryString;
        console.log('queryString values -- ' + queryString);
        const params = new URLSearchParams(queryString);
        var action = params.get(HTTP_PARAM_NAME);
        if(action && action.toLowerCase() === 'allow'){
            callback(null, generateAuthResponse(ALLOW_ACTION, ACCOUNT_ID));
        }else{
            callback(null, generateAuthResponse(DENY_ACTION, ACCOUNT_ID));
        }
    }
};

var generateAuthResponse = function(effect, ACCOUNT_ID) {
    var statement = {};
    statement.Action = [
        "iot:Receive",
        "iot:Subscribe",
        "iot:Connect",
        "iot:Publish"
    ];
    statement.Effect = effect;
    statement.Resource = [
        "arn:aws:iot:ap-northeast-1:" + ACCOUNT_ID + ":topicfilter/test_sub",
        "arn:aws:iot:ap-northeast-1:" + ACCOUNT_ID + ":client/*",
        "arn:aws:iot:ap-northeast-1:" + ACCOUNT_ID + ":topic/test_sub"
    ];

    var policyDocument = {};
    policyDocument.Version = '2012-10-17';
    policyDocument.Statement = [];
    policyDocument.Statement[0] = statement;

    var authResponse = {};
    authResponse.isAuthenticated = true;
    authResponse.principalId = 'principalId';
    authResponse.policyDocuments = [policyDocument];
    authResponse.disconnectAfterInSeconds = 3600;
    authResponse.refreshAfterInSeconds = 600;

    console.log('authResponse --> ' + JSON.stringify(authResponse));

    return authResponse;
}

次は、AWS IoT Coreにおいて、セキュリティのカスタムオーソライザを作成します。
名前は適当に「test-custom-authorizer」としました。Lambdaには、先ほど作成した「test-iot-custom-authorizer」を選択します。
オーソライザのステータスは、すぐに使いたいので「アクティブ」にします。
トークン検証のところは特にいじらずOffのままでよいです。

image.png

出来上がると以下の画面が表示されて、「オーソライザARN」が割当たります。それをメモります。(オーソライザー Lambda 関数の ARNではないです)

image.png

次に、Lambdaに戻って、アクセス権のところにある、ポリシーステートメントの「アクセス権を追加」ボタンを押下します。

image.png

AWSのサービスを選択し、サービスに「AWS IoT」、ステートメントIDとして任意の値(例えば、Id-124)、プリンシパルは自動で入力され(iot.amazonaws.com)、ソースARNに先ほどのAWS IoT CoreのオーソライザのオーソライザARNを指定し、アクションには「lambda:InvokeFunction」を選択します。保存ボタンを押下して完了です。

image.png

また、デバッグしやすいように、トップに戻って「設定」から「ログの管理」ボタンを押下し、有効にしておくと有用です。

image.png

ブラウザのJavascriptからは以下のように実行します。

\public\js\start.js
const AWSIOT_CUSTOM_AUTHORIZER = 'test-custom-authorizer';
const HTTP_PARAM_NAME = 'actionToken';

・・・

        start_custom: async function(){
            var credential = {
                protocol: 'wss-custom-auth',
                customAuthHeaders: {
                    'X-Amz-CustomAuthorizer-Name': AWSIOT_CUSTOM_AUTHORIZER,
                },
                customAuthQueryString: '?x-amz-customauthorizer-name=' + AWSIOT_CUSTOM_AUTHORIZER + '&' + HTTP_PARAM_NAME + '=allow',
            };
            await this.mqtt_subscribe(AWSIOT_TOPIC, AWSIOT_CLIENT_ID, credential);
        },

protocolがちょっと特殊で「wss-custom-auth」となっているところが大事です。ライブラリ「aws-iot-sdk-browser-bundle.js」の中でうまく処理してくれているんです。
HTTP_PARAM_NAME + '=allow'のところで渡しているパラメータをカスタムオーソライザが解釈しています。

ソースコード

まとめると以下になります。

クライアント側(Javascript)

\public\js\start.js
'use strict';

//const vConsole = new VConsole();
//window.datgui = new dat.GUI();

// for cognito
const COGNITO_REDIRECT_URL = 'https://[立ち上げたNode.jsのホスト名]/auth_dialog/index.html';
const COGNITO_CLIENT_ID = '[CognitoクライアントID]';

// for awsiot
const AWSIOT_ENDPOINT = '[AWS IoTデバイスデータエンドポイント]'; // XXXXXXXXX-ats.iot.ap-northeast-1.amazonaws.com';
const AWSIOT_CLIENT_ID = "test01";
const AWSIOT_TOPIC = "test_sub";

// for awsiot custom authorozer
const AWSIOT_CUSTOM_AUTHORIZER = 'test-custom-authorizer';
const HTTP_PARAM_NAME = 'actionToken';

// for awsiot iam
const IAM_ACCESSKEY_ID = '[アクセスキーID]';
const IAM_SECRET_KEY = '[シークレットアクセスキー]';

const base_url = 'https://[立ち上げたNode.jsのホスト名]';

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

var vue_options = {
    el: "#top",
    mixins: [mixins_bootstrap],
    data: {
        is_connected: false,
        is_subscribed: false,
        state: "abcd",
        message: "hello world",
        logmessage: '',
        client_id: AWSIOT_CLIENT_ID,
    },
    computed: {
    },
    methods: {
        do_publish: async function(){
            if( !deviceIot ){
                alert('is not connected');
                return;
            }
            deviceIot.publish(AWSIOT_TOPIC, JSON.stringify({ message: this.message } ));
        },

        do_publish_custom_http: async function(){
            var params = {
                message : this.message
            };
            var result = await do_post(base_url + '/publish-custom-http', params );
            console.log(result);
        },

        start_iam: async function(){
            var credential = {
                protocol: 'wss',
                accessKeyId: IAM_ACCESSKEY_ID,
                secretKey: IAM_SECRET_KEY,
            };
            await this.mqtt_subscribe(AWSIOT_TOPIC, AWSIOT_CLIENT_ID, credential);
        },

        start_assumerole: async function(){
            var params = {
                client_id: AWSIOT_CLIENT_ID
            };
            var result = await do_post(base_url + "/make-credential-assumerole", params );
            console.log(result);
            var credential = {
                protocol: 'wss',
                accessKeyId: result.credential.AccessKeyId,
                secretKey: result.credential.SecretAccessKey,
                sessionToken: result.credential.SessionToken
            };
            await this.mqtt_subscribe(AWSIOT_TOPIC, AWSIOT_CLIENT_ID, credential);
        },

        start_custom: async function(){
            var credential = {
                protocol: 'wss-custom-auth',
                customAuthHeaders: {
                    'X-Amz-CustomAuthorizer-Name': AWSIOT_CUSTOM_AUTHORIZER,
                },
                customAuthQueryString: '?x-amz-customauthorizer-name=' + AWSIOT_CUSTOM_AUTHORIZER + '&' + HTTP_PARAM_NAME + '=allow',
            };
            await this.mqtt_subscribe(AWSIOT_TOPIC, AWSIOT_CLIENT_ID, credential);
        },

        start_login: function(){
            var params = {
                state: this.state,
                client_id: COGNITO_CLIENT_ID,
                scope: 'openid profile'
            };
            new_win = open(COGNITO_REDIRECT_URL + to_urlparam(params), null, 'width=500,height=750');
        },

        do_token: async function(message){
            if( this.state != message.state ){
                alert('state is mismatch');
                return;
            }
            console.log(message);

            var params = {
                code: message.code,
                redirect_uri: COGNITO_REDIRECT_URL
            };
            var result = await do_post(base_url + "/make-credential-cognito", params );
            console.log(result);

            var credential = {
                protocol: 'wss',
                accessKeyId: result.credential.AccessKeyId,
                secretKey: result.credential.SecretKey,
                sessionToken: result.credential.SessionToken
            };
            console.log(credential);

            await this.mqtt_subscribe(AWSIOT_TOPIC, AWSIOT_CLIENT_ID, credential);            
        },

        mqtt_subscribe: async function(topic, client_id, credential){
            console.log('start mqtt_subscribe');
            try{
                var params = {
                    region: 'ap-northeast-1',
                    host: AWSIOT_ENDPOINT,
                    clientId: client_id,
                };
                params = Object.assign(params, credential);

                if( deviceIot ){
                    deviceIot.end();
                    deviceIot = null;
                    this.is_connected = false;
                    this.is_subscribed = false;
                }
                deviceIot = awsIot.device(params);

                deviceIot.on('connect', () => {
                    this.is_connected = true;
                    console.log('connected');
                    deviceIot.subscribe(topic, undefined, (err, granted) =>{
                        if( err ){
                            console.error(err);
                            return;
                        }
                        this.is_subscribed = true;
                        console.log("deviceIot subscribe ok");
                    });
                });
                
                deviceIot.on('close', () =>{
                    this.is_connected = false;
                    this.is_subscribed = false;
                    console.log('closed');
                }),

                deviceIot.on('message', (topic, payload) => {
                    console.log('message (' + topic + ')');
                    console.log(payload.toString());
                    this.logmessage += (new Date().toLocaleString( 'ja-JP', {} )) + ": (" + topic + ") " + payload.toString() + '\n';
                });
            }catch(error){
                console.log('mqtt_subscribe error: ' + error);
            }
        }
    },
    created: function(){
    },
    mounted: function(){
        proc_load();
    }
};
vue_add_data(vue_options, { progress_title: '' }); // for progress-dialog
vue_add_global_components(components_bootstrap);
vue_add_global_components(components_utils);

/* add additional components */
  
window.vue = new Vue( vue_options );

function to_urlparam(qs){
    var params = new URLSearchParams(qs);
    // for( var key in qs )
    //     params.set(key, qs[key] );
    var param = params.toString();

    if( param == '' )
        return '';
    else
        return '?' + param;
}

サーバ側(Node.js)

\api\controllers\make-credential\index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || "/opt/";
const Response = require(HELPER_BASE + 'response');
const Redirect = require(HELPER_BASE + 'redirect');

const COGNITO_FEDERATED_ID = '[Cognito IDプール]'; //ap-northeast-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX';
const COGNITO_USERPOOL_ID = '[CognitoユーザプールID]'; //ap-northeast-1_XXXXXXXXX';
const COGNITO_URL = '[Cognitoドメイン名]'; //https://XXXXX.auth.ap-northeast-1.amazoncognito.com';
const COGNITO_CLIENT_ID = '[クライアントID]';
const COGNITO_CLIENT_SECRET = '[クライアントシークレット]';
const TARGET_ASSUMEROLE_ARN = 'arn:aws:iam::[AWSアカウントID]:role/iot_limitedaccess_assumerole';
const AWSIOT_POLICY_NAME = "iot_limitedaccess_policy";
const AWSIOT_CUSTOM_AUTHORIZER = 'test-custom-authorizer';
const AWSIOT_ENDPOINT = '[AWS IoTデバイスデータエンドポイント]'; //XXXXXXXXX-ats.iot.ap-northeast-1.amazonaws.com';
const AWSIOT_TOPIC = "test_sub";
const HTTP_PARAM_NAME = 'actionToken';

const { URL, URLSearchParams } = require('url');
const fetch = require('node-fetch');
const Headers = fetch.Headers;

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

const sts = new AWS.STS({
	apiVersion: '2011-06-15',
});

const iot = new AWS.Iot({
	apiVersion: '2015-05-28'
});

const cognitoidentity = new AWS.CognitoIdentity({
	apiVersion: '2014-06-30'
});

exports.handler = async (event, context, callback) => {
	if( event.path == '/publish-custom-http'){
		var body = JSON.parse(event.body);
		var headers = {
			'X-Amz-CustomAuthorizer-Name' : AWSIOT_CUSTOM_AUTHORIZER
		};
		await do_post_with_headers("https://" + AWSIOT_ENDPOINT + '/topics/' + AWSIOT_TOPIC + '?' + HTTP_PARAM_NAME + '=allow', body, headers);
		return new Response({});
	}else
	if( event.path == '/make-credential-assumerole' ){
		var body = JSON.parse(event.body);

		var data = await sts.assumeRole({
			RoleArn: TARGET_ASSUMEROLE_ARN,
			RoleSessionName: body.client_id
		}).promise();

		return new Response({ credential: data.Credentials });
	}else
	if( event.path == '/make-credential-cognito' ){
		var body = JSON.parse(event.body);
		var params = {
			grant_type: 'authorization_code',
			client_id: COGNITO_CLIENT_ID,
			redirect_uri: body.redirect_uri,
			code: body.code
		};
		console.log(params);
		var result = await do_post_basic(COGNITO_URL + '/oauth2/token', params, COGNITO_CLIENT_ID, COGNITO_CLIENT_SECRET);
		console.log(result);
		var id_token = result.id_token;

		var params = {
			IdentityPoolId: COGNITO_FEDERATED_ID,
			Logins: {
				["cognito-idp.ap-northeast-1.amazonaws.com/" + COGNITO_USERPOOL_ID]: id_token
			}
		};
		var result = await cognitoidentity.getId(params).promise();
		console.log(result);

		var identityId = result.IdentityId;

		var params = {
			target: identityId
		};
		var result = await iot.listAttachedPolicies(params).promise();
		console.log(result);
		
		var item = result.policies.find(item => item.policyName == AWSIOT_POLICY_NAME);
		if( !item ){
			console.log("policy not found");
			var params = {
				policyName: AWSIOT_POLICY_NAME,
				target: identityId,
			};
			await iot.attachPolicy(params).promise();
		}

		var params = {
			IdentityId: identityId,
			Logins: {
				["cognito-idp.ap-northeast-1.amazonaws.com/" + COGNITO_USERPOOL_ID]: id_token
			}
		};
		var result = await cognitoidentity.getCredentialsForIdentity(params).promise();
		console.log(result);
		return new Response({ credential: result.Credentials });
	}else{
		var body = JSON.parse(event.body);
		console.log(body);
		return new Response({ message: 'Hello World' });
	}
};

async function do_post_basic(url, params, client_id, client_secret){
	var data = new URLSearchParams(params).toString();
	var basic = 'Basic ' + Buffer.from(client_id + ':' + client_secret).toString('base64');
	const headers = new Headers( { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization' : basic } );

	return fetch(url, {
			method : 'POST',
			body : data,
			headers: headers
	})
	.then((response) => {
			if( !response.ok )
					throw 'status is not 200';
			return response.json();
	})
}

function do_post_with_headers(url, body, hds) {
	var headers = new Headers(hds);
	headers.append("Content-Type", "application/json");

	return fetch(url, {
		method: 'POST',
		body: JSON.stringify(body),
		headers: headers
	})
	.then((response) => {
		if (!response.ok)
			throw 'status is not 200';
		return response.json();
//    return response.text();
//    return response.blob();
//    return response.arrayBuffer();
	});
}
\api\controllers\make-credential\swagger.yaml
paths:
  /publish-custom-http:
    post:
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: object

  /make-credential-cognito:
    post:
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: object

  /make-credential-assumerole:
    post:
      parameters:
        - in: body
          name: body
          schema:
            type: object
      responses:
        200:
          description: Success
          schema:
            type: object

(おまけ)MQTT Publishを受けてLambdaを起動する

適当なLambdaを作成します。例えば名前を「test_iot_lambda」とします。

\lambda\test_iot_lambda.js
exports.handler = async (event, context, callback) => {
    console.log('handler called');
    console.log(JSON.stringify(event));
    console.log(context);
    console.log(callback);
    
        const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

次に、AWS IoT Coreにおいて、メッセージのルーティングからルールを選択し、「ルールを作成」ボタンを押下します。

例えば、ルール名を「iot_limitedaccess_rule」とします。

image.png

SQLステートメントを以下のようにします。

SELECT { 'payload': *, 'topic': topic(), 'client_id': clientid() } FROM 'test_sub'

image.png

ルールアクションとしてLambdaを選択し、先ほど作成した「test_iot_lambda」を選択します。

image.png

作成ボタン押下で完了です。
トピック「test_sub」にPublishするたびに、このLambdaが呼ばれます。CloudWatchログを見れば確認できます。

Lambdaが呼び出されたときの引数eventは以下のようになっています。

{ "payload": { "message": "hello world" }, "topic": "test_sub", "client_id": "test01" }

Client_idの指定がない環境でPublishした場合は以下が渡ってきます。

{ "payload": { "message": "hello world" }, "topic": "test_sub", "client_id": "N/A" }

ご参考

Qiita

 AWS CognitoとAWS IoTを連携させてみる
 AWS CognitoとAWS IoTを連携させてみる(パート2)
 Google Smart HomeデバイスをAWS IoTで実装してみた
 AWS IoTにMosquittoをブリッジにしてつなぐ
 AWS Cognitoの画面遷移しないサインインページを作る

AWS

 https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/what-is-aws-iot.html
 https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Iot.html
 https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/IotData.html
 https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CognitoIdentity.html
 https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/STS.html

以上

3
1
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
3
1