Autenticación de Node.js con Twilio Verify

En este tutorial, aprenderá cómo integrar la autenticación de dos factores en su aplicación Express.js. Creará una aplicación rápida que autentica a los usuarios mediante la autenticación tradicional basada en contraseña con una capa adicional de seguridad utilizando OTP impulsadas por el servicio Twilio Verify. Aprenderá cómo crear un backend rápido desde cero e implementar la autenticación mientras aprende sobre la arquitectura MVC. MongoDB es la base de datos elegida y la interfaz de usuario se crea utilizando EJS. Al final de este tutorial, tendrá una aplicación rápida completamente funcional que implementa OTP y contraseñas para la autenticación de usuarios.
Integrar la autenticación en una aplicación es una tarea tediosa. Sin embargo, asegurarse de que esta autenticación sea a prueba de balas es aún más difícil. Como desarrolladores, está fuera de nuestro control lo que los usuarios hacen con sus contraseñas, cómo las protegen, a quién se las entregan o cómo las generan. Todo lo que podemos hacer es acercarnos lo suficiente para asegurarnos de que la solicitud de autenticación fue realizada por nuestro usuario y no por otra persona. Las OTP ciertamente ayudan con eso, y servicios como Twilio Verify nos ayudan a generar OTP seguras rápidamente sin tener que preocuparnos por la lógica.
¿Qué hay de malo con las contraseñas?
Los desarrolladores enfrentan varios problemas cuando utilizan únicamente la autenticación basada en contraseña, ya que presenta los siguientes problemas:
- Los usuarios pueden olvidar sus contraseñas y escribirlas (lo que las hace robables);
- Los usuarios pueden reutilizar contraseñas en todos los servicios (haciendo que todas sus cuentas sean vulnerables a una violación de datos);
- Los usuarios pueden utilizar contraseñas sencillas con fines de recuerdo, lo que las hace relativamente fáciles de piratear.
Ingrese OTP
Una contraseña de un solo uso ( OTP ) es una contraseña o PIN válido solo para una sesión o transacción de inicio de sesión. Dado que solo se puede usar una vez, estoy seguro de que ya puedes ver cómo el uso de OTP compensa las deficiencias de las contraseñas tradicionales.
Las OTP añaden una capa adicional de seguridad a las aplicaciones, que el sistema tradicional de autenticación de contraseñas no puede proporcionar. Las OTP se generan aleatoriamente y solo son válidas por un corto período de tiempo, lo que evita varias deficiencias asociadas con la autenticación tradicional basada en contraseña.
Las OTP se pueden utilizar para sustituir las contraseñas tradicionales o reforzar las contraseñas utilizando el enfoque de autenticación de dos factores (2FA). Básicamente, las OTP se pueden utilizar siempre que sea necesario garantizar la identidad de un usuario confiando en un medio de comunicación personal propiedad del usuario, como el teléfono, el correo, etc.
Este artículo está dirigido a desarrolladores que quieran obtener información sobre:
- Aprenda a crear una aplicación express.js de pila completa;
- Implementar autenticación con passport.js;
- Cómo Twilio Verify para la verificación de usuarios por teléfono.
Para lograr estos objetivos, crearemos una aplicación de pila completa utilizando node.js , express.js , EJS con autenticación realizada mediante pasaporte.js y rutas protegidas que requieren OTP para acceder.
Nota : Me gustaría mencionar que usaremos algunos paquetes de terceros (creados por otras personas) en nuestra aplicación. Esta es una práctica común, ya que no es necesario reinventar la rueda. ¿Podríamos crear nuestro propio servidor de nodos? Sí, claro. Sin embargo, ese tiempo podría dedicarse mejor a crear una lógica específica para nuestra aplicación.
Tabla de contenido
- Descripción básica de la autenticación en aplicaciones web;
- Construyendo un servidor Express;
- Integrar MongoDB en nuestra aplicación Express;
- Construyendo las vistas de nuestra aplicación usando el motor de plantillas EJS;
- Autenticación básica mediante un número de pasaporte;
- Usando Twilio Verify para proteger rutas.
Requisitos
- Nodo.js
- MongoDB
- Un editor de texto (por ejemplo, VS Code)
- Un navegador web (por ejemplo, Chrome, Firefox)
- Comprensión de HTML, CSS, JavaScript, Express.js
Aunque crearemos toda la aplicación desde cero, aquí está el repositorio de GitHub para el proyecto.
Descripción básica de la autenticación en aplicaciones web
¿Qué es la autenticación?
La autenticación es todo el proceso de identificar a un usuario y verificar que tiene una cuenta en nuestra aplicación.
No debe confundirse la autenticación con la autorización. Aunque trabajan de la mano, no hay autorización sin autenticación.
Dicho esto, veamos de qué se trata la autorización.
¿Qué es la autorización?
La autorización, en su forma más básica, tiene que ver con los permisos del usuario: lo que un usuario puede hacer en la aplicación. En otras palabras:
- Autenticación: ¿Quién eres?
- Autorización: ¿Qué puedes hacer?
La autenticación viene antes que la autorización.
No hay Autorización sin Autenticación.
La forma más común de autenticar a un usuario es mediante usernamey password.
Configurando nuestra aplicación
Para configurar nuestra aplicación, creamos nuestro directorio de proyecto:
mkdir authWithTwilioVerify
Construyendo un servidor Express
Usaremos Express.js para construir nuestro servidor.
¿Por qué necesitamos Express?
Construir un servidor Nodepuede resultar tedioso, pero los marcos nos facilitan las cosas. es el marco web Expressmás popular . NodeNos permite:
- Escribir controladores para solicitudes con diferentes
HTTPverbos en diferentesURLrutas (rutas); - Integre con
viewmotores de renderizado para generar respuestas insertando datos en plantillas; - Establezca configuraciones comunes de aplicaciones web, como las
portutilizadas para conectarse y la ubicación de las plantillas utilizadas para representar la respuesta; - Agregue procesamiento de solicitudes adicional
middlewareen cualquier punto dentro del proceso de manejo de solicitudes.
Además de todo esto, los desarrolladores han creado paquetes de middleware compatibles para abordar casi cualquier problema de desarrollo web.
En nuestro authWithTwilioVerifydirectorio, inicializamos un archivo package.jsonque contiene información sobre nuestro proyecto.
cd authWithTwilioVerifynpm init -y
De acuerdo con la arquitectura Model View Controller (MVC), tenemos que crear las siguientes carpetas en nuestro authWithTwilioVerifydirectorio:
mkdir public controllers views routes config models
Muchos desarrolladores tienen diferentes razones para usar la arquitectura MVC, pero para mí personalmente es porque:
- Fomenta la separación de preocupaciones;
- Ayuda a escribir código limpio;
- Proporciona una estructura a mi código base y, dado que otros desarrolladores lo usan, comprender el código base no será un problema.
Controllersel directorio alberga los controladores;Modelsel directorio contiene nuestros modelos de bases de datos;Publicel directorio contiene nuestros activos estáticos, por ejemplo, archivos CSS, imágenes, etc.;Viewsel directorio contiene las páginas que se mostrarán en el navegador;Routesdirectorio contiene las diferentes rutas de nuestra aplicación;ConfigEl directorio contiene información que es peculiar de nuestra aplicación.
Necesitamos instalar los siguientes paquetes para construir nuestra aplicación:
nodemonreinicia automáticamente nuestro servidor cuando realizamos cambios;expressnos brinda una interfaz agradable para manejar rutas;express-sessionnos permite manejar sesiones fácilmente en nuestra aplicación express;connect-flashnos permite mostrar mensajes a nuestros usuarios.
npm install nodemon -D
Agregue el siguiente script en el package.jsonarchivo para iniciar nuestro servidor usando nodemon.
"scripts": { "dev": "nodemon index" },
npm install express express-session connect-flash --save
Crea un index.jsarchivo y agrega los paquetes necesarios para nuestra aplicación.
Tenemos requirelos paquetes instalados en nuestro index.jsarchivo para que nuestra aplicación se ejecute bien y luego configuramos los paquetes de la siguiente manera:
const path = require('path')const express = require('express');const session = require('express-session')const flash = require('connect-flash')const port = process.env.PORT || 3000const app = express();app.use('/static', express.static(path.join(__dirname, 'public')))app.use(session({ secret: "please log me in", resave: true, saveUninitialized: true }));app.use(express.json())app.use(express.urlencoded({ extended: true }))// Connect flashapp.use(flash());// Global variablesapp.use(function(req, res, next) { res.locals.success_msg = req.flash('success_msg'); res.locals.error_msg = req.flash('error_msg'); res.locals.error = req.flash('error'); res.locals.user = req.user next();});//define error handlerapp.use(function(err, req, res, next) { res.render('error', { error : err })})//listen on portapp.listen(port, () = { console.log(`app is running on port ${port}`)});
Analicemos el segmento de código anterior.
Además de las requiredeclaraciones, utilizamos la app.use()función, que nos permite utilizar el nivel de aplicación middleware.
Las funciones de middleware son funciones que tienen acceso al objeto de solicitud, al objeto de respuesta y a la siguiente función de middleware en el ciclo de solicitud y respuesta de la aplicación.
La mayoría de los paquetes que tienen acceso al estado de nuestra aplicación (objetos de solicitud y respuesta) y pueden alterar esos estados se suelen utilizar como middleware. Básicamente, el middleware agrega funcionalidad a nuestra aplicación express.
Es como entregar el estado de la aplicación a la función del middleware, diciendo aquí está el estado, haz lo que quieras con él y llama la next()función al siguiente middleware.
Finalmente, le decimos a nuestro servidor de aplicaciones que escuche las solicitudes en el puerto 3000.
Luego en la terminal ejecuta:
npm run dev
Si lo ve app is running on port 3000en la terminal, significa que nuestra aplicación se está ejecutando correctamente.
Integrando MongoDB en nuestra aplicación Express
MongoDB almacena datos como documentos. Estos documentos se almacenan en MongoDB en formato JSON (JavaScript Object Notation). Como usamos Node.js, es bastante fácil convertir datos almacenados en MongoDB en objetos JavaScript y manipularlos.
Para instalar MongoDB en su máquina, visite la documentación de MongoDB .
Para integrar MongoDB en nuestra aplicación express, usaremos Mongoose . Mongoose es un ODM (que es el acrónimo de object data mapper).
Básicamente, Mongoose nos facilita el uso de MongoDB en nuestra aplicación al crear un contenedor alrededor de las funciones nativas de MongoDB.
npm install mongoose --save
En index.js, se requiere mongoose:
const mongoose = require('mongoose')const app = express()//connect to mongodbmongoose.connect('mongodb://localhost:27017/authWithTwilio', { useNewUrlParser: true, useUnifiedTopology: true }).then(() = { console.log(`connected to mongodb`)}).catch(e = console.log(e))
La mongoose.connect()función nos permite configurar una conexión a nuestra base de datos MongoDB utilizando la cadena de conexión.
El formato de la cadena de conexión es mongodb://localhost:27017/{database_name}.
mongodb://localhost:27017/es el host predeterminado de MongoDB y database_namees como queramos llamar a nuestra base de datos.
Mongoose se conecta a la base de datos llamada database_name. Si no existe, crea una base de datos database_namey se conecta a ella.
Mongoose.connect()es una promesa, por lo que siempre es una buena práctica registrar un mensaje en la consola en los métodos then()y catch()para informarnos si la conexión fue exitosa o no.
Creamos nuestro modelo de usuario en nuestro modelsdirectorio:
cd modelstouch user.js
user.jsrequiere mangosta y crea nuestro esquema de usuario:
const mongoose = require('mongoose');const userSchema = new mongoose.Schema({ name : { type: String, required: true }, username : { type: String, required: true }, password : { type: String, required: true }, phonenumber : { type: String, required: true }, email : { type: String, required: true }, verified: Boolean})module.exports = mongoose.model('user', userSchema)
A schemaproporciona una estructura para nuestros datos. Muestra cómo se deben estructurar los datos en la base de datos. Siguiendo el segmento de código anterior, especificamos que un objeto de usuario en la base de datos siempre debe tener name, username, password, phonenumbery email. Dado que esos campos son obligatorios, si los datos ingresados en la base de datos carecen de alguno de estos campos obligatorios, mongoose arroja un error.
Aunque puedes crear datos sin esquema en MongoDB, no es recomendable hacerlo; créeme, tus datos serían un desastre. Además, los esquemas son geniales. Le permiten dictar la estructura y la forma de los objetos en su base de datos. ¿Quién no querría esos poderes?
Cifrar contraseñas
Advertencia: nunca almacene las contraseñas de los usuarios como texto sin formato en su base de datos.
Cifre siempre las contraseñas antes de enviarlas a la base de datos.
La razón por la que necesitamos cifrar las contraseñas de los usuarios es la siguiente: en caso de que alguien de alguna manera obtenga acceso a nuestra base de datos, tenemos cierta seguridad de que las contraseñas de los usuarios son seguras, porque lo único que esta persona vería sería un archivo hash. Esto proporciona cierto nivel de garantía de seguridad, pero un pirata informático sofisticado aún puede descifrarlo hashsi tiene las herramientas adecuadas. De ahí la necesidad de OTP, pero centrémonos en cifrar las contraseñas de los usuarios por ahora.
bcryptjsproporciona una forma de cifrar y descifrar las contraseñas de los usuarios.
npm install bcryptjs
En models/user.js, se requiere bcryptjs:
//after requiring mongooseconst bcrypt = require('bcryptjs')//before module.exports//hash password on saveuserSchema.pre('save', async function() { return new Promise( async (resolve, reject) = { await bcrypt.genSalt(10, async (err, salt) = { await bcrypt.hash(this.password, salt, async (err, hash) = { if(err) { reject (err) } else { resolve (this.password = hash) } }); }); })})userSchema.methods.validPassword = async function(password) { return new Promise((resolve, reject) = { bcrypt.compare(password, this.password, (err, res) = { if(err) { reject (err) } resolve (res) }); })}
El código anterior hace un par de cosas. Veámoslos.
Es userSchema.pre('save', callback)una mongoose hookque nos permite manipular datos antes de guardarlos en la base de datos. En el callback function, devolvemos una promesa que intenta obtener hash(encrypt) bcrypt.hash()la contraseña utilizando la que bcrypt.genSalt()generamos. Si se produce un error durante esto hashing, nosotros rejecto nosotros resolveconfigurando this.password = hash. this.passwordsiendo el userSchema password.
A continuación, mongoosenos proporciona una manera de agregar métodos a esquemas usando el archivo schema.methods.method_name. En nuestro caso, estamos creando un método que nos permite validar las contraseñas de los usuarios. Al asignar un valor de función a *userSchema.methods.validPassword*, podemos usar fácilmente el método de comparación bcryptjs bcryprt.compare()para verificar si la contraseña es correcta o no.
bcrypt.compare()toma dos argumentos y una devolución de llamada. La passwordes la contraseña que se pasa al llamar a la función, mientras que this.passwordes la de userSchema.
Prefiero este método de validar la contraseña de los usuarios porque es como una propiedad del objeto de usuario. Uno podría llamar fácilmente
User.validPassword(password)y obtener respuesta verdadera o falsa.
Con suerte, podrás ver la utilidad de la mangosta. Además de crear un esquema que da estructura a los objetos de nuestra base de datos, también proporciona buenos métodos para manipular esos objetos; eso habría sido de otra manera si se hubiera usado solo MongoDB nativo.
Express es para Node, como Mongoose es para MongoDB.
Construyendo las vistas de nuestra aplicación utilizando el motor de plantillas EJS
Antes de comenzar a crear las vistas de nuestra aplicación, echemos un vistazo a la arquitectura front-end de nuestra aplicación.
Arquitectura frontal
EJSes un motor de plantillas que funciona directamente con Express. No es necesario un marco de interfaz de usuario diferente. EJShace que la transferencia de datos sea muy fácil. También hace que sea más fácil realizar un seguimiento de lo que sucede, ya que no es necesario cambiar del back-end al front-end.
Tendremos un viewsdirectorio que contendrá los archivos que se mostrarán en el navegador. Todo lo que tenemos que hacer es llamar al res.render()método desde nuestro controlador. Por ejemplo, si deseamos representar la página de inicio de sesión, es tan simple como res.render('login'). También podríamos pasar datos a las vistas agregando un argumento adicional, que es un objeto del render()método, como res.render('dashboard', { user }). Luego, en nuestro view, podríamos mostrar los datos con el evaluation syntax %= %. Todo lo que tiene esta etiqueta se evalúa; por ejemplo, %= user.username %muestra el valor de la propiedad de nombre de usuario del objeto de usuario. Además de la sintaxis de evaluación, EJStambién proporciona una sintaxis de control ( % %), que nos permite escribir declaraciones de control del programa como condicionales, bucles, etc.
Básicamente, EJSnos permite incrustar JavaScript en nuestro HTML.
npm install ejs express-ejs-layouts --save
En index.js, se requiere express-ejs-layouts:
//after requiring connect-flashconst expressLayouts = require('express-ejs-layouts')//after the mongoose.connect logicapp.use(expressLayouts);app.set('view engine', 'ejs');
Entonces:
cd viewstouch layout.ejs
En views/layout.ejs,
!DOCTYPE htmlhtml head meta charset="UTF-8" / meta name="viewport" content="width=device-width, initial-scale=1.0" / meta http-equiv="X-UA-Compatible" content="ie=edge" / link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous" link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" link rel="stylesheet" href="/static/css/app.css" link rel="stylesheet" href="/static/css/intlTelInput.css" titleNode js authentication/title /head body div %- body % /div script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous" /script script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"/script /body/html
El layout.ejsarchivo sirve como un index.htmlarchivo, donde podemos incluir todos nuestros scripts y hojas de estilo. Luego, en divwith clases ui container, representamos bodyel resto de las vistas de nuestra aplicación.
Usaremos la interfaz de usuario semántica como nuestro marco CSS.
Construyendo los parciales
Los parciales son donde almacenamos código reutilizable, para no tener que reescribirlos cada vez. Lo único que hacemos es incluirlos allí donde sean necesarios.
Se podría pensar en los parciales como componentes en los marcos de front-end: fomentan el código DRY y también la reutilización del código. Piense en los parciales como una versión anterior de los componentes.
Por ejemplo, queremos parciales para nuestro menú, de modo que no tengamos que escribir código cada vez que necesitemos el menú en nuestra página.
cd viewsmkdir partials
Crearemos dos archivos en la /views/partialscarpeta:
cd partialstouch menu.ejs message.ejs
En menu.ejs,
div a href="/" Home /a % if(locals.user) { % a href="/users/dashboard" dashboard /a div a class='ui item' %= user.username % /a a href="/users/logout" Logout /a /div % } else {% div a href="/users/signup" Sign Up /a a href="/users/login" Login /a /div % } % /div
En message.ejs,
% if(typeof errors != 'undefined'){ % % errors.forEach(function(error) { % div i/i div User registration unsuccessful /div %= error.msg % /div% }); % % } % % if(success_msg != ''){ %div i/i div Your user registration was successful. /div %= success_msg %/div% } % % if(error_msg != ''){ %div i/i div /div %= error_msg %/div% } % % if(error != ''){ %div i/i div /div %= error %/div% } %
Creación de la página del panel
En nuestra carpeta de vistas, creamos un dashboard.ejsarchivo:
%- include('./partials/menu') %h1 DashBoard/h1
Aquí incluimos el menu partialspara que tengamos el menú en la página.
Construyendo la página de error
En nuestra carpeta de vistas, creamos un error.ejsarchivo:
h1Error Page/h1p%= error %/p
Construyendo la página de inicio
En nuestra carpeta de vistas, creamos un home.ejsarchivo:
%- include('./partials/menu') %h1 Welcome to the Home Page/h1
Construyendo la página de inicio de sesión
En nuestra carpeta de vistas, creamos un login.ejsarchivo:
div %- include ('./partials/message') % h3 Login Form /h3 form action="/users/login" method="POST" div labelEmail/label input type="email" name="email" placeholder="Email address" /div div labelPassword/label input type="password" name="password" placeholder="Password" /div button type="submit"Login/button /form/div
Construyendo la página de verificación
En nuestra carpeta de vistas, creamos un login.ejsarchivo:
%- include ('./partials/message') %h1Verify page/h1pplease verify your account/pform action="/users/verify" method="POST" div labelverification code/label input type="text" type="number" name="verifyCode" placeholder="code" /div button type="submit"Verify/button/formbra href="/users/resend"Resend Code/a
Aquí, proporcionamos un formulario para que los usuarios ingresen el código de verificación que se les enviará.
Creación de la página de registro
Necesitamos obtener el número de móvil del usuario y todos sabemos que los códigos de país difieren de un país a otro. Por lo tanto, usaremos el [intl-tel-input](https://intl-tel-input.com/)para ayudarnos con los códigos de país y la validación de números de teléfono.
npm install intl-tel-input
-
En nuestra carpeta pública, creamos un
cssdirectorio,jsdirectorio yimgdirectorio:cd publicmkdir css js img -
Copiamos el
intlTelInput.cssarchivo delnode_modulesintl-tel-inputbuildcssarchivo a nuestropublic/cssdirectorio. -
Copiamos tanto la carpeta
intlTelInput.jscomoutils.jsla denode_modulesintl-tel-inputbuildjsen nuestropublic/jsdirectorio. -
Copiamos tanto la carpeta
flags.pngcomoflags@2x.pngla denode_modulesintl-tel-inputbuildimgen nuestropublic/imgdirectorio.
Creamos un app.css en nuestra public/csscarpeta:
cd publictouch app.css
En app.css, agregue los estilos a continuación:
.iti__flag {background-image: url("/static/img/flags.png");} @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .iti__flag {background-image: url("/static/img/flags@2x.png");}}.hide { display: none}.error { color: red; outline: 1px solid red;}.success{ color: green;}
Finally, we create a signup.ejs file in our views folder:
div %- include ('./partials/message') % h3 Signup Form /h3 form action="/users/signup" method="POST" div labelName/label input type="text" name="name" placeholder="name" /div div labelUsername/label input type="text" name="username" placeholder="username" /div div labelPassword/label input type="password" name="password" placeholder="Password" /div div labelPhone number/label input type="tel" id='phone' span✓ Valid/span span/span /div div labelEmail/label input type="email" name="email" placeholder="Email address" /div button type="submit"Sign up/button /form/divscript src="/static/js/intlTelInput.js"/scriptscript const input = document.querySelector("#phone") const errorMsg = document.querySelector("#error-msg") const validMsg = document.querySelector("#valid-msg") const errorMap = ["Invalid number", "Invalid country code", "Too short", "Too long", "Invalid number"]; const iti = window.intlTelInput(input, { separateDialCode: true, autoPlaceholder: "aggressive", hiddenInput: "phonenumber", utilsScript: "/static/js/utils.js?1590403638580" // just for formatting/placeholders etc }); var reset = function() { input.classList.remove("error"); errorMsg.innerHTML = ""; errorMsg.classList.add("hide"); validMsg.classList.add("hide"); }; // on blur: validate input.addEventListener('blur', function() { reset(); if (input.value.trim()) { if (iti.isValidNumber()) { validMsg.classList.remove("hide"); } else { input.classList.add("error"); var errorCode = iti.getValidationError(); errorMsg.innerHTML = errorMap[errorCode]; errorMsg.classList.remove("hide"); } } }); // on keyup / change flag: reset input.addEventListener('change', reset); input.addEventListener('keyup', reset); document.querySele

Deja un comentario