Cómo Manejar Contraseñas de Forma Segura con BcryptsJS en JavaScript

Introducción

Proteger las contraseñas de los sitios web es una habilidad esencial que cualquier desarrollador debería tener. JavaScript ofrece una opción para garantizar el almacenamiento seguro y el procesamiento de contraseñas u otros datos sensibles utilizando los algoritmos de hash proporcionados por el módulo BcryptJS de JavaScript.

En este tutorial, aprenderás sobre BcryptJS y el hashing para configurar un servidor básico de express que almacenará contraseñas como hashes en una base de datos en lugar de cadenas sin procesar, y las recuperará para autenticar la contraseña.

Prerrequisitos

Para continuar con este tutorial, debes tener la siguiente configuración.

  1. Una versión estable de Node.js instalada en tu computadora con la versión 12.x o superior. Puedes usar este tutorial de DigitalOcean para instalar la última versión de Node Js en tu computadora.

  2. Debes saber cómo programar en JavaScript.

  3. Debes tener Express JS instalado en tu computadora. Puedes usar esta guía para aprender cómo configurar un servidor Express.

  4. Por último, necesitarás la base de datos de la comunidad MongoDB o Atlas para completar este tutorial. Puedes instalarla utilizando una de estas guías de DigitalOcean sobre cómo instalar MongoDB.

¿Por qué usar BcryptJS?

Bcrypt es un algoritmo de hash para crear hashes de contraseñas y almacenarlas en caso de una violación de datos. Este algoritmo de hash avanzado utiliza sales, lo que hace difícil de descifrar mediante ataques como el de fuerza bruta.

BcryptJS es la implementación en JavaScript del algoritmo de hash Bcrypt, lo que te permite utilizar el cifrado de hash sin tener que lidiar con funciones de hash complejas. Algunas de las razones que hacen de BcryptJS una gran elección para la seguridad de contraseñas son las siguientes:

  1. Seguridad – BcryptJS implementa el algoritmo Bcrypt, un algoritmo lento (lo cual es bueno en el hashing) y que requiere una gran potencia computacional. Esto dificulta la tarea de los atacantes de descifrar el hash de la contraseña, asegurando la seguridad de las contraseñas incluso en caso de una violación de datos._

  2. Saltado – BcryptJS maneja la generación de sales aleatorias para contraseñas para garantizar la seguridad del almacenamiento (aprenderemos más sobre los hashes y las sales en la próxima sección). Las sales hacen que el hash de una contraseña relativamente débil sea más complejo, lo que dificulta su descifrado.

  3. Facilidad de uso – BcryptJS proporciona a los desarrolladores de JavaScript una herramienta para cifrar sus contraseñas sin requerir un profundo conocimiento de hashing._

En el siguiente paso, aprenderemos brevemente sobre hashes y saltos.

¿Cómo funciona el hash?

Antes de utilizar estos conceptos en tus proyectos, debes entender cómo funciona el hash y el salto.

Hashing

El hashing es la conversión de una cadena simple o texto plano en una cadena de caracteres aleatorios (cifrado). Esto permite almacenar y/o transmitir datos sensibles de forma segura. El hashing involucra los siguientes pasos clave:

Entrada de datos – En primer lugar, se almacena el dato que puede ser de cualquier tipo (binario, caracter, decimal, etc.) como texto plano o cadena.

La función de hash – La función de hash es un algoritmo matemático que toma la entrada de los datos y la convierte en un conjunto de caracteres o códigos de hash. Las funciones de hash son deterministas (producen la misma salida para la misma entrada) y funciones unidireccionales (esto significa que es casi imposible invertir el resultado de las funciones de hash, es decir, un hash en sus datos de entrada).

Resistencia a colisiones – Esto significa que una función hash se crea teniendo en cuenta la idea de resistencia a colisiones, es decir, dos entradas diferentes no pueden tener la misma salida (código hash).

Autenticación – Las funciones hash son deterministas, produciendo el mismo hash para la misma entrada. Por lo tanto, al autenticar una contraseña almacenada como hash, la idea general es que si la contraseña para autenticar coincide con el hash almacenado en la base de datos, la contraseña es correcta.

Salado

Dado que el hash ha existido durante décadas, ha habido desarrollos como tablas arcoíris, que contienen miles de millones de entradas de datos que contienen cadenas de datos y sus hash respectivos basados en diferentes algoritmos de hash.

Ahora, consideremos una situación en la que un usuario que crea una cuenta en tu sitio web crea una contraseña débil. Por lo tanto, en caso de una violación de datos, un atacante puede buscar los hashes de tus usuarios y encontrar la coincidencia del hash para la cuenta de usuario con una contraseña débil. Esto sería desastroso en aplicaciones de alta seguridad. Para evitar que esto suceda, se utilizan los saltos.

