https://d226lax1qjow5r.cloudfront.net/blog/blogposts/add-2fa-to-nuxt-with-nexmo-verify-dr/poster-image-1.png

Añada 2FA a su aplicación Nuxt con Nexmo Verify

Publicado el April 19, 2021

Tiempo de lectura: 17 minutos

En este tutorial vamos a crear una aplicación básica con autenticación de dos factores utilizando el módulo Nuxt JS de Nuxt JS.

Nuestra aplicación contiene una sección secreta a la que sólo se puede acceder si el usuario se verifica introduciendo un código PIN que se le envía por SMS.

Si quieres ver todo el código de este ejemplo puedes echar un vistazo al archivo nexmo-verify-nuxt en nuestro GitHub de la Comunidad Nexmo.

Requisitos previos

Si vas a seguirnos necesitarás lo siguiente:

  • Node JS - esta aplicación fue construida usando v10.0, pero debería funcionar bien con la versión 8 o superior.

  • Experiencia previa con VueJS sería útil pero no absolutamente necesaria. Esta es una aplicación muy básica y sirve como una buena introducción para los nuevos aprendices.

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

This tutorial also uses a virtual phone number. To purchase one, go to Numbers > Buy Numbers and search for one that meets your needs.

Introducción a Nuxt

Nuxt es un framework para construir 'Universal Vue.js Applications'. Esencialmente, esto significa que cuando se utiliza Nuxt tiene la opción de construir su aplicación terminada para la producción de tres maneras diferentes:

  1. Como una aplicación renderizada del lado del servidor que construye todo el HTML que se renderizará en el navegador en el lado del servidor en tiempo de ejecución y luego lo envía al usuario.

  2. Como una Single Page Application donde todo el renderizado de la UI tiene lugar en el navegador en tiempo de ejecución. Esto sería lo más parecido a una aplicación Vue JS tradicional.

  3. Como un sitio estático, donde todo el HTML, CSS y JS se construye en archivos estáticos que se pueden subir a Amazon S3, Páginas GitHub, Netlify - ¡donde quieras!

Es un framework realmente flexible con el que, como fan de Vue JS, he disfrutado mucho trabajando. Si quieres saber más sobre él, echa un vistazo a la introducción a Nuxt JS en su documentación.

¿Por qué utilizar Nuxt en este caso?

He elegido Nuxt para este tutorial porque quería destacar un par de características que realmente me han impresionado y que han hecho que las posibilidades de trabajar con un framework como este sean casi ilimitadas. Estas son:

  • Renderizado del lado del servidor

  • Middleware de servidor personalizado

No se preocupe demasiado si aún no sabe lo que son. Los cubriremos muy pronto. Vamos a empezar con el código para nuestra aplicación mediante la instalación de Nuxt y la construcción de nuestra estructura de carpetas.

Estructurar la aplicación

Empieza por crear una carpeta vacía para trabajar, llámala como quieras.

Vamos a utilizar NPM para instalar las dependencias de este proyecto, así que empieza por ejecutar:

npm init -y # The -y flag will skip through the questions

A continuación, instale las dependencias:

npm install nuxt express jsonwebtoken axios nexmo@beta

También trabajaremos con el excelente dotenv para gestionar nuestras variables de entorno, así que instálalo como devDependency.

npm install -D dotenv

Antes de entrar en la estructura de carpetas de la aplicación hay que hacer un pequeño cambio. Abre package.json en tu editor y sustituye la sección scripts por esto:

"scripts": {
  "dev": "nuxt",
  "build": "nuxt build",
  "start": "nuxt start"
}

Añadir la estructura de carpetas

Nuxt utiliza un conjunto de carpetas específicas para organizarse. A menudo, la presencia de estas carpetas asegura que las dependencias para hacer ciertas cosas se incluyan automáticamente cuando construyes tu aplicación.

Por ejemplo, si necesita utilizar VueX en tu aplicación para gestionar el estado compartido, no necesitas instalarlo. Basta con crear una carpeta llamada storelanza un index.js dentro y Nuxt incluirá automáticamente VueX por ti. Es bastante dulce.

Nosotros vamos a Vamos a utilizar VueX, y un par de cosas más en nuestra aplicación por lo que, en la raíz de su directorio de trabajo, crear algunas carpetas nuevas:

mkdir pages store layouts middleware node-scripts assets

