AWS IoT Coreにカスタム認証でブラウザから接続
はじめに
ブラウザからでもMQTTブローカーに接続したかったのでAWS IoT CoreにブラウザからMQTT over WebSocketとカスタム認証で接続した時のメモ。クライアント側の例がなかなか見つからなかったので参考になれば。
カスタム認証については「カスタム認証ワークフローについて」に詳しくかいてある。
設定
全体の流れは以下の通り
- Authorizerをつくる
- Authorizerが呼び出すLambda(
index.js
)をつくる。この関数が実際の認証・認可を行う - AuthorizerとLambdaを紐つけて、クライアントからの認証リクエストがLamdaに届くようにする
- Lambda関数をAuthorizerがInvokeできるようにLambda側に権限(Resource-based policy)を設定する
- Default Authorizerを設定し、クライアントがAuthorizerの指定を省略できるようにする(オプション)
- クライアントから接続
1. Authorizerをつくる
- AWS IoT > Security > Custom authorizers > Create authorizer
- Authorizer name: Authorizer とする。
2. Authorizerが呼び出すLambdaをつくる
-
Authorizer function > Lambda function > Create a Lambda function
- Select Author from scratch
- FunctionName: AuthorizerFunction
- Create function
-
Lambda > Functions > AuthorizerFunction
- 以下のコードを
index.js
に貼り付ける。コード内のアカウント番号、リージョン、MQTTトピック、MQTTクライアント名は自分のものに置き換えて、Deploy を押す。
- 以下のコードを
// A simple Lambda function for an authorizer. It demonstrates
// how to parse an MQTT password and generate a response.
exports.handler = function(event, context, callback) {
var uname = event.protocolData.mqtt.username;
var pwd = event.protocolData.mqtt.password;
console.log("pwd: " + pwd);
var buff = new Buffer(pwd, 'base64');
var passwd = buff.toString('ascii');
console.log("passwd: " + passwd);
switch (passwd) {
case 'test':
callback(null, generateAuthResponse(passwd, 'Allow'));
default:
callback(null, generateAuthResponse(passwd, 'Deny'));
}
};
// Helper function to generate the authorization response.
var generateAuthResponse = function(token, effect) {
var authResponse = {};
authResponse.isAuthenticated = true;
authResponse.principalId = 'TEST123';
var policyDocument = {};
policyDocument.Version = '2012-10-17';
policyDocument.Statement = [];
var connectStatement = {};
var receiveStatement = {};
var subscribeStatement = {};
connectStatement.Action = ["iot:Connect"];
connectStatement.Effect = effect;
connectStatement.Resource = ["arn:aws:iot:ap-northeast-1:<account-id>:client/browser"];
subscribeStatement.Action = ["iot:Subscribe"];
subscribeStatement.Effect = effect;
subscribeStatement.Resource = ["arn:aws:iot:ap-northeast-1:<account-id>:topicfilter/*"];
receiveStatement.Action = ["iot:Receive"];
receiveStatement.Effect = effect;
receiveStatement.Resource = ["arn:aws:iot:ap-northeast-1:<account-id>:topic/*"];
policyDocument.Statement[0] = connectStatement;
policyDocument.Statement[1] = subscribeStatement;
policyDocument.Statement[2] = receiveStatement;
authResponse.policyDocuments = [policyDocument];
authResponse.disconnectAfterInSeconds = 3600;
authResponse.refreshAfterInSeconds = 300;
return authResponse;
}
公式のサンプルにはiot:Receive
がないが、私の環境ではiot:Receive
を指定しない場合、クライアントからのpublishもsubscribeもエラーとなった。このサンプルの Helper function わかりにくい... JSオブジェクトを直に書いた方がいいのでは?
3. AuthorizerとLambdaを紐つける
- AWS IoT > Security > Custom authorizers > Create authorizer に戻る。
- Authorizer function > Lambda function > Lambda function で上記で作った Function を選択してCreate。
- AWS IoT > Security > Custom authorizersでActivate。
aws iot describe-authorizer --authorizer-name Authorizer
{
"authorizerDescription": {
"authorizerName": "Authorizer",
"authorizerArn": "arn:aws:iot:ap-northeast-1:<account-id>:authorizer/Authorizer",
"authorizerFunctionArn": "arn:aws:lambda:ap-northeast-1:<account-id>:function:AuthorizerFunction",
"status": "ACTIVE",
"creationDate": "2022-09-07T10:01:25.419000+09:00",
"lastModifiedDate": "2022-09-07T11:26:39.092000+09:00",
"signingDisabled": true
}
}
最初に試しで設定するときは、signingDisabled: true
のままで、してToken validationをスキップしたほうがよい。
4. AuthorizerがLambdaをInvokeできるように権限を設定する
- Resource-based policyを設定する
aws lambda add-permission --function-name AuthorizerFunction --principal iot.amazonaws.com --source-arn arn:aws:iot:ap-northeast-1:<account-id>:authorizer/Authorizer --statement-id Id-123 --action "lambda:InvokeFunction"
{
"Statement": "{\"Sid\":\"Id-123\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"iot.amazonaws.com\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:ap-northeast-1:<account-id>:function:AuthorizerFunction\",\"Condition\":{\"ArnLike\":{\"AWS:SourceArn\":\"arn:aws:lambda:ap-northeast-1:<account-id>:function:AuthorizerFunction\"}}}"
}
ちなみにGUIだとここで設定できる。
- Lambda > Functions > AuthorizerFunction > Configuration tab > Resource-based policy
5. Default Authorizerを設定する(オプション)
設定
aws iot set-default-authorizer --authorizer-name Authorizer
{
"authorizerName": "Authorizer",
"authorizerArn": "arn:aws:iot:ap-northeast-1:<account-id>:authorizer/Authorizer"
}
(参考) Default authorizerのクリアと確認方法
aws iot clear-default-authorizer
aws iot describe-default-authorizer
テスト
Authorizerの動作を確認してみる。
aws iot test-invoke-authorizer --authorizer-name Authorizer \
--mqtt-context '{"username": "test", "password": "dGVzdA==", "clientId":"browser"}'
{
"isAuthenticated": true,
"principalId": "TEST123",
"policyDocuments": [
"{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":[\"iot:Connect\"],\"Effect\":\"Allow\",\"Resource\":[\"arn:aws:iot:us-east-1:<account-id>:client/browser\"]},{\"Action\":[\"iot:Publish\"],\"Effect\":\"Allow\",\"Resource\":[\"arn:aws:iot:us-east-1:<account-id>:topic/notification/browser\"]}]}"
],
"refreshAfterInSeconds": 300,
"disconnectAfterInSeconds": 3600
}
awscliでテストする時は、パスワードはbase64エンコード echo -n test | base64
→ dGVzdA==
が必要。エンコードしないと結果が "isAuthenticated": true
にはなるが \"Effect\":\"Deny\"
になってしまう。
6. クライアントから接続
まずはMosquittoクライアントからMQTT over WebSocketで接続する
mosquitto_pub \
--cafile AmazonRootCA1.pem \
-h <endpoint>-ats.iot.ap-northeast-1.amazonaws.com \
-p 443 \
-u 'test' \
-P 'test' \
-t 'notification/browser' \
-i browser \
--tls-alpn mqtt \
-l \
-d
mosquitto_sub \
--cafile thing/AmazonRootCA1.pem \
-h <endpoint>-ats.iot.ap-northeast-1.amazonaws.com \
-p 443 \
-u 'test?x-amz-customauthorizer-name=Authorizer' \
-P 'test' \
-t 'notification' \
-i browser \
--tls-alpn mqtt \
-d
- ポートは
443
- WebSocketの上のプロトコルを指定するためにTLS-ALPN(Application Layer Protocol Negotiation) を指定
--tls-alpn mqtt
- Default Authorizer が設定されてない場合は
-u 'test?x-amz-customauthorizer-name=Authorizer'
として指定する。
MQTT.js クライアントからMQTT over WebSocketで接続する
const mqtt = require('mqtt');
const fs = require('fs');
const options ={
clean: true,
connectTimeout: 4000,
clientId: 'browser',
username: 'test',
password: 'test',
}
const client = mqtt.connect('wss://<endpoint>-ats.iot.ap-northeast-1.amazonaws.com:443?x-amz-customauthorizer-name=Authorizer', options);
client.on('connect', () => {
console.log('connected');
});
ここではURLのQuery stringでAuthorizerの名前を指定しているが、これは以下のように usernameにつけてもよい。
username: 'test?x-amz-customauthorizer-name=Authorizer'
デバッグモードはONにしておいたほうがよい
npm install debug
DEBUG='mqttjs*' node my_mqtt_client.js
あとは、CDNからMQTT.jsを読み込むなどして、上のコードを貼り付ければブラウザから動くでしょう。
うまくいかない時は
うまくいかない時は以下の方法でログを確認するとよい。
- IoT Core の Authrizer
aws logs tail --follow AWSIotLogsV2
- Lamda AuthrizerFunction
aws logs tail --follow /aws/lambda/AuthorizerFunction
まとめ
- 公式の Lambda関数のサンプル だと、ブローカーからの
Publish-Out
が失敗する。おそらくPermission周りが原因。そのため、index.js
はちょっと書き換えている。 - サンプルではパスワードをLambdaにハードコーディングしているが、実際はパスワードDBを別に持つことになるだろう。
- 今回はAuthorizerの Token validation は使っていない。これGUIで一回ONしたら、OFFにできなくなってしまって、Authorizerを作り直すハメになった(謎)。作り直してAuthorizerの名前が変わったらLambdaのresouce-policyの
AWS:SourceArn
部分もアップデートが必要なので注意 - 一般的に証周りの設定はどこでコケているのか見えないとつらい。そのため最初からAWS IoT Core, Lambda のログをCloudWatchで見られるように設定しておくと切り分けが早い。
Discussion