El salado es una capa adicional de seguridad agregada a los hashes al agregar una cadena aleatoria de caracteres al hash de una contraseña antes de almacenarla en una base de datos. Por lo tanto, incluso si los datos se filtran en una violación, será difícil para un atacante descifrar un hash que contiene sal. Considera el siguiente ejemplo:

  1. Password = ‘sammy’
  2. Hash = £%$^&£!23!3%!!
  3. Salt = 2vqw£4Df$%sdfk
  4. Hash + Salt = £%$^&£!23!3%!!2vqw£4Df$%sdfk

Como podemos ver claramente, la contraseña almacenada con sal es menos probable que se pueda descifrar debido a la naturaleza determinista de los hashes. Por lo tanto, si un atacante, por ejemplo, busca esta cadena de hash+contraseña en una tabla arcoíris, no obtendrá la contraseña real sino algo completamente diferente.

Ahora estás listo para usar BcryptJS y asegurar tus contraseñas de una manera estándar en la industria.

Instalación de BcryptJS y otros módulos requeridos

Ahora que conoces sobre el hash y la sal, solo te queda encender tu computadora y comenzar a programar. La estructura del proyecto será la siguiente:

Project Structure

Primero, vamos a crear un proyecto npm con los siguientes pasos:

  1. Abre una carpeta y crea un archivo llamado app.js.

  2. Abre una ventana de terminal en esta carpeta y escribe el siguiente comando

npm init

Después de esto, se te pedirán entradas, pero puedes presionar Enter sin proporcionar ninguna entrada.

  1. Luego, crea 3 archivos más, a saber,
  • auth.js
  • db.js
  • User.js
  1. En la misma ventana de terminal, escribe el siguiente comando para instalar los paquetes necesarios.
npm install express mongoose BcryptJS nodemon

Ahora tienes un entorno de proyecto completo para seguir este tutorial. En el siguiente paso, aprenderás cómo crear un servidor para utilizar BcryptJS y almacenar y autenticar contraseñas de forma segura con MongoDB.

Configuración de un servidor con Express JS

Ahora que has configurado la estructura del proyecto, puedes crear un servidor que utilice bcrytjs para asegurar las contraseñas almacenándolas como hashes y autenticándolas. Crearemos el servidor siguiendo los siguientes pasos adecuados.

Paso 1: Crear una conexión a la base de datos de MongoDB

Para conectarnos a mongoDB, estamos utilizando la edición comunitaria. Para mantener el proyecto organizado, guardarás el código para configurar la conexión en el archivo db.js.

  1. const mongoose = require("mongoose");
  2. const mongoURI = "mongodb://127.0.0.1:27017/bcrypt_database";
  3. const connectMongo = async () => {
  4. try {
  5. await mongoose.connect(mongoURI);
  6. console.log("Connected to MongoDB!");
  7. } catch (error) {
  8. console.error("Error connecting to MongoDB: ", error.message);
  9. }
  10. };
  11. module.exports = connectMongo;

Aquí, importas el paquete mongoose, que proporciona una API para conectar JavaScript a MongoDB. Además, en este caso, la URI de conexión es para una instalación local de mongoDB. Si estás utilizando una base de datos en la nube (como Atlas), solo necesitas cambiar la URI por la URI específica de tu base de datos.

Información
Una URI es similar a una URL de servidor con la diferencia de que una URI puede identificar el nombre y la identidad de los recursos y su ubicación en Internet. En cambio, una URL es un subconjunto de una URI que solo puede hacer lo último.

La función connectToMongo es una función async porque mongoose.connect devuelve una promesa de JavaScript. Esta función dará una conexión al finalizar la ejecución con éxito. De lo contrario, devolverá un error.

Finalmente, utilizamos module.exports para exportar esta función siempre que el módulo db.js sea importado.

Paso 2 – Creando un esquema de usuario

Necesitarás un esquema básico para crear o autenticar usuarios en una base de datos MongoDB. Si no sabes qué es un esquema, puedes utilizar esta excelente guía de DO para entender y crear el esquema en MongoDB. Solo utilizaremos dos campos para nuestro esquema, email y password.

Utiliza el siguiente código en tu módulo User.js.

  1. const mongoose = require("mongoose");
  2. const UserSchema = new mongoose.Schema({
  3. email:{
  4. type:String,
  5. required:true,
  6. unique:true
  7. },
  8. password:{
  9. type:String,
  10. required:true
  11. }
  12. });
  13. module.exports = mongoose.model('user', UserSchema)

Creamos dos campos en este esquema, email y password. Ambos son campos obligatorios y de tipo de dato string. Además, email es un campo único, lo que significa que un correo electrónico solo se puede usar una vez para tener una cuenta en este servidor.

Finalmente, exportamos un modelo llamado users. Un modelo representa un esquema en la base de datos. Puedes pensar en un esquema como una regla para definir un modelo, mientras que el modelo se almacena como una colección en la base de datos MongoDB.

Puedes convertir un esquema en un modelo utilizando la función model() de la biblioteca mongoose.

Paso 3 – Configurando el servidor en app.js

Después de seguir los pasos anteriores, has creado con éxito un modelo y un módulo para establecer una conexión con la base de datos MongoDB. Ahora aprenderás a configurar el servidor. Utiliza el siguiente código en tu archivo app.js.

  1. const connectToMongo = require("./db");
  2. const express = require("express");
  3. const app = express();
  4. connectToMongo();
  5. app.use(express.json());
  6. app.use("/auth", require("./auth"));
  7. const port = 3300;
  8. app.listen(port, () => {
  9. console.log(`Listening at http://localhost:${port}`);
  10. });

Aquí, importamos los módulos express y db.js para conectarnos a MongoDB. Luego, utilizamos el middleware express.json() para manejar las respuestas en formato JSON. Las rutas se crean en un módulo diferente (auth.js) para mantener el código limpio y organizado. Finalmente, creamos un punto final para que el servidor escuche en el puerto 3300 en localhost. (Puedes usar cualquier puerto de tu elección)

Cifrando contraseñas y almacenándolas en una base de datos MongoDB

Hasta este punto, el servidor está casi listo, y ahora crearás endpoints para el servidor. Crearemos dos endpoints: signup y login. En el endpoint de signup, tomaremos el correo electrónico y la contraseña de un nuevo usuario y la almacenaremos con cifrado para la contraseña usando BcryptJS.

En el archivo auth.js, escribe el siguiente código:

  1. const express = require("express");
  2. const router = express.Router();
  3. const User = require("./User");
  4. const bcrypt = require("bcryptjs");
  5. // RUTA 1:
  6. router.post("/signup", async (req, res) => {
  7. const salt = await bcrypt.genSalt(10);
  8. const secPass = await bcrypt.hash(req.body.password, salt);
  9. let user = await User.create({
  10. email: req.body.email,
  11. password: secPass,
  12. });
  13. res.json({ user });
  14. });
  15. module.exports = router;

Hacemos las importaciones necesarias y luego configuramos el enrutador de express para crear el endpoint /signup. Estamos utilizando el método POST para que las credenciales no se revelen en la URL de la aplicación. Después de eso, creamos una sal usando la función genSalt del paquete scripts; el parámetro pasado a la función genSalt() contiene la longitud de los caracteres de la sal. Luego, utilizamos la función hash() de BcryptJS, que toma un parámetro requerido, la cadena de contraseña que se convertirá en código hash, y un argumento opcional, la cadena de sal. Y luego devuelve un hash que contiene tanto la contraseña como la sal.

Después de esto, usamos la función create() del módulo mongoose para crear un documento en nuestra base de datos definido por las reglas del modelo users. Recibe un objeto Javascript que contiene el correo electrónico y la contraseña, pero en lugar de proporcionar una cadena sin procesar, le damos el secPass (hash de la contraseña + sal) que se almacenará en la base de datos. De esta manera, hemos almacenado de forma segura una contraseña en la base de datos utilizando su hash combinado con una sal en lugar de una cadena sin procesar. Finalmente, devolvemos una respuesta JSON que contiene el modelo de usuario. (Este método de enviar respuestas solo se utiliza en la fase de desarrollo; en producción, se reemplazará por un token de autenticación u otra cosa).

Para probar este punto final, primero debes ejecutar el servidor, lo cual se puede hacer escribiendo el siguiente comando en la terminal.

cd <path to your project folder>
nodemon ./app.js

Este comando ejecutará tu servidor en localhost y en el puerto 3300 (o en el puerto que especifiques). Luego, puedes enviar una solicitud HTTP a la URL http://localhost:3300/auth/signup con el siguiente cuerpo:

  1. {
  2. "email":"[email protected]",
  3. "password":"sammy"
  4. }

Esto producirá la siguiente salida/respuesta:

{
  "user": {
    "email": "[email protected]",
    "password": "$2a$10$JBka/WyJD0ohkzyu5Wu.JeCqQm33UIx/1xqIeNJ1AQI9kYZ0Gr0IS",
    "_id": "654510cd8f1edaa59a8bb589",
    "__v": 0
  }
}

Nota: El hash de la contraseña y el ID no serán iguales, ya que siempre son únicos.

En la siguiente sección, aprenderás cómo acceder al hash almacenado para la contraseña y autenticarlo con una contraseña proporcionada en el inicio de sesión/autenticación.

Accediendo a la contraseña encriptada y usándola para autenticación

Hasta ahora, has aprendido sobre BcryptJS, hashing, salting, y desarrollar un servidor express que crea nuevos usuarios con contraseñas almacenadas como hashes. Ahora, aprenderás cómo usar la contraseña almacenada y autenticar a un usuario cuando intenta iniciar sesión en la aplicación.

Para agregar un método de autenticación, agrega la siguiente ruta en tu archivo auth.js después de la ruta /signup:

  1. // RUTA 2:
  2. router.post("/login", async (req, res) => {
  3. let user = await User.findOne({ email: req.body.email });
  4. if (!user) {
  5. return res.status(400).json({ error: "Login with proper credentials!" });
  6. }
  7. const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  8. if (!passwordCompare) {
  9. return res
  10. .status(400)
  11. .json({ error: "Login with proper credentials!" });
  12. }
  13. res.json({ success: "Authenticated!" });
  14. });

Aquí, usamos la subruta /auth/login para realizar una autenticación de inicio de sesión para un usuario que ya existe. Al igual que el punto de conexión /auth/signup, esta será una función async-await ya que BcryptJS devuelve promesas.

En primer lugar, usamos la función findOne de la biblioteca Mongoose, que se utiliza para encontrar un documento en una colección basado en una consulta de búsqueda dada. En este caso, estamos buscando al usuario basado en el correo electrónico. Si no hay un usuario con el correo electrónico proporcionado, esto enviará una respuesta con el código de estado 400 para credenciales inválidas. (No es una buena práctica proporcionar qué parámetro es incorrecto durante el inicio de sesión, ya que los atacantes pueden usar esa información para encontrar cuentas existentes).

Si el usuario con el correo electrónico proporcionado existe, el programa pasa a la comparación de contraseñas. Para este propósito, BcryptJS proporciona el método compare(), que toma una cadena sin formato como primer argumento y un hash (con o sin salt) como segundo argumento. Luego, devuelve una promesa Booleana; true si la contraseña coincide con el hash y false si no coinciden. Luego, puedes agregar una simple comprobación utilizando una declaración if y devolver éxito o error en función de la comparación.

Finalmente, exportarás el router de express utilizando module.exports para que el punto de inicio app.js lo use para las rutas.

Para probar esta ruta, puedes enviar otra respuesta HTTP a esta URL http://localhost:3300/auth/login con el cuerpo de la siguiente manera:

  1. {
  2. "email":"[email protected]",
  3. "password":"sammy"
  4. }

Esta solicitud dará la siguiente respuesta:

{
  "success": "Authenticated!"
}

Finalmente, auth.js se verá así:

  1. const express = require("express");
  2. const router = express.Router();
  3. const User = require("./User");
  4. const bcrypt = require("bcryptjs");
  5. // RUTA 1:
  6. router.post("/signup", async (req, res) => {
  7. const salt = await bcrypt.genSalt(10);
  8. const secPass = await bcrypt.hash(req.body.password, salt);
  9. let user = await User.create({
  10. email: req.body.email,
  11. password: secPass,
  12. });
  13. res.json({ user });
  14. });
  15. // RUTA 2:
  16. router.post("/login", async (req, res) => {
  17. let user = await User.findOne({ email: req.body.email });
  18. if (!user) {
  19. return res.status(400).json({ error: "Login with proper credentials!" });
  20. }
  21. const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  22. if (!passwordCompare) {
  23. return res
  24. .status(400)
  25. .json({ error: "Login with proper credentials!" });
  26. }
  27. res.json({ success: "Authenticated!" });
  28. });
  29. module.exports = router;

Conclusión

Este tutorial creó un servidor para explicar el uso de BcryptJS para almacenar y acceder de manera segura a contraseñas de bases de datos. Puedes llevar esto más lejos:

  • Implementando más rutas para otras tareas como obtener credenciales de usuario, actualizar credenciales de usuario, etc.

  • Implementando tokens de autenticación para enviar como respuestas, etc.

Esto comienza tu camino para manejar contraseñas de forma segura; siempre puedes añadir más seguridad con diferentes técnicas y tecnologías, permitiéndote crear aplicaciones más seguras y resilientes.

Source:
https://www.digitalocean.com/community/tutorials/how-to-handle-passwords-safely-with-bcryptsjs-in-javascript