https://d226lax1qjow5r.cloudfront.net/blog/blogposts/how-to-add-two-factor-authentication-with-node-js-dr/Blog_Node-js_Verify_1200x600.png

Cómo añadir autenticación de dos factores con Node.js y Koa.js

Publicado el November 5, 2020

Tiempo de lectura: 14 minutos

La autenticación de dos factores (2FA) recibe su nombre del hecho de que necesitas dos cosas para verificar tu identidad. Algo que sabes, como una contraseña, y algo que tienes, como el código de verificación de tu dispositivo móvil o token físico.

Agregar 2FA a tu aplicación no tiene que ser una tarea difícil. Este tutorial cubrirá cómo implementar 2FA para tus aplicaciones y servicios web para una capa adicional de seguridad con la ayuda de Vonage Verify API. Construiremos una simple aplicación Koa.js para entender cómo funciona el mecanismo subyacente. Esto hará más fácil ver cómo esto encajará en tus propios proyectos existentes, incluso si no estás usando Koa.js.

Este tutorial cubrirá cómo implementar un sistema de token de verificación con Vonage Verify API y Koa.js. Tenemos un tutorial similar en Node.js usando Express.js - puedes encontrarlo aquí.

Empezarás con una página de inicio de sesión en la que se pedirá al usuario un número de teléfono móvil. Una vez introducido, se le pedirá que introduzca un código de verificación que se enviará a su número de teléfono móvil por SMS. Una vez hecho esto, podrán acceder a la aplicación.

Requisitos previos

  • Conocimientos básicos de Javascript

  • Node.js instalado en su máquina

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.

Una vez que tengas una cuenta de API de Vonage, podrás encontrar tu clave y secreto de API en la parte superior del Panel de API de Vonage.

Este tutorial te llevará a través del proceso desde cero. Si quieres ver el código terminado, puedes clonar el repositorio repositorio git de este proyecto. También tenemos una versión Glitch, que tiene un diseño más exagerado, y que puedes remezclar también. Tenga en cuenta que son ligeras diferencias para la implementación Glitch para atender a cómo los proyectos están alojados en la plataforma.

Glitch version of demoGlitch version of demo

Empezar un proyecto Koa.js desde cero

Crea una carpeta de proyecto en tu máquina local y ejecuta el siguiente comando para configurar un nuevo proyecto Node.js.

npm init

Esto activará una serie de preguntas que generarán su archivo package.json archivo. Si lo desea, puede dejar las respuestas en blanco para utilizar los valores por defecto.

Configuring package.jsonConfiguring package.json

A continuación, instale Koa.js. Ten en cuenta que Koa requiere node v7.6.0 o superior para ES2015 y soporte de funciones async.

npm install koa --save

Cree un server.js en la carpeta del proyecto.

touch server.js

Pegue el siguiente código en el archivo recién creado.

const Koa = require('koa')
const port = process.env.PORT || 3000
const app = new Koa()

app.use(async ctx => {
  ctx.body = 'Hello Unicorn 🦄'
})

const listener = app.listen(port, function() {
  console.log('Your app is listening on port ' + listener.address().port)
})

Ejecute el server.js archivo.

node server.js

Si navega a http://localhost:3000 desde tu navegador, deberías ver una página vacía con el texto "Hola Unicornio 🦄".

Check that server is runningCheck that server is running

También debe instalar dotenvque le permite cargar variables de entorno almacenadas en un archivo .env en un archivo process.env.

npm install dotenv --save

Y ahora puede crear el archivo .env y debe contener al menos las siguientes variables:

NEXMO_API_KEY='' NEXMO_API_SECRET=''

Para acceder a las variables de entorno, tendrás que requerirlo, idealmente en la parte superior de tu archivo server.js archivo.

require('dotenv').config()

Si no se ha cuenta en Nexmo ahora es un buen momento para hacerlo. Una vez que haya iniciado sesión en el panel de control, sus credenciales de la API debe ser la primera cosa que usted ve. Asegúrate de escribir la clave y el secreto entre comillas.

Estructura del proyecto

En este momento, su proyecto probablemente sólo tendría un package.json, a server.js y un archivo .env archivo. Vamos a configurar la estructura del proyecto para que puedas tener un frontend básico con el que los usuarios puedan interactuar.