En pages, storey middleware son específicas de Nuxtpara esto sirven:

  • pages - Aquí es donde guardas las páginas de tu sitio. Nuxt soporta generación automática de rutas que se alinea con su estructura de carpetas. Por ejemplo, para lograr https://myapp.xyz/register, usted tendría que tener una carpeta llamada register dentro de la carpeta pages carpeta.

  • store - Esta carpeta contiene los archivos de su tienda VueX. VueX se incluirá automáticamente si tienes esta carpeta.

  • middleware - Si desea definir funciones personalizadas para ejecutar antes de que las páginas se rendericen, aquí es donde tienen que vivir.

Si quieres saber más sobre cómo la estructura de carpetas se relaciona con las acciones en Nuxt, consulta su Estructura de directorios de Nuxt.

Configuración, estilo, tienda y diseño predeterminado

Dado que es mejor centrarse en el aspecto central de este tutorial, voy a sugerir que copies ciertos aspectos directamente del repositorio o de bloques de código en aras de la rapidez.

En la raíz de su directorio de trabajo, cree un nuevo archivo llamado ed nuxt.config.js y ábrelo. Añade el siguiente código:

module.exports = {
  head: {
    meta: [
      { name: 'viewport', content: 'width=device-width, initial-scale=1.0' }
    ],
    script: [
      {
        src:
          'https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/js/materialize.min.js',
        body: true
      }
    ],
    link: [
      {
        rel: 'stylesheet',
        href: 'https://fonts.googleapis.com/icon?family=Material+Icons'
      },
      {
        rel: 'stylesheet',
        href:
          'https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/css/materialize.min.css'
      }
    ]
  },
  loading: false,
  build: {
    vendor: ['axios']
  },
  env: {
    baseUrl: process.env.BASE_URL || 'http://localhost:3000'
  }
};

Esto asegurará que todo el estilo y las fuentes que he utilizado también funcionen en tu versión. Estamos utilizando Materialize CSS como nuestro marco de interfaz de usuario, usted puede comprobar fuera de su documentación si necesita más información sobre cualquiera de los elementos de diseño.

Para obtener más información sobre la configuración de Nuxt, consulte su guía.

A continuación, abra la carpeta layouts y cree un nuevo archivo llamado default.vue. Añádele la siguiente marca:

<template>
  <div class="container">
    <nuxt/>
  </div>
</template>

Esto garantizará que todas las demás vistas que creemos estén contenidas y dispuestas correctamente.

Esta aplicación utiliza cierta gestión de estados, por lo que dentro de la carpeta store crea un archivo llamado index.js y añade el siguiente código:

import Vuex from 'vuex';

export const store = () => {
  return new Vuex.Store({
    state: {
      token: null
    },
    mutations: {
      SET_TOKEN(state, token) {
        state.token = token || null;
      },
      INVALIDATE_TOKEN(state) {
        state.token = null;
      }
    },
    getters: {
      isVerified(state) {
        return state.token;
      }
    }
  });
};

export default store;

Una vez hecho esto, ¡es hora de escribir el código!

Crear las páginas

La aplicación que estamos construyendo tiene dos páginas. La primera es una página de aterrizaje con un formulario de inicio de sesión, la segunda es una sección secreta a la que los usuarios pueden acceder una vez que han sido verificados.

Primero trabajaremos en la página de aterrizaje. Dentro del directorio pages cree un nuevo archivo llamado index.vue.

La página de aterrizaje

Abra el archivo index.vue en su editor y añada el siguiente código:

<template>
  <div>
    <div class="row">
      <div class="col s12 center-align">
        <h1><i class="medium material-icons">verified_user</i> Login</h1>
      </div>
    </div>
    <div class="row" v-if="request.token === ''">
      <form v-on:submit.prevent class="col s12">
      <div class="row center-align">
        <div class="input-field col s12">
          <i class="material-icons prefix">phone</i>
          <input type="text" id="phoneNumber" v-model="phoneNumber"/>
          <label for="phoneNumber"> Phone Number</label>
        </div>
        <div class="row center-align">
          <button v-on:click.stop.prevent="sendVerificationCode" type="submit" class="waves-effect waves-light btn"><i class="material-icons left">account_box</i>Send me a verifiation code</button>
        </div>
      </div>
    </form>
    </div>
    <div class="row" v-else>
      <form v-on:submit.prevent class="col s12">
        <div class="row center-align">
          <div class="input-field col s12">
            <i class="material-icons prefix">sms</i>
            <input type="text" id="verificationPin" v-model="request.verificationPin"/>
            <label for="verificationPin"> Enter the pin you were sent</label>
          </div>
          <div class="row center-align">
            <button v-on:click.stop.prevent="verifyPin" type="submit" class="waves-effect waves-light btn"><i class="material-icons left">account_box</i>Verify me</button>
          </div>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import { mapMutations } from 'vuex';
