Saltar al contenido principal
MQTT es un protocolo ligero que suele usarse para que los dispositivos se comuniquen con otros sistemas. Está diseñado para el patrón de mensajería de publicación/suscripción. Puedes obtener más información sobre MQTT en Wikipedia. En términos generales, hay 3 componentes:
  1. Un publicador de mensajes.
  2. Un suscriptor de mensajes.
  3. Un broker que conecta ambos.
Existe el concepto de topics (también llamados channels o subjects), con los que se asocian los mensajes. Los topics se usan para enrutar mensajes entre publicadores y suscriptores. El protocolo MQTT admite un mecanismo básico de autenticación basado en usernames y passwords. Estas credenciales se envían con el mensaje CONNECT. En este artículo se muestra una integración entre el broker MQTT basado en Node.js mosca y Auth0. En este ejemplo, Auth0 se utiliza para autenticar a los publicadores y suscriptores en el broker, y después autorizar el enrutamiento de mensajes.
Diagrama de flujo de datos de MQTT

Componentes de la solución

El broker

mosca es fácil de alojar y puede integrarse en otros servidores. Para este ejemplo, simplemente alojamos nosotros mismos un servidor mosca:
var mosca = require('mosca')
var Auth0Mosca = require('auth0mosca');

var settings = {
  port: 9999,
};

//'Thermostats' es una conexión de base de datos donde se registran todos los dispositivos.
var auth0 = new Auth0Mosca('https://eugeniop.auth0.com', '{Your Auth0 ClientID}', '{Your Auth0 Client Secret}','Thermostats');

//Configurar el servidor Mosca
var server = new mosca.Server(settings);

//Vincular autenticación y autorización a mosca
server.authenticate = auth0.authenticateWithCredentials();
server.authorizePublish = auth0.authorizePublish();
server.authorizeSubscribe = auth0.authorizeSubscribe();

server.on('ready', setup);

// Se ejecuta cuando el servidor mqtt está listo
function setup() {
    console.log('Mosca server is up and running');
}

server.on('clientConnected', function(client) {
  console.log('New connection: ', client.id );
});
Esto crea un servidor que escucha mensajes MQTT en el puerto 9999. mosca le permite sobrescribir las 3 funciones que se usan para autenticar y autorizar operaciones. En este ejemplo, usamos un módulo muy simple, auth0mosca, para realizar estas funciones. Auth0 está integrado con mosca.

El módulo Auth0Mosca

Este pequeño módulo proporciona las 4 funciones que utiliza mosca: authenticateWithCredentials, authenticateWithJWT, authorizePublish y 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;
authenticateWithCredentials usa OAuth2 Resource Owner Password Credential Grant para autenticar el broker y todas sus conexiones. Cada vez que un publicador o un suscriptor envían un mensaje CONNECT al broker, se llama a la función authenticate. En ella, llamamos al endpoint de Auth0 y reenviamos el username/password del dispositivo. Auth0 valida esto con su almacén de cuentas (esa es la primera llamada a request.post en el código). Si la validación es correcta, valida y analiza el (JWT) para obtener el perfil del dispositivo y lo agrega al objeto client, que representa al suscriptor o al publicador. Esto se hace en la llamada a jwt.verify. Por convención, todos los dispositivos conectados al broker tienen una cuenta en Auth0. Ten en cuenta que el perfil del dispositivo también tiene una propiedad topics. Es un arreglo con todos los temas a los que este dispositivo en particular tiene acceso. En la captura de pantalla anterior, thermostat-1a podrá publicar (o suscribirse) a los temas temperature y config. Las funciones authorizePublish y authorizeSubscribe simplemente comprueban que el tema solicitado esté presente en esta lista. authenticateWithJWT espera un JWT en el campo password. En este caso, el flujo es ligeramente distinto:
  1. El publicador y el suscriptor obtienen un token
  2. Se conectan a mosca enviando el JWT
  3. mosca valida el JWT
  4. Los mensajes se envían y se retransmiten a los suscriptores
Flujo de datos de MQTT con JWT
Los publicadores y los suscriptores obtendrán el JWT por algún medio. Tenga en cuenta que el broker ya no necesita comunicarse con Auth0. Los JWT son artefactos autocontenidos que pueden validarse con el secreto utilizado para firmarlos.

El publicador

Para este ejemplo, el publicador es un programa sencillo en Node.js que utiliza el módulo mqtt y añade las credenciales adecuadas:
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'
}

// conexión del cliente
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));
}
Por supuesto, aquí username & password deberán coincidir con lo que esté almacenado en Auth0.

El suscriptor

El suscriptor es muy parecido al publicador:
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'
}

// conexión del cliente
var client = mqtt.createClient(port, host, settings);


client.subscribe('temperature');

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

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

Resumen

Esto muestra lo fácil que es usar Auth0 en distintos escenarios. El almacén de usuarios de Auth0 se utiliza para administrar dispositivos. Por supuesto, se podrían definir reglas de autorización mucho más sofisticadas en función de otras condiciones: hora, ubicación, device_id, etc. Todo esto sería muy sencillo de implementar, ya sea mediante atributos adicionales del perfil o mediante Rules. Esto también muestra cómo el flexible perfil de Auth0 puede ampliarse para admitir artefactos arbitrarios (como topics en el ejemplo). Para obtener más información sobre Rules, puede consultar Auth0 Rules. Nunca es buena idea enviar credenciales (username/password) a través de redes no seguras. Existen otras implementaciones que proporcionan seguridad de nivel de transporte y evitarían que el contenido de los mensajes quede expuesto. mosca admite TLS, por ejemplo. Es probable que una implementación en producción prefiera esto, a menos que todo el tráfico se produzca dentro de una red cerrada.

Agradecimientos

Muchas gracias a Matteo Collina por revisar este artículo y por crear el fantástico mosca.