PROJECT_NAME/               
    |-- public/             
    |   |-- client.js
    |   `-- style.css
    |-- views/
    |   `-- index.html
    |-- .env
    |-- package.json
    `-- server.js

Con eso, tendrás que hacer algunos ajustes en el archivo server.js para servir el archivo index.html y los activos relacionados, en lugar de simplemente una línea de texto. Koa.js es un framework bastante básico, por lo que cualquier funcionalidad adicional para enrutar o servir activos estáticos necesita ser instalada por separado. Aquí está la lista de módulos adicionales y sus usos:

  • koa-static para servir activos estáticos

  • koa-bodyparser para gestionar los datos enviados a través de peticiones POST

  • koa-router para el enrutamiento

  • koa-views para renderizar plantillas

Este ejemplo también utiliza Nunjucks para generar archivos de plantilla. La API Verify de Vonage se utilizará para activar el código de verificación a través de SMS, por lo que también deberás instalar la biblioteca cliente Node.js de Vonage.

npm install koa-static koa-bodyparser koa-router koa-views nunjucks nexmo --save

Servir activos estáticos y archivos HTML

Para permitir que la aplicación sirva activos estáticos, como hojas de estilo y JavaScript del lado del cliente, fuera del directorio /público puede añadir lo siguiente al archivo server.js archivo:

const serve = require('koa-static')
app.use(serve('./public'))

Para servir archivos HTML desde el directorio /vistas puede utilizar koa-viewsque proporciona una función render() función. El motor de plantillas utilizado en este ejemplo es Nunjucks, pero usted es libre de elegir cualquier motor de plantillas que funcione mejor para usted.

const views = require('koa-views')
app.use(views('./views', { map: { html: 'nunjucks' }}))

Lo siguiente a configurar serían algunas rutas básicas para servir las páginas de su aplicación.

const Router = require('koa-router')
const router = new Router()

router.get('/', (ctx, next) => {
  return ctx.render('./index')
})

app.use(router.routes()).use(router.allowedMethods())

Para este ejemplo, necesitará 3 páginas, la index.html como página de destino principal, verify.html para que los usuarios introduzcan su código de verificación y result.html para mostrar si la verificación se ha realizado correctamente o no.

La estructura del formulario web es bastante sencilla, y usted es libre de adornarlo con CSS como desee.

<form method="post" action="verify">
  <input name="phone" type="tel" placeholder="+6588888888">
  <button>Get OTP</button>
</form>

Este formulario enviará las entradas del usuario a la ruta /verify y puede utilizar el número de teléfono en la entrada para activar la solicitud de código de verificación. Un formulario similar se puede utilizar para las otras 2 rutas para /check y /cancel también.

<form method="post" action="check">
  <input name="pin" placeholder="Enter PIN">
  <input name="reqId" type="hidden" value="{{ reqId }}">
  <button>Verify</button>
</form>
<form method="post" action="cancel">
  <input name="reqId" type="hidden" value="{{ reqId }}">
  <button class="inline">Cancel verification</button>
</form>

Tratamiento de las entradas de usuario

Luego, para manejar las entradas del usuario a través de formularios web, necesitará algunas rutas para manejar POST también. Asegúrese de declarar bodyparser() antes de cualquiera de las rutas.

const bodyParser = require('koa-bodyparser')

/* This should appear before any routes */
app.use(bodyParser())

router.post('/verify/', async (ctx, next) => {
  const payload = await ctx.request.body
  /* Function to trigger verification code here */
})

router.post('/check/', async (ctx, next) => {
  const payload = await ctx.request.body
  /* Function to check verification code here */
})

router.post('/cancel/', async (ctx, next) => {
  const payload = await ctx.request.body
  /* Function to cancel verification code here */
})

Ahora que puedes recibir el número de teléfono de tu usuario, necesitarás usar Verify API para enviarle un código PIN. Inicializa una nueva instancia de Nexmo con tus credenciales de API de Vonage.

const Nexmo = require('nexmo');
const nexmo = new Nexmo({
  apiKey: YOUR_API_KEY,
  apiSecret: YOUR_API_SECRET
});

Hay 3 funciones de las que debemos ocuparnos. La primera es activar el código de verificación con la función nexmo.verify.request() función. Implica el número de teléfono del usuario, y una cadena para el nombre de la marca que se mostrará al usuario como remitente.

async function verify(number) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.request({
      number: number,
      brand: process.env.NEXMO_BRAND_NAME
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

Una vez que el usuario haya recibido el código PIN por SMS, deberá enviarlo a la función nexmo.verify.check() para que pueda ser verificado. Observará un parámetro request_id parámetro. Este valor se obtiene cuando el código PIN se ha activado correctamente. Hay varias formas de pasar el ID de la solicitud a la función nexmo.verify.check() y este ejemplo utiliza un campo oculto en la función comprobar formulario.

async function check(reqId, code) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.check({
      request_id: reqId,
      code: code
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

La última función ofrece al usuario la opción de cancelar la verificación si ha cambiado de opinión. Utiliza la función nexmo.verify.control() y, de nuevo, requiere el ID de solicitud generado al activar el código PIN y un valor de cadena de cancel.

async function cancel(reqId) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.control({
      request_id: reqId,
      cmd: 'cancel'
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}

Landing page for demoLanding page for demo

Ahora necesitas hacer uso de estas 3 funciones en las rutas que especificamos anteriormente, comenzando primero con la que activa el código de verificación.

router.post('/verify/', async (ctx, next) => {
  const payload = await ctx.request.body
  const phone = payload.phone

  const result = await verify(phone)
  const reqId = result.request_id 
  ctx.status = 200
  return ctx.render('./verify', { reqId: reqId })
})

La página ctx.request.body será algo parecido a esto:

{ 
  "phone": "+40987654321"
}

Puedes coger ese número de teléfono y pasarlo a la función verify() función. Siempre que sea un número de teléfono válido, se disparará el código de verificación y recibirás una respuesta que contendrá un icono request_id y status.

{ 
  "request_id": "1bf002ecd1e94d8aa81ba7463b19f583",
  "status": "0"
}

A partir de ahí, puede enviar el ID de solicitud al frontend para que lo utilice cuando el usuario introduzca el código de verificación.

The request_id is passed to the frontendThe request_id is passed to the frontend

Cuando el usuario introduzca el PIN correcto, deberá introducir tanto el PIN como el ID de la solicitud en la función check() función

router.post('/check/', async (ctx, next) => {
  const payload = await ctx.request.body
  const code = payload.pin
  const reqId = payload.reqId
  
  const result = await check(reqId, code)
  const status = result.status
  ctx.status = 200
  return ctx.render('./result', { status: status })
})

De nuevo, ambos valores se pueden obtener del ctx.request.body y si se valida que el PIN es correcto, recibirá una respuesta parecida a ésta:

{ 
  "request_id": "1bf002ecd1e94d8aa81ba7463b19f583",
  "status": "0",
  "event_id": "150000001AC57AB2",
  "price": "0.10000000",
  "currency": "EUR" 
}

A continuación, puede utilizar el código de estado para determinar el mensaje que desea mostrar al usuario. Este ejemplo utiliza Nunjucks, por lo que el marcado en la página de resultados podría ser algo como esto:

{% if status == 0 %}
<p>Code verified successfully. ¯\_(ツ)_/¯</p>
{% else %}
<p>Something went wrong… ಠ_ಠ</p>
<p>Please contact the administrator for more information.</p>
{% endif %}

Verification messagesVerification messages

Este ha sido un desglose exhaustivo de cada parte del código, pero para ver el aspecto de la aplicación en su totalidad, echa un vistazo al código fuente en GitHub.

Otras cosas que hay que tener en cuenta

Este tutorial es una versión simplificada, destacando sólo los bits necesarios para implementar la autenticación de dos factores. Pero hay numerosas cosas de las que hay que ocuparse en una aplicación real. Una de las más importantes es la gestión de errores. La Verify API devuelve un valor de estado de 0 para las consultas correctas, pero cualquier otro valor indica un error.

Estos errores deben ser manejados y la interfaz de usuario en el frontend debe reflejar cualquier error potencial que impida una verificación exitosa. También podría ser una buena idea implementar algún tipo de validación de frontend, o incluso utilizar la Number Insight API de Vonage de Vonage para garantizar que sólo se pasen números de teléfono válidos a Verify API.

¿Y ahora qué?

Si quieres saber más sobre estas API, aquí tienes algunos enlaces que pueden resultarte útiles:

Compartir:

https://a.storyblok.com/f/270183/384x384/46621147f0/huijing.png
Hui Jing ChenAntiguos alumnos de Vonage

Hui Jing es defensora de los desarrolladores en Nexmo. Tiene un amor desmesurado por CSS y la tipografía, y en general es una apasionada de todo lo relacionado con la web.