import axios from 'axios';

export default {
  data() {
    return {
      phoneNumber: '',
      request: {
        token: '',
        verificationPin: ''
      }
    };
  },
  methods: {
    displayMessage: function(type, message) {
      if (type === 'error') {
        M.toast({
          html: `${message}`,
          classes: 'rounded red accent-1',
          displayTime: 3000
        });
      }
    },

    sendVerificationCode: async function() {
      const { data } = await axios.post('/verification/send', {
        phoneNumber: this.phoneNumber
      });
      if (!data.token) {
        this.displayMessage('error', data.error_text || data.error);
      } else {
        const { token } = data;
        this.request.token = token;
      }
    },

    verifyPin: async function() {
      const { data } = await axios.post('/verification/verify', {
        token: this.request.token,
        code: this.request.verificationPin
      });
      if (!data.token) {
        this.displayMessage('error', data.error_text || data.error);
      } else {
        const { token } = data;
        this.$store.commit('SET_TOKEN', token);
        this.$nuxt.$router.replace({ path: '/secret' });
      }
    }
  }
};
</script>

Hay dos secciones en este archivo <template> y <script>. La sección <template> contiene el HTML de nuestro formulario de acceso y la sección <script> tiene todos los métodos que necesitamos para hacer cosas con ese formulario.

Estamos exponiendo tres métodos aquí, en orden, esto es lo que hacen:

  • displayMessage - Esta es sólo una función de ayuda que mostrará cualquier error devuelto a través de la API en un mensaje de error superpuesto. Las clases que ves ahí vienen directamente de Materialize CSS.

  • sendVerificationCode - Toma el número de teléfono introducido por el usuario y lo pasa a nuestro middleware API para trabajar con Nexmo Verify

  • verifyPin - Cuando el usuario recibe su código PIN tiene que introducirlo aquí también, este método lo pasa al punto final de verificación en nuestro middleware API.

Ya puedes ejecutar la aplicación:

npm run dev

Si todo va bien, debería poder dirigirse a https://localhost:3000 y ver esto:

Log in Page

Lamentablemente, al hacer clic en "Envíame un código de verificación" no se va a hacer nada porque nuestro código está tratando de pasar el número a un punto final llamado /verification/send que no existe.

Es hora de pasar al lado del servidor.

Añadir una mini API de verificación

El sitio Verify API de Nexmo requiere que utilices una clave y un secreto para autenticar las peticiones. En aplicaciones típicas de navegador, o de una sola página, esto no sería algo que podríamos hacer sin exponer nuestra clave secreta al mundo - desastre.

La ruta habitual para resolver esto sería crear un rápido script NodeJS que expone algunos puntos finales Express que puede golpear para lograr lo que quiere, manteniendo sus claves secretas.

El inconveniente es que, a menos que tu aplicación realmente necesite una API voluminosa con muchos puntos finales y funcionalidad del lado del servidor, encontrar un lugar para alojar este script además de tu aplicación Nuxt es, para mí, más esfuerzo del necesario.

¿Sabías que puedes ejecutar Express dentro de Nuxt?

Así es. Como Nuxt ya está renderizando cosas del lado del servidor, técnicamente NodeJS ya está en juego, lo que significa que puedes usar paquetes como Express como middleware del lado del servidor.

Esto significa que no hay ningún servidor adicional para nuestra pequeña API de verificación.

Un archivo, dos puntos finales, todo del lado del servidor

Cree un nuevo archivo dentro de la carpeta node-scripts llamado verification_api.js y ábrelo en tu editor.

if (process.env.NODE_ENV !== 'production') {
  require('dotenv').config();
}
const express = require('express');
const Nexmo = require('nexmo');
const jwt = require('jsonwebtoken');

const API_KEY = process.env.apiKey;
const API_SECRET = process.env.apiSecret;
const JWT_SECRET = process.env.jwtSecret;

