Authentifier et autoriser des appareils en utilisant MQTT avec Auth0

MQTT est un protocole léger souvent utilisé par les appareils pour communiquer avec d’autres systèmes. Il est conçu pour le modèle de messagerie publication/abonnement. Vous pouvez en savoir plus sur MQTT sur Wikipedia.

D’une manière générale, il y a trois composantes :

  1. Un publisher de messages.

  2. Un subscriber de messages.

  3. Un broker qui relie l’un et l’autre.

Il existe une notion de topics (également appelés channels ou subjects) auxquels les messages sont associés. Les sujets sont utilisés pour acheminer les messages entre les éditeurs et les abonnés.

Le protocole MQTT prend en charge un mécanisme d’authentification de base basé sur des usernames et des passwords. Ces identifiants sont envoyés avec le message CONNECT.

Cet article montre une intégration entre un agent MQTT basé sur nodejs : mosca et Auth0. Dans cet exemple, Auth0 est utilisé pour authentifier les publishers et subscribers auprès de l’agent, puis pour autoriser l’acheminement des messages.

Diagramme de flux de données MQTT

Composants de la solution

L’agent

mosca est facile à héberger et peut être intégré à d’autres serveurs. Pour les besoins de cet exemple, nous nous contentons d’héberger nous-mêmes un serveur mosca :

var mosca = require('mosca')
var Auth0Mosca = require('auth0mosca');

var settings = {
  port: 9999,
};

//'Thermostats' is a Database connection where all devices are registered.
var auth0 = new Auth0Mosca('https://eugeniop.auth0.com', '{Your Auth0 ClientID}', '{Your Auth0 Client Secret}','Thermostats');

//Setup the Mosca server
var server = new mosca.Server(settings);

//Wire up authentication & authorization to mosca
server.authenticate = auth0.authenticateWithCredentials();
server.authorizePublish = auth0.authorizePublish();
server.authorizeSubscribe = auth0.authorizeSubscribe();

server.on('ready', setup);

// Fired when the mqtt server is ready
function setup() {
    console.log('Mosca server is up and running');
}

server.on('clientConnected', function(client) {
  console.log('New connection: ', client.id );
});

Was this helpful?

/

Cela crée un serveur qui écoute les messages MQTT sur le port 9999. mosca vous permet de remplacer les trois fonctions utilisées pour authentifier et autoriser les opérations.

Dans cet exemple, nous utilisons un module très simple, auth0mosca, pour exécuter ces fonctions. Auth0 est connecté à mosca.

Le module Auth0Mosca

Ce petit module fiournit les quatre fonctions utilisées par mosca, authenticateWithCredentials, authenticateWithJWT, authorizePublish et authorizeSubscribe :

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;

Was this helpful?

/

authenticateWithCredentials utilise OAuth2 Propriétaire de ressource, attribution de mot de passe et d’identifiant pour authentifier l’agent et toutes les connexions qui lui sont adressées. Chaque fois qu’un publisher ou un subscriber envoie un message CONNEXION à l’agent, la fonction authenticate est appelée. Nous y appelons le point de terminaison Auth0 et lui transmettons leusername/password. Auth0 le valide par rapport à sa base de données de comptes (c’est le premierrequest.post dans le code). En cas de succès, il valide et analyse le jeton Web JSON (JWT) pour obtenir le profil de l’appareil et l’ajoute à l’objet client qui représente soit le subscriber ou le publisher. Cela se produit dans l’appel jwt.verify.

Par convention, tous les appareils connectés à l’agent ont un compte dans Auth0.

Remarquez que le profil de l’appareil a également une propriété topics. Il s’agit d’un tableau contenant tous les sujets auxquels cet appareil particulier est autorisé à accéder. Dans la capture d’écran ci-dessus, le thermostat-1a sera autorisé à publier (ou à s’abonner) aux sujets temperature et config.

Les fonctionnalités authorizePublish et authorizeSubscribe vérifient simplement qu’un sujet particulier demandé est présent dans cette liste.

authenticateWithJWT attend un JWT dans le champ password. Dans ce cas, le flux est légèrement différent :

  1. L’éditeur et l’abonné obtiendront un jeton

  2. Ils se connectent à mosca en soumettant le JWT

  3. mosca valide le JWT

  4. Les messages sont envoyés et retransmis aux abonnés

MQTT JSON Web Token Data Flow

Les éditeurs et les abonnés obtiendront le JWT d’une manière ou d’une autre. Notez que l’agent n’a plus besoin de communiquer avec Auth0. Les JWT sont des artefacts autonomes qui peuvent être validés à l’aide du secret utilisé pour les signer.

L’éditeur

Pour cet exemple, l’éditeur est un simple programme nodejs qui utilise le module mqtt, et ajoute les bons identifiants :

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'
}

// client connection
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));
}

Was this helpful?

/

Bien entendu, le username et le password devront correspondre à ceux stockés dans Auth0.

L’abonné

L’abonné est très similaire à l’éditeur :

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'
}

// client connection
var client = mqtt.createClient(port, host, settings);


client.subscribe('temperature');

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

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

Was this helpful?

/

Résumé

Cela montre à quel point il est facile d’utiliser Auth0 dans différents scénarios. Le magasin d’utilisateurs d’Auth0 est utilisé pour gérer les appareils. Bien entendu, des règles d’autorisation beaucoup plus sophistiquées pourraient être élaborées sur la base d’autres conditions : heure, lieu, device_id, etc. Toutes ces règles seraient très simples à mettre en œuvre, soit au moyen d’attributs de profil supplémentaires, soit au moyen de règles. Cela montre également comment le profil Auth0 flexible peut être étendu pour prendre en charge des artefacts arbitraires (tels que les topics dans l’exemple).

Pour en savoir plus sur les règles, consultez Règles d'Auth0.

Il n’est jamais bon d’envoyer des identifiants (username/password) sur des réseaux non sécurisés. Il existe d’autres implémentations qui fournissent une sécurité au niveau du transport qui empêcherait le contenu des messages d’être révélé. mosca prend en charge TLS par exemple. Il est probable qu’un déploiement en production privilégie cette solution, à moins que tout le trafic se fasse dans un réseau fermé.

Remerciements

Un grand merci à Matteo Collina pour la relecture de cet article et pour la production de mosca.