メインコンテンツへスキップ
MQTT は、デバイスが他のシステムと通信する際によく使われる軽量なプロトコルです。publish/subscribe メッセージングパターン向けに設計されています。MQTT の詳細については、Wikipedia を参照してください。 一般に、構成要素は 3 つあります。
  1. メッセージを送信する パブリッシャー
  2. メッセージを受信する サブスクライバー
  3. その両者を接続する ブローカー
topics (channelssubjects とも呼ばれます) という概念があり、メッセージはこれらに関連付けられます。トピックは、パブリッシャー と サブスクライバー の間でメッセージをルーティングするために使用されます。 MQTT プロトコルは、usernamespasswords に基づく基本的な認証メカニズムをサポートしています。これらの認証情報は CONNECT メッセージで送信されます。 この記事では、Node.js ベースの MQTT ブローカー である moscaAuth0 の統合について説明します。この例では、Auth0 を使用して ブローカー に接続する publisherssubscribers認証し、その後、メッセージのルーティングを認可します。
MQTT データフロー図

ソリューションの構成要素

ブローカー

mosca は容易にホストでき、ほかのサーバーに組み込むこともできます。このサンプルでは、mosca サーバーを自前でホストするだけです。
var mosca = require('mosca')
var Auth0Mosca = require('auth0mosca');

var settings = {
  port: 9999,
};

//'Thermostats' はすべてのデバイスが登録されているデータベース接続です。
var auth0 = new Auth0Mosca('https://eugeniop.auth0.com', '{Your Auth0 ClientID}', '{Your Auth0 Client Secret}','Thermostats');

//Moscaサーバーをセットアップする
var server = new mosca.Server(settings);

//認証と認可をmoscaに紐付ける
server.authenticate = auth0.authenticateWithCredentials();
server.authorizePublish = auth0.authorizePublish();
server.authorizeSubscribe = auth0.authorizeSubscribe();

server.on('ready', setup);

// mqttサーバーの準備が完了したときに発火する
function setup() {
    console.log('Mosca server is up and running');
}

server.on('clientConnected', function(client) {
  console.log('New connection: ', client.id );
});
これにより、ポート 9999 で MQTT メッセージを待ち受けるサーバーが作成されます。mosca では、操作の認証と認可に使用する 3 つの関数をオーバーライドできます。 このサンプルでは、これらの機能を実行するために、非常にシンプルなモジュール auth0mosca を使用しています。Auth0 は mosca と連携するように組み込まれています。

Auth0Mosca モジュール

この小さなモジュールは、mosca で使用される 4 つの関数 authenticateWithCredentialsauthenticateWithJWTauthorizePublishauthorizeSubscribe を提供します。
var request = require('request');
var jwt = require('jsonwebtoken');

function Auth0Mosca(auth0Namespace, clientId, clientSecret, connection)
{
  this.auth0Namespace = auth0Namespace;
  this.connection = connection;
  this.clientId = clientId;
  this.clientSecret = clientSecret;
}

Auth0Mosca.prototype.authenticateWithJWT = function(){

  var self = this;

  return function(client, username, password, callback) {

    if( username !== 'JWT' ) { return callback("Invalid Credentials", false); }

    // console.log('Password:'+password);

    jwt.verify(password, self.clientSecret, function(err,profile){
          if( err ) { return callback("Error getting UserInfo", false); }
          console.log("Authenticated client " + profile.user_id);
          console.log(profile.topics);
          client.deviceProfile = profile;
          return callback(null, true);
        });
  }
}

Auth0Mosca.prototype.authenticateWithCredentials = function(){

  var self = this;

  return function(client, username, password, callback) {
    
    var data = {
        client_id:   self.clientId, // {client-name}
        username:    username.toString(),
        password:    password.toString(),
        connection:  self.connection,
        grant_type:  "password",
        scope: 'openid name email' //Details: https:///scopes
    };

    request.post({
        headers: {
                "Content-type": "application/json"
            },
        url: self.auth0Namespace + '/oauth/ro',
        body: JSON.stringify(data)
      }, function(e,r,b){
        if(e){
          console.log('Error in Authentication');
          return callback(e,false);
        }
        var r = JSON.parse(b);

        if( r.error ) { return callback( r, false); }

        jwt.verify(r.id_token, self.clientSecret, function(err,profile){
          if( err ) { return callback("Error getting UserInfo", false); }
          client.deviceProfile = profile;
          return callback(null, true);
        });
    });
  }
}

Auth0Mosca.prototype.authorizePublish = function() {
  return function (client, topic, payload, callback) {
   callback(null, client.deviceProfile && client.deviceProfile.topics && client.deviceProfile.topics.indexOf(topic) > -1);
  }
}