const app = express();
app.use(express.json());

const nexmo = new Nexmo({ apiKey: API_KEY, apiSecret: API_SECRET });

app.post('/send', async (req, res) => {
  // Get the phone number from the request body of our main app
  let phoneNumber = req.body.phoneNumber;

  nexmo.verify.request(
    {
      number: phoneNumber,
      brand: 'MyApp',
      code_length: 6,
      pin_expiry: 60
    },
    async (err, result) => {
      if (err || result.status !== '0') {
        res.json({ error: err || result.error_text });
      } else {
        const accessToken = await jwt.sign(
          {
            phoneNumber,
            request_id: result.request_id
          },
          JWT_SECRET
        );
        res.json({ token: accessToken });
      }
    }
  );
});

app.post('/verify', async (req, res) => {
  let { token, code } = req.body;
  const tokenObject = await jwt.verify(token, JWT_SECRET);

  nexmo.verify.check(
    { request_id: tokenObject.request_id, code: code },
    async (err, result) => {
      if (err || result.status !== '0') {
        res.json({ error: err || result.error_text });
      } else {
        const tokenizeResult = await jwt.sign(result, JWT_SECRET);
        res.json({ token: tokenizeResult });
      }
    }
  );
});

app.post('/auth-check', async (req, res) => {
  let { token } = req.body;
  const tokenObject = await jwt.verify(token, JWT_SECRET);
  const { request_id } = tokenObject;

  nexmo.verify.search(request_id, async (err, result) => {
    if (err) {
      res.json({ error: err });
    } else {
      res.json(result);
    }
  });
});

module.exports = {
  path: '/verification',
  handler: app
};

Los que estéis familiarizados con Node & Express Applications os sentiréis como en casa con este código.

Hay tres puntos finales, /send, /verify y /auth-check expuestos. Esto es lo que hacen:

  • /send - Recibe el número de teléfono del usuario del formulario y crea una nueva solicitud de verificación utilizando el SDK del Nodo Nexmo. El request_id de esta nueva solicitud se convierte en un Token Web JSON firmado y se envía de vuelta al front end.

  • /verify - Recibe el JWT y el código pin del usuario. Se descodifica el JWT y se extrae el original. request_id y se pasa junto con el pin a la Verify API para ver si coinciden. Si coinciden, la verificación se realiza correctamente.

  • /check-auth - Es un método de ayuda que se utiliza para comprobar si ya existe una autorización para el usuario. request_idpara que los usuarios no tengan que introducir su número cada vez.

A diferencia de una aplicación express típica en la que establecerías server.listen a un puerto, vamos a exportar todo nuestro script como un módulo para que Nuxt pueda referenciarlo.

Antes de que funcione, sin embargo, crear un archivo llamado .env en la raíz de tu directorio de trabajo y añade lo siguiente:

apiKey = "YOUR NEXMO API KEY GOES HERE"
apiSecret = "YOUR NEXMO API SECRET GOES HERE"
jwtSecret = "ANY RANDOM STRING OF LETTERS & NUMBERS GOES HERE"

Nota: Asegúrate de mantener .env fuera de cualquier repositorio de GitHub añadiendo también un archivo .gitignore a tu directorio de trabajo. Puede copiar el que he creado para este proyecto.

Montaje de middleware del lado del servidor

Para que nuestros nuevos puntos finales sean accesibles en nuestra aplicación Nuxt, hay que completar dos pasos.

La primera es registrar el script como middleware en nuxt.config.js. Hazlo añadiendo la siguiente línea:

serverMiddleware: ['~/node-scripts/verification_api'];

Si necesita comprobar exactamente dónde debe ir, puede consultar el ejemplo en GitHub.

El segundo paso consiste en crear la aplicación para que estas nuevas rutas (/verification/send y /verification/verify) estén disponibles. Hazlo ejecutando este comando en tu terminal:

npm run build

Una vez hecho esto, reinicie el servidor de desarrollo:

npm run dev

Vaya a https://localhost:3000 e introduzca su número en el formulario. Asegúrate de introducir también el prefijo del país.

Al hacer clic en "Enviarme un código de verificación" recibirá un SMS con un código PIN de 6 dígitos. También te darás cuenta de que la vista ha cambiado y ahora está esperando el pin.

Verification page

La introducción del PIN finalizará la verificación y, si todo es correcto, la aplicación intentará redirigir a una ruta llamada /secret.

