Angular y Passport (I)

5 diciembre 2019 0 Por Juan Martin

Seguridad en el API

La seguridad como principio de diseño

Cuando nos planteamos desarrollar una aplicacion la primera cosa que se nos debe pasar por la cabeza, antes de entrar en los detalles propios del diseño y desarrollo de la misma, es como vamos a proteger de miradas no autorizadas la informacion que nuestra app va a manejar.

De todos los mecanismos de gestion de la seguridad que Nodejs pone a nuestra disposicion voy a centrarme en uno de los mas usados, Passport, con su extension  para JSON Web Tokens ( https://jwt.io/ ).

 

JSON Web Tokens

 

JWT es estandar abierto regulado por RFC7519 gestionado por el IETF. Es una manera de codificar informacion acerca de una persona o empresa sin exponer URL o datos confidenciales de cara a terceros y que se codifican y alojan en un documento JSON.

La informacion es firmada por una clave secreta que ambas partes conocen por lo que es muy facil averiguar si esta ha sido falsificada en el proceso a la vez que mantiene dicha informacion oculta a ojos no autorizados.

Un token JWt se compone de tres partes separadas por un punto (.):

  • Cabecera
  • Carga (o payload)
  • Firma

 

La cabecera se puede descomponer en dos elementos. El primero indica el metodo de cifrado y el segundo el tipo de token, generalmente JWT.

 


{
"alg": "HS256",
"typ": "JWT"
}

 

La carga o payload puede ser de tres formas. La primera es lo que llaman Nombres de Reclamacion Registrada, un tipo de informacion predefinida en el mencionado RFC.
La segunda de las formas es lo que llaman Nombres de Reclamacion Publicas, gestionadas por IANA (https://www.iana.org/assignments/jwt/jwt.xhtml) y la tercera es lo que llaman Nombres de Reclamacion Privada, que son los datos que normalmente usaremos.


{
"email": "mymail@mydomain.com",
"profile": "user"
}


En nuestro caso, los datos de usuario que firmaremos con JWT serian:


const body = { _id: user._id, name: user.name };

 

Solo le pasaremos el id almacenado en la base de datos y el nombre del usuario al proceso de firma.
Con passport-jwt esto se consigue llamando al metodo sign.


const token = jwt.sign({ user: body }, 'top_secret');

Una vez firmado obtenemos el token que nos ayudara a identificarnos cuando accedamos al sitio y que tiene un aspecto como este:


eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Il9pZCI6IjVlMjgxMTkxOWY2ZjAzMWQ5NDgzNTZhOSIsIm5hbWUiOiJQZXJpIn0sImlhdCI6MTU3OTY4NDI1NX0.hJW8Hs_eM4PiHyi4jLuDF0NbQgYTNNL3UEgfDT3llNE

Estrategias

Passport usa lo que se conocen como «Estrategias» para gestionar el acceso y la generación de tokens. A fecha de la escritura de este post Passport soporta mas de quinientas estrategias, desde la que usaremos aquí, local, hasta autenticacion con Facebook, Twitter, etc..

La estrategia local usualmente se usa para autenticar un usuario en base a un nombre de usuario y un password contra una base de datos, local o remota, a la que tengamos acceso.

Los usuarios se autentican llamando al metodo authenticate()


router.post('/login', (req, res, next) => {
  // Autenticacion de usuario.
  // Si no existe devolvemos un mensaje y user y token a null
  passport.authenticate('login', (err, user, info) => {
    try {
      console.log(user);
      if (err || !user) {
        res.json({
          message: 'Login unsuccessful',
          user: null,
          token: null
        });
      }

      // Si existe....
      req.login(user, { session: false }, error => {
        if (error) return next(error);
        // Codificamos el token a partir del user id de la db y el nombre del usuario.
        const body = { _id: user._id, name: user.name };

        // Firmamos el payload con nuestra superclave
        const token = jwt.sign({ user: body }, 'top_secret');

        // Devolvemos un mensaje y un user mas el token que debe usar el cliente para
        // acceder a la parte privada del api
        res.json({
          message: 'Login successful',
          user: req.user,
          token: token
        });
      });
    } catch (error) {
      return next(error);
    }
  })(req, res, next);
});

Las estrategias se configuran a traves del metodo use()

 


var localStrategy = require('passport-local').Strategy;

  const JWTstrategy = require('passport-jwt').Strategy;
  // Extractor de JWT
  const ExtractJWT = require('passport-jwt').ExtractJwt;

  var opts = {
    // Clave para el cifrado. Super secreta. No poner aqui, solo para efectos ilustrativos
    secretOrKey: 'top_secret',
    // Establecemos el extractor que vamos a usar. Usamos un campo llamado token que se adjunta al body como x-www.form-urlencoded.
    jwtFromRequest: ExtractJWT.fromBodyField('token')
  };

passport.use(
    new JWTstrategy(opts, (payload, done) => {
      User.findOne({ id: payload.sub }, function(err, user) {
        if (err) {
          return done(err, false);
        }
        if (user) {
          return done(null, user);
        } else {
          // Podriamos redirigir al signup
          return done(null, false);
        }
      });
    })
  );

A dicho método le pasamos una callback que passport usara para verificar que los usuarios son correctos.

Ademas de esta forma de configurar la estrategia esta la forma nombrada, a la que le damos un nombre para ser usado con authenticate()

 


passport.use(
    'login',
    new localStrategy(
      {
        usernameField: 'name',
        passwordField: 'password'
      },
      async (name, password, done) => {
        try {
          console.warn('Searching User...');

          // Buscamos nuestro user
          const user = await User.findOne({ name });

          if (!user) {
            // Si no lo encontramos...
            console.warn('User not found');

            // retornamos un error.
            return done(null, false, { message: 'User not found' });
          }
          // Validamos el password comparandolo con el hash que tenemos almacenado en la base de datos
          // Si son iguales procedemos a enviar la cookie con el token de autorizacion

          console.log('User found: ' + user.name);
          console.warn('Validating user....wait');

          console.log(user);

          const validate = await user.isValidPassword(password);

          if (!validate) {
            return done(null, false, { message: 'Wrong Password' });
          }
          // Enviamos el resultado al proximo middleware
          console.warn('User found!!!');
          return done(null, user, { message: 'Logged in Successfully' });
        } catch (error) {
          return done(error);
        }
      }
    )
  );

De igual forma que la forma general, esta recibe una callback que se ejecutara al finalizar la llamada al metodo.

Resumiendo

En resumidas cuentas Passport nos ofrece una manera segura de manejar la autentificacion de usuarios a través de un montón de fuentes.

Para ello se requiere una callback de verificación a la hora de configurar la estrategia y callbacks adicionales si queremos controlar su comportamiento durante los procesos, por ejemplo, de registro de un usuario o login.

En la próxima entrada hablaremos del modelo de datos que vamos a usar y de las rutas para el registro y logueo de usuarios.

 

Un saludo.