Auth0Mosca.prototype.authorizeSubscribe = function() {
  return function(client, topic, callback) {
  callback(null, client.deviceProfile && client.deviceProfile.topics && client.deviceProfile.topics.indexOf(topic) > -1);
}

module.exports = Auth0Mosca;
authenticateWithCredentials は、ブローカーとそれに対するすべての接続を認証するために OAuth2 Resource Owner Password Credential Grant を使用します。パブリッシャー または サブスクライバー がブローカーに CONNECT メッセージを送信するたびに、authenticate 関数が呼び出されます。この関数内で Auth0 の endpoint を呼び出し、デバイスの username/password を転送します。Auth0 はこれをアカウントストアに対して検証します (これはコード内の最初の request.post です) 。成功すると、デバイスのユーザープロファイルを取得するために (JWT) を検証して解析し、それを サブスクライバー または パブリッシャー を表す client オブジェクトに追加します。これは jwt.verify の呼び出しで行われます。 慣例として、ブローカーに接続されるすべてのデバイスは Auth0 にアカウントを持ちます。 Device Profile にも topics プロパティがあることに注目してください。これは、この特定のデバイスに許可されているすべてのトピックを含む配列です。上のスクリーンショットでは、thermostat-1a はトピック temperatureconfig へのパブリッシュ (またはサブスクライブ) を許可されます。 authorizePublish 関数と authorizeSubscribe 関数は、要求された特定のトピックがこのリストに含まれていることを確認するだけです。 authenticateWithJWT は、password フィールドに JWT が含まれていることを想定しています。この場合のフローは少し異なります。
  1. パブリッシャー と サブスクライバー がトークンを取得します
  2. JWT を送信して mosca に接続します
  3. mosca が JWT を検証します
  4. メッセージが送信され、サブスクライバー に再送信されます
MQTT JSON Web Token データフロー
パブリッシャーとサブスクライバーは、何らかの方法で JWT を取得します。ブローカーは、もはや Auth0 と通信する必要がないことに注意してください。JWT は自己完結型であり、署名に使用したシークレットを使って検証できます。

パブリッシャー

このサンプルでは、パブリッシャーは mqtt モジュールを使用し、適切な認証情報を設定したシンプルな Node.js プログラムです。
var mqtt = require('mqtt')
  , host = 'localhost'
  , port = '9999';

var settings = {
  keepalive: 1000,
  protocolId: 'MQIsdp',
  protocolVersion: 3,
  clientId: 'Thermostat 1a',
  username:'thermostat-1a',
  password:'the password'
}

// クライアント接続
var client = mqtt.createClient(port, host, settings);

setInterval(sendTemperature, 2000, client);

function sendTemperature(client){
  var t = {
    T: Math.random() * 100,
    Units: "C"
  };

  client.publish('temperature', JSON.stringify(t));
}
もちろん、ここでの usernamepassword は、Auth0 に保存されているものと一致している必要があります。

サブスクライバー

サブスクライバーは、パブリッシャーと非常によく似ています。
var mqtt = require('mqtt')
  , host = 'localhost'
  , port = '9999';

var settings = {
  keepalive: 1000,
  protocolId: 'MQIsdp',
  protocolVersion: 3,
  clientId: 'Reader-X1',
  username:'reader-X1',
  password:'the password'
}

// クライアント接続
var client = mqtt.createClient(port, host, settings);


client.subscribe('temperature');

client.on('message', function(topic, message) {

  if(topic ==='temperature')
  {
    console.log('New reading', message);
  }
});

概要

これは、さまざまなシナリオで Auth0 を簡単に利用できることを示しています。デバイスの管理には Auth0 のユーザーストアが使用されています。もちろん、時間、場所、device_id などの別の条件に基づいて、より高度な認可ルールを記述することもできます。これらはすべて、追加のユーザープロファイル属性や Rules を使って、非常に簡単に実装できます。また、柔軟な Auth0 Profile を拡張して、任意の項目 (この例の topics など) をサポートできることも示しています。 Rules の詳細については、Auth0 Rules を参照してください。 安全でないネットワーク上で認証情報 (username/password) を送信するのは、決してよい考えではありません。トランスポート層のセキュリティを提供し、メッセージ内容の漏えいを防ぐ実装もあります。たとえば、mosca は TLS をサポートしています。すべてのトラフィックが閉じたネットワーク内で完結するのでない限り、本番環境ではこちらが選ばれるでしょう。

謝辞

この記事のレビューをしてくださり、またすばらしい mosca を開発してくださった Matteo Collina 氏に深く感謝します。