...que aún no existe. Creémoslo y asegurémonos de que es seguro usando algún middleware Nuxt.

Proteger nuestra página secreta

Cree una nueva carpeta dentro de la carpeta pages llámela secret y añádele un archivo llamado index.vue a la misma.

Abra index.vue en tu editor y añade el siguiente código:

<template>
  <div>
    <div class="row center-align">
      <h1>Secret Area</h1>
    </div>
    <div class="row center-align">
      <button v-on:click.stop.prevent="logout" class="waves-effect waves-light btn red"><i class="material-icons left">account_box</i>Logout</button>
    </div>
  </div>
</template>

<script>
export default {
  middleware: 'check_auth',
  methods: {
    // Clicking log out triggers this function that wipes out any pre-existing tokens
    logout: function() {
      this.$store.commit('INVALIDATE_TOKEN');
      this.$nuxt.$router.replace({ path: '/' });
    }
  }
};
</script>

Verás que este código hace referencia a un middleware llamado check_auth. Este middleware será llamado cada vez que este archivo sea solicitado, y por lo tanto puede ser usado para asegurar la página.

Crear el middleware de autenticación

En la carpeta middleware cree un nuevo archivo llamado check_auth.js y ábrelo en tu editor. Añade el siguiente código:

import axios from 'axios';

export default function({ store, route, redirect }) {
  if (store.getters.isVerified) {
    const token = store.getters.isVerified;
    axios
      .post('/verification/auth-check', {
        token
      })
      .then(res => {
        const { data } = res;

        if (!data.error_text && data.checks[0].status === 'VALID') {
          console.log('valid, allowing access');
          redirect('/secret');
        } else {
          console.log('invalid, redirecting...');
          redirect('/');
        }
      })
      .catch(err => console.log(err));
  } else {
    redirect('/');
  }
}

Este archivo será llamado cada vez que /secret como ruta pero antes de que el HTML sea renderizado y enviado al navegador. Por encima, los siguientes pasos tienen lugar:

  • El almacén VueX se comprueba mediante un método getter para ver si ya se ha devuelto un Token Web JSON desde nuestra API de verificación

  • En caso afirmativo, lo pasamos al /auth-check endpoint para ver si esa autenticación sigue activa

  • Si no existe, o la autenticación no es válida, redirigimos la petición de vuelta al formulario de acceso.

Si su servidor de desarrollo sigue funcionando en este momento, reinícielo para que el middleware se registre correctamente y repita el proceso de inicio de sesión de nuevo. Esta vez, una verificación correcta debería mostrarle la página /secret página

(La página secreta que creé para la aplicación de ejemplo es más divertida que ésta. Puedes conseguirla aquí si quieres usarla).

Conclusión

Nuxt nos permite tomar una tecnología que ya conozcamos, como Vue, y añadir elementos adicionales potentes, como la autenticación a través de middleware sin tener que desviarnos para trabajar en API separadas en servidores diferentes.

Obviamente, hay límites en cuanto a la cantidad de cosas que querrías empaquetar en el Middleware del Lado Servidor antes de dar el paso a la construcción de una API externa sería la mejor opción. Depende de ti, pero yo diría que más de 5 puntos finales básicos podrían justificar el esfuerzo. Cualquier cosa menos, considerar la construcción como middleware - especialmente si es sólo un proxy para las llamadas a otra API como este ejemplo está haciendo.

Espero que esto ha proporcionado una buena idea de lo que se puede hacer, y saber que este enfoque funciona para una gran cantidad de las API Nexmo, no sólo Verify. Usted podría hacer fácilmente su Middleware del lado del servidor enviar mensajes SMS en lugar de verificar los usuarios.

Sé creativo y si se te ocurren otros ejemplos, no dudes en compartirlos con nosotros a través de nuestro canal Slack de la Comunidad Nexmo.

Compartir:

https://a.storyblok.com/f/270183/250x250/d0444194cd/martyn.png
Martyn DaviesAntiguos alumnos de Vonage

Antiguo Director de Educación para Desarrolladores en Vonage. Con experiencia como desarrollador creativo, gestor de productos y organizador de jornadas de hacking, Martyn lleva trabajando como defensor de la tecnología desde 2012, tras haber pasado anteriormente por el mundo de la radiodifusión y las grandes discográficas. Educa y capacita a desarrolladores de todo el mundo.