Creación de flujos de contraseñas seguros con NodeJS y MySQL

 

 

 


Índice
  1. Consejo de seguridad n.º 1
  • Instalación
    1. Consejo de seguridad n.º 2
  • Configuración de la base de datos
    1. Consejo de seguridad n.º 3
  • Configurar ruta para restablecer contraseña
    1. Consejos de seguridad n.º 4 a 6
  • Configurar la ruta "Restablecer contraseña"
    1. Consejo de seguridad n.º 7:
    2. Consejo de seguridad n.º 8 al 11:
  • Agregue el enlace a su página de inicio de sesión
  • Próximos pasos
  • La funcionalidad de restablecer contraseña es fundamental para cualquier aplicación fácil de usar. También puede ser una pesadilla para la seguridad. Utilizando NodeJS y MySQL, Darshan demuestra cómo crear con éxito un flujo de restablecimiento de contraseña seguro para evitar estos errores.

     

    Si eres como yo, has olvidado tu contraseña más de una vez, especialmente en sitios que no has visitado por un tiempo. Probablemente también haya visto, y/o se haya sentido mortificado por, correos electrónicos de restablecimiento de contraseña que contienen su contraseña en texto sin formato.

    Desafortunadamente, el flujo de trabajo para restablecer la contraseña recibe poca atención y atención limitada durante el desarrollo de la aplicación. Esto no sólo puede generar una experiencia de usuario frustrante, sino que también puede dejar su aplicación con enormes agujeros de seguridad.

    Vamos a cubrir cómo crear un flujo de trabajo seguro para restablecer la contraseña. Usaremos NodeJS y MySQL como nuestros componentes base. Si escribe utilizando un lenguaje, marco o base de datos diferente, aún puede beneficiarse al seguir los "Consejos de seguridad" generales que se describen en cada sección.

     

    Un flujo de restablecimiento de contraseña consta de los siguientes componentes:

    • Un enlace para enviar al usuario al inicio del flujo de trabajo.
    • Un formulario que permite al usuario enviar su correo electrónico.
    • Una búsqueda que valida el correo electrónico y envía un correo electrónico a la dirección.
    • Un correo electrónico que contiene el token de restablecimiento con vencimiento que permite al usuario restablecer su contraseña.
    • Un formulario que permite al usuario generar una nueva contraseña.
    • Guardar la nueva contraseña y permitir que el usuario inicie sesión nuevamente con la nueva contraseña.

    Además de Node, Express y MySQL, usaremos las siguientes bibliotecas:

    • Secuelizar ORM
    • Nodemailer

    Sequelize es un ORM de base de datos NodeJS que facilita la ejecución de migraciones de bases de datos y consultas de creación de seguridad. Nodemailer es una biblioteca de correo electrónico NodeJS popular que usaremos para enviar correos electrónicos de restablecimiento de contraseña.

    Consejo de seguridad n.º 1

    Algunos artículos sugieren que se pueden diseñar flujos de contraseñas seguros utilizando JSON Web Tokens (JWT), que eliminan la necesidad de almacenamiento en la base de datos (y, por lo tanto, son más fáciles de implementar). No utilizamos este enfoque en nuestro sitio porque los secretos de los tokens JWT generalmente se almacenan directamente en el código. Queremos evitar tener "un secreto" para controlarlos a todos (por la misma razón que no se agregan contraseñas con el mismo valor) y, por lo tanto, necesitamos mover esta información a una base de datos.

    Instalación

    Primero, instale Sequelize, Nodemailer y otras bibliotecas asociadas:

    $ npm install --save sequelize sequelize-cli mysql crypto nodemailer

    En la ruta donde desea incluir sus flujos de trabajo de restablecimiento, agregue los módulos necesarios. Si necesita un repaso sobre Express y rutas, consulte su guía .

    const nodemailer = require('nodemailer');

    Y configúrelo con sus credenciales SMTP de correo electrónico.

    const transport = nodemailer.createTransport({ host: process.env.EMAIL_HOST, port: process.env.EMAIL_PORT, secure: true, auth: { user: process.env.EMAIL_USER, pass: process.env.EMAIL_PASS }});

    La solución de correo electrónico que estoy usando es el Servicio de correo electrónico simple de AWS , pero puedes usar cualquier cosa (Mailgun, etc.).

    Si es la primera vez que configura su servicio de envío de correo electrónico, deberá dedicar algo de tiempo a configurar las claves de dominio adecuadas y las autorizaciones. Si usas la Ruta 53 junto con SES, esto es súper simple y se hace prácticamente de forma automática, por eso lo elegí. AWS tiene algunos tutoriales sobre cómo funciona SES con Route53 .

     

    Consejo de seguridad n.º 2

    Para almacenar las credenciales fuera de mi código, uso dotenv , que me permite crear un archivo .env local con mis variables de entorno. De esa manera, cuando implemento en producción, puedo usar diferentes claves de producción que no son visibles en el código y, por lo tanto, me permiten restringir los permisos de mi configuración solo a ciertos miembros de mi equipo.

    Configuración de la base de datos

    Dado que enviaremos tokens de reinicio a los usuarios, debemos almacenar esos tokens en una base de datos.

    Supongo que tiene una tabla de usuarios en funcionamiento en su base de datos. Si ya estás usando Sequelize, ¡genial! De lo contrario, es posible que desee repasar Sequelize y Sequelize CLI .

    Si aún no ha utilizado Sequelize en su aplicación, puede configurarlo ejecutando el siguiente comando en la carpeta raíz de su aplicación:

    $ sequelize init

    Esto creará una serie de carpetas nuevas en su configuración, incluidas migraciones y modelos.

    Esto también creará un archivo de configuración. En su archivo de configuración, actualice el developmentbloque con las credenciales de su servidor de base de datos MySQL local.

    Utilicemos la herramienta CLI de Sequelize para generar la tabla de la base de datos.

    $ sequelize model:create --name ResetToken --attributes email:string,token:string,expiration:date,used:integer$ sequelize db:migrate

    Esta tabla tiene las siguientes columnas:

    • Dirección de correo electrónico del usuario,
    • Token que se ha generado,
    • Caducidad de ese token,
    • Si el token se ha utilizado o no.

    En segundo plano, sequelize-cli ejecuta la siguiente consulta SQL:

    CREATE TABLE `ResetTokens` ( `id` int(11) NOT NULL AUTO_INCREMENT, `email` varchar(255) DEFAULT NULL, `token` varchar(255) DEFAULT NULL, `expiration` datetime DEFAULT NULL, `createdAt` datetime NOT NULL, `updatedAt` datetime NOT NULL, `used` int(11) NOT NULL DEFAULT '0', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

    Verifique que esto funcionó correctamente usando su cliente SQL o la línea de comando:

    mysql describe ResetTokens;+------------+--------------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+------------+--------------+------+-----+---------+----------------+| id | int(11) | NO | PRI | NULL | auto_increment || email | varchar(255) | YES | | NULL | || token | varchar(255) | YES | | NULL | || expiration | datetime | YES | | NULL | || createdAt | datetime | NO | | NULL | || updatedAt | datetime | NO | | NULL | || used | int(11) | NO | | 0 | |+------------+--------------+------+-----+---------+----------------+7 rows in set (0.00 sec)

    Consejo de seguridad n.º 3

    Si actualmente no estás utilizando un ORM, deberías considerar hacerlo. Un ORM automatiza la escritura y el escape adecuado de consultas SQL, lo que hace que su código sea más legible y seguro de forma predeterminada. Le ayudarán a evitar ataques de inyección SQL escapando adecuadamente de sus consultas SQL.

     

    Configurar ruta para restablecer contraseña

    Cree la ruta de obtención en user.js :

    router.get('/forgot-password', function(req, res, next) { res.render('user/forgot-password', { });});

    Luego cree la ruta POST, que es la ruta que se accede cuando se publica el formulario de restablecimiento de contraseña. En el código siguiente, he incluido un par de características de seguridad importantes.

    Consejos de seguridad n.º 4 a 6

    1. Incluso si no encontramos una dirección de correo electrónico, devolvemos "ok" como nuestro estado. No queremos que robots indeseados descubran qué correos electrónicos son reales y qué no reales en nuestra base de datos.
    2. Cuantos más bytes aleatorios uses en un token, es menos probable que pueda ser pirateado. Estamos usando 64 bytes aleatorios en nuestro generador de tokens ( no use menos de 8 ).
    3. Caducar el token en 1 hora. Esto limita el período de tiempo en que funciona el token de reinicio.
    router.post('/forgot-password', async function(req, res, next) { //ensure that you have a user with this email var email = await User.findOne({where: { email: req.body.email }}); if (email == null) { /** * we don't want to tell attackers that an * email doesn't exist, because that will let * them use this form to find ones that do * exist. **/ return res.json({status: 'ok'}); } /** * Expire any tokens that were previously * set for this user. That prevents old tokens * from being used. **/ await ResetToken.update({ used: 1 }, { where: { email: req.body.email } }); //Create a random reset token var fpSalt = crypto.randomBytes(64).toString('base64'); //token expires after one hour var expireDate = new Date(new Date().getTime() + (60 * 60 * 1000)) //insert token data into DB await ResetToken.create({ email: req.body.email, expiration: expireDate, token: fpSalt, used: 0 }); //create email const message = { from: process.env.SENDER_ADDRESS, to: req.body.email, replyTo: process.env.REPLYTO_ADDRESS, subject: process.env.FORGOT_PASS_SUBJECT_LINE, text: 'To reset your password, please click the link below.nnhttps://'+process.env.DOMAIN+'/user/reset-password?token='+encodeURIComponent(token)+'email='+req.body.email }; //send email transport.sendMail(message, function (err, info) { if(err) { console.log(err)} else { console.log(info); } }); return res.json({status: 'ok'});});

    Verá una variable de usuario a la que se hace referencia anteriormente: ¿qué es esto? Para los propósitos de este tutorial, asumimos que tiene un modelo de usuario que se conecta a su base de datos para recuperar valores. El código anterior se basa en Sequelize, pero puede modificarlo según sea necesario si consulta la base de datos directamente (¡pero recomiendo Sequelize!). Aviation Questions and Answers

    Ahora necesitamos generar la vista. Usando Bootstrap CSS , jQuery y el marco pug integrado en el marco Node Express, la vista se parece a la siguiente:

    extends ../layout block content div.container div.row div.col h1 Forgot password p Enter your email address below. If we have it on file, we will send you a reset email. div.forgot-message.alert.alert-success(style="display:none;") Email address received. If you have an email on file we will send you a reset email. Please wait a few minutes and check your spam folder if you don't see it. form#forgotPasswordForm.form-inline(onsubmit="return false;") div.form-group label.sr-only(for="email") Email address: input.form-control.mr-2#emailFp(type='email', name='email', placeholder="Email address") div.form-group.mt-1.text-center button#fpButton.btn.btn-success.mb-2(type='submit') Send email script. $('#fpButton').on('click', function() { $.post('/user/forgot-password', { email: $('#emailFp').val(), }, function(resp) { $('.forgot-message').show(); $('#forgotPasswordForm').remove(); }); });

    Aquí está el formulario en la página:

     

    Su formulario de restablecimiento de contraseña. ( Vista previa grande )

    En este punto, debería poder completar el formulario con una dirección de correo electrónico que esté en su base de datos y luego recibir un correo electrónico para restablecer la contraseña en esa dirección. Hacer clic en el enlace de reinicio no hará nada todavía.

    Configurar la ruta "Restablecer contraseña"

    Ahora sigamos adelante y configuremos el resto del flujo de trabajo.

    Agregue el módulo Sequelize.Op a su ruta:

    const Sequelize = require('sequelize');const Op = Sequelize.Op;

    Ahora vamos a construir la ruta GET para los usuarios que han hecho clic en el enlace de restablecimiento de contraseña. Como verá a continuación, queremos asegurarnos de validar el token de reinicio de manera adecuada.

    Consejo de seguridad n.º 7:

    Asegúrese de buscar solo tokens de reinicio que no hayan caducado y no se hayan utilizado.

    Para fines de demostración, también borro todos los tokens vencidos en carga aquí para mantener la tabla pequeña. Si tiene un sitio web grande, muévalo a un cronjob.

    router.get('/reset-password', async function(req, res, next) { /** * This code clears all expired tokens. You * should move this to a cronjob if you have a * big site. We just include this in here as a * demonstration. **/ await ResetToken.destroy({ where: { expiration: { [Op.lt]: Sequelize.fn('CURDATE')}, } }); //find the token var record = await ResetToken.findOne({ where: { email: req.query.email, expiration: { [Op.gt]: Sequelize.fn('CURDATE')}, token: req.query.token, used: 0 } }); if (record == null) { return res.render('user/reset-password', { message: 'Token has expired. Please try password reset again.', showForm: false }); } res.render('user/reset-password', { showForm: true, record: record });});

    Ahora creemos la ruta POST, que es la que se activa una vez que el usuario completa los detalles de su nueva contraseña.

    Consejo de seguridad n.º 8 al 11:

    • Asegúrese de que las contraseñas coincidan y cumplan con sus requisitos mínimos.
    • Verifique el token de reinicio nuevamente para asegurarse de que no se haya utilizado y no haya caducado. Necesitamos verificarlo nuevamente porque un usuario envía el token a través del formulario.
    • Antes de restablecer la contraseña, marque el token como usado. De esa manera, si ocurre algo imprevisto (caída del servidor, por ejemplo), la contraseña no se restablecerá mientras el token siga siendo válido.
    • Utilice una sal aleatoria criptográficamente segura (en este caso, utilizamos 64 bytes aleatorios).
    router.post('/reset-password', async function(req, res, next) { //compare passwords if (req.body.password1 !== req.body.password2) { return res.json({status: 'error', message: 'Passwords do not match. Please try again.'}); } /** * Ensure password is valid (isValidPassword * function checks if password is = 8 chars, alphanumeric, * has special chars, etc) **/ if (!isValidPassword(req.body.password1)) { return res.json({status: 'error', message: 'Password does not meet minimum requirements. Please try again.'}); } var record = await ResetToken.findOne({ where: { email: req.body.email, expiration: { [Op.gt]: Sequelize.fn('CURDATE')}, token: req.body.token, used: 0 } }); if (record == null) { return res.json({status: 'error', message: 'Token not found. Please try the reset password process again.'}); } var upd = await ResetToken.update({ used: 1 }, { where: { email: req.body.email } }); var newSalt = crypto.randomBytes(64).toString('hex'); var newPassword = crypto.pbkdf2Sync(req.body.password1, newSalt, 10000, 64, 'sha512').toString('base64'); await User.update({ password: newPassword, salt: newSalt }, { where: { email: req.body.email } }); return res.json({status: 'ok', message: 'Password reset. Please login with your new password.'});});And again, the view:extends ../layout block content div.container div.row div.col h1 Reset password p Enter your new password below. if message div.reset-message.alert.alert-warning #{message} else div.reset-message.alert(style='display:none;') if showForm form#resetPasswordForm(onsubmit="return false;") div.form-group label(for="password1") New password: input.form-control#password1(type='password', name='password1') small.form-text.text-muted Password must be 8 characters or more. div.form-group label(for="password2") Confirm new password input.form-control#password2(type='password', name='password2') small.form-text.text-muted Both passwords must match. input#emailRp(type='hidden', name='email', value=record.email) input#tokenRp(type='hidden', name='token', value=record.token) div.form-group button#rpButton.btn.btn-success(type='submit') Reset password script. $('#rpButton').on('click', function() { $.post('/user/reset-password', { password1: $('#password1').val(), password2: $('#password2').val(), email: $('#emailRp').val(), token: $('#tokenRp').val() }, function(resp) { if (resp.status == 'ok') { $('.reset-message').removeClass('alert-danger').addClass('alert-success').show().text(resp.message); $('#resetPasswordForm').remove(); } else { $('.reset-message').removeClass('alert-success').addClass('alert-danger').show().text(resp.message); } }); });

    Así es como debería verse:

     

    Su formulario de restablecimiento de contraseña. ( Vista previa grande )

    Agregue el enlace a su página de inicio de sesión

    Por último, ¡no olvide agregar un enlace a este flujo desde su página de inicio de sesión! Una vez que haga esto, debería tener un flujo de restablecimiento de contraseña funcional. Asegúrese de realizar pruebas exhaustivas en cada etapa del proceso para confirmar que todo funciona y que sus tokens tienen una caducidad breve y están marcados con el estado correcto a medida que avanza el flujo de trabajo.

    Próximos pasos

    Esperamos que esto le haya ayudado en su camino hacia la codificación de una función de restablecimiento de contraseña segura y fácil de usar.

    • Si está interesado en aprender más sobre seguridad criptográfica, le recomiendo el resumen de Wikipedia (¡advertencia, es denso!).
    • Si desea agregar aún más seguridad a la autenticación de su aplicación, consulte 2FA . Existen muchas opciones diferentes.
    • Si te he asustado para que no crees tu propio flujo de restablecimiento de contraseña, puedes confiar en sistemas de inicio de sesión de terceros como Google y Facebook. PassportJS es un middleware que puede utilizar para NodeJS que implementa estas estrategias.

    (dm, yk, il)Explora más en

    • Nodo.js
    • Autenticación
    • javascript
    • Seguridad





    Tal vez te puede interesar:

    1. Creación de su propia biblioteca de validación de React: las características (Parte 2)
    2. Introducción a Quasar Framework: creación de aplicaciones multiplataforma
    3. Creación de un componente web retro que se puede arrastrar con iluminación
    4. Creación y acoplamiento de una aplicación Node.js con arquitectura sin estado con la ayuda de Kinsta

    Creación de flujos de contraseñas seguros con NodeJS y MySQL

    Creación de flujos de contraseñas seguros con NodeJS y MySQL

    Índice Consejo de seguridad n.º 1 I

    programar

    es

    https://aprendeprogramando.es/static/images/programar-creacion-de-flujos-de-contrasenas-seguros-con-nodejs-y-mysql-1018-0.jpg

    2024-05-21

     

    Creación de flujos de contraseñas seguros con NodeJS y MySQL
    Creación de flujos de contraseñas seguros con NodeJS y MySQL

    Si crees que alguno de los contenidos (texto, imagenes o multimedia) en esta página infringe tus derechos relativos a propiedad intelectual, marcas registradas o cualquier otro de tus derechos, por favor ponte en contacto con nosotros en el mail [email protected] y retiraremos este contenido inmediatamente

     

     

    Top 20