50% OFF Plus
--:--:--
¡Obtener!

Hash de contraseñas (bcrypt)

Intermedio
Express
Express
Actualizado: 20/06/2025

¡Desbloquea el curso de Express completo!

IA
Ejercicios
Certificado
Entrar

Mira la lección en vídeo

Accede al vídeo completo de esta lección y a más contenido exclusivo con el Plan Plus.

Desbloquear Plan Plus

Hashing con bcrypt

El hashing de contraseñas es una práctica fundamental en el desarrollo de aplicaciones web seguras. A diferencia del cifrado tradicional, el hashing es un proceso unidireccional que transforma la contraseña original en una cadena de caracteres irreversible, garantizando que incluso si alguien accede a la base de datos, no pueda obtener las contraseñas originales.

bcrypt es una función de hashing específicamente diseñada para contraseñas que incorpora un mecanismo de "salt" automático y un factor de coste configurable. Esta biblioteca se ha convertido en el estándar de la industria debido a su resistencia contra ataques de fuerza bruta y su capacidad de adaptarse al aumento del poder computacional a lo largo del tiempo.

Instalación y configuración inicial

Para comenzar a trabajar con bcrypt en Express, necesitamos instalar la biblioteca correspondiente:

npm install bcrypt
npm install --save-dev @types/bcrypt

Una vez instalada, podemos importar bcrypt en nuestros controladores o middlewares:

import bcrypt from 'bcrypt';

Configuración del factor de coste

El factor de coste (salt rounds) determina cuántas veces se ejecuta el algoritmo de hashing. Un valor más alto significa mayor seguridad pero también mayor tiempo de procesamiento. Para aplicaciones modernas, se recomienda un valor entre 10 y 12:

const SALT_ROUNDS = 12;

Este valor puede configurarse como variable de entorno para facilitar ajustes según el entorno de despliegue:

const SALT_ROUNDS = parseInt(process.env.BCRYPT_ROUNDS) || 12;

Implementación básica del hashing

El proceso de hashing de contraseñas en bcrypt se realiza mediante el método hash(), que genera automáticamente un salt único para cada contraseña:

import bcrypt from 'bcrypt';

const hashPassword = async (plainPassword) => {
  try {
    const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS);
    return hashedPassword;
  } catch (error) {
    throw new Error('Error al procesar la contraseña');
  }
};

Integración en el registro de usuarios

En el contexto de Express, el hashing se integra típicamente en el proceso de registro de usuarios. Aquí un ejemplo práctico de cómo implementarlo en un controlador:

import bcrypt from 'bcrypt';
import User from '../models/User.js';

const SALT_ROUNDS = 12;

export const registerUser = async (req, res) => {
  try {
    const { email, password, name } = req.body;
    
    // Verificar si el usuario ya existe
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(400).json({ 
        error: 'El usuario ya existe' 
      });
    }
    
    // Hash de la contraseña
    const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
    
    // Crear nuevo usuario con contraseña hasheada
    const newUser = new User({
      email,
      password: hashedPassword,
      name
    });
    
    await newUser.save();
    
    res.status(201).json({ 
      message: 'Usuario registrado exitosamente',
      userId: newUser._id 
    });
    
  } catch (error) {
    res.status(500).json({ 
      error: 'Error interno del servidor' 
    });
  }
};

Manejo de errores y validaciones

Es crucial implementar un manejo robusto de errores al trabajar con bcrypt, ya que el proceso de hashing puede fallar por diversos motivos:

const hashPasswordSafely = async (password) => {
  // Validación básica de entrada
  if (!password || typeof password !== 'string') {
    throw new Error('Contraseña inválida');
  }
  
  if (password.length < 8) {
    throw new Error('La contraseña debe tener al menos 8 caracteres');
  }
  
  try {
    return await bcrypt.hash(password, SALT_ROUNDS);
  } catch (error) {
    console.error('Error en bcrypt.hash:', error);
    throw new Error('Error al procesar la contraseña');
  }
};

Optimización del rendimiento

Para aplicaciones con alto volumen de registros, es importante considerar el impacto en el rendimiento del hashing. bcrypt es intencionalmente lento, pero podemos optimizar su uso:

// Middleware para pre-procesar contraseñas
export const preprocessPassword = async (req, res, next) => {
  if (req.body.password) {
    try {
      req.body.hashedPassword = await bcrypt.hash(req.body.password, SALT_ROUNDS);
      // Eliminar la contraseña en texto plano del request
      delete req.body.password;
      next();
    } catch (error) {
      res.status(500).json({ error: 'Error al procesar la contraseña' });
    }
  } else {
    next();
  }
};

Consideraciones de seguridad adicionales

Al implementar hashing con bcrypt, es importante seguir buenas prácticas de seguridad:

  • Nunca almacenar contraseñas en texto plano: Siempre hashear antes de guardar en la base de datos
  • Validar la entrada: Verificar que la contraseña cumple con los requisitos mínimos de seguridad
  • Logging seguro: Evitar registrar contraseñas en logs, incluso las hasheadas
// Ejemplo de logging seguro
const logUserRegistration = (userData) => {
  const safeData = { ...userData };
  delete safeData.password;
  delete safeData.hashedPassword;
  
  console.log('Nuevo usuario registrado:', safeData);
};

La implementación correcta del hashing con bcrypt establece las bases para un sistema de autenticación seguro, protegiendo las credenciales de los usuarios incluso en caso de compromiso de la base de datos.

Verificación de contraseñas

Guarda tu progreso

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

Una vez que las contraseñas están almacenadas de forma segura mediante hashing, el siguiente paso fundamental es implementar un sistema de verificación de contraseñas que permita autenticar a los usuarios durante el proceso de inicio de sesión. bcrypt proporciona métodos específicos para comparar contraseñas en texto plano con sus versiones hasheadas de manera segura.

Método compare de bcrypt

El método compare() de bcrypt es la herramienta principal para verificar contraseñas. Este método toma la contraseña en texto plano proporcionada por el usuario y la compara con el hash almacenado en la base de datos:

import bcrypt from 'bcrypt';

const verifyPassword = async (plainPassword, hashedPassword) => {
  try {
    const isMatch = await bcrypt.compare(plainPassword, hashedPassword);
    return isMatch;
  } catch (error) {
    throw new Error('Error al verificar la contraseña');
  }
};

La función compare() devuelve un valor booleano: true si la contraseña coincide con el hash, false en caso contrario. Internamente, bcrypt extrae el salt del hash almacenado y lo utiliza para hashear la contraseña proporcionada, comparando después ambos hashes.

Implementación en el login de usuarios

La verificación de contraseñas se integra típicamente en el controlador de inicio de sesión. Aquí un ejemplo completo de cómo implementar esta funcionalidad:

import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import User from '../models/User.js';

export const loginUser = async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Buscar usuario por email
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ 
        error: 'Credenciales inválidas' 
      });
    }
    
    // Verificar contraseña
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(401).json({ 
        error: 'Credenciales inválidas' 
      });
    }
    
    // Generar token JWT si la autenticación es exitosa
    const token = jwt.sign(
      { userId: user._id, email: user.email },
      process.env.JWT_SECRET,
      { expiresIn: '24h' }
    );
    
    res.json({ 
      message: 'Inicio de sesión exitoso',
      token,
      user: {
        id: user._id,
        email: user.email,
        name: user.name
      }
    });
    
  } catch (error) {
    res.status(500).json({ 
      error: 'Error interno del servidor' 
    });
  }
};

Manejo seguro de errores en la verificación

Es crucial implementar un manejo de errores consistente que no revele información sensible sobre la existencia de usuarios o el estado de las contraseñas:

const authenticateUser = async (email, password) => {
  try {
    const user = await User.findOne({ email });
    
    // Realizar la comparación incluso si el usuario no existe
    // para evitar ataques de timing
    const hashedPassword = user ? user.password : '$2b$12$dummy.hash.to.prevent.timing.attacks';
    const isPasswordValid = await bcrypt.compare(password, hashedPassword);
    
    if (!user || !isPasswordValid) {
      throw new Error('Credenciales inválidas');
    }
    
    return user;
  } catch (error) {
    throw new Error('Credenciales inválidas');
  }
};

Middleware de autenticación

Para aplicaciones más complejas, es útil crear un middleware de autenticación que encapsule la lógica de verificación:

export const authenticatePassword = async (req, res, next) => {
  try {
    const { email, password } = req.body;
    
    if (!email || !password) {
      return res.status(400).json({ 
        error: 'Email y contraseña son requeridos' 
      });
    }
    
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ 
        error: 'Credenciales inválidas' 
      });
    }
    
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(401).json({ 
        error: 'Credenciales inválidas' 
      });
    }
    
    // Adjuntar usuario autenticado al request
    req.user = user;
    next();
    
  } catch (error) {
    res.status(500).json({ 
      error: 'Error en la autenticación' 
    });
  }
};

Validación de entrada y sanitización

Antes de proceder con la verificación, es importante validar y sanitizar las entradas del usuario:

const validateLoginInput = (email, password) => {
  const errors = [];
  
  if (!email || typeof email !== 'string') {
    errors.push('Email es requerido');
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
    errors.push('Formato de email inválido');
  }
  
  if (!password || typeof password !== 'string') {
    errors.push('Contraseña es requerida');
  } else if (password.length < 1) {
    errors.push('Contraseña no puede estar vacía');
  }
  
  return errors;
};

export const loginWithValidation = async (req, res) => {
  const { email, password } = req.body;
  
  // Validar entrada
  const validationErrors = validateLoginInput(email, password);
  if (validationErrors.length > 0) {
    return res.status(400).json({ 
      errors: validationErrors 
    });
  }
  
  try {
    const user = await User.findOne({ email: email.toLowerCase().trim() });
    if (!user) {
      return res.status(401).json({ 
        error: 'Credenciales inválidas' 
      });
    }
    
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(401).json({ 
        error: 'Credenciales inválidas' 
      });
    }
    
    // Proceder con la autenticación exitosa
    // ...resto de la lógica
    
  } catch (error) {
    res.status(500).json({ 
      error: 'Error interno del servidor' 
    });
  }
};

Implementación con rate limiting

Para proteger contra ataques de fuerza bruta, es recomendable implementar limitación de intentos de login:

import rateLimit from 'express-rate-limit';

// Configurar rate limiting para login
export const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 5, // máximo 5 intentos por IP
  message: {
    error: 'Demasiados intentos de inicio de sesión. Intenta nuevamente en 15 minutos.'
  },
  standardHeaders: true,
  legacyHeaders: false,
});

// Aplicar en la ruta de login
app.post('/api/auth/login', loginLimiter, loginUser);

Comparación segura con timing constante

Para aplicaciones de alta seguridad, bcrypt ya implementa protección contra ataques de timing, pero podemos reforzar esta protección:

const secureComparePasswords = async (email, password) => {
  const startTime = Date.now();
  
  try {
    const user = await User.findOne({ email });
    const hashedPassword = user ? user.password : '$2b$12$dummy.hash.value';
    
    const isValid = await bcrypt.compare(password, hashedPassword);
    
    // Asegurar tiempo mínimo de respuesta
    const minResponseTime = 100; // 100ms mínimo
    const elapsedTime = Date.now() - startTime;
    if (elapsedTime < minResponseTime) {
      await new Promise(resolve => setTimeout(resolve, minResponseTime - elapsedTime));
    }
    
    return user && isValid ? user : null;
    
  } catch (error) {
    throw new Error('Error en la autenticación');
  }
};

La verificación correcta de contraseñas con bcrypt garantiza que solo los usuarios legítimos puedan acceder al sistema, manteniendo la seguridad incluso cuando las credenciales son interceptadas o la base de datos se ve comprometida.

Aprendizajes de esta lección de Express

  • Comprender qué es el hashing de contraseñas y por qué es fundamental para la seguridad.
  • Aprender a instalar y configurar bcrypt en un proyecto Express.
  • Implementar el hashing de contraseñas con un factor de coste adecuado.
  • Integrar el hashing en el registro y la verificación en el inicio de sesión de usuarios.
  • Aplicar buenas prácticas de seguridad, manejo de errores y optimización del rendimiento en el uso de bcrypt.

Completa este curso de Express y certifícate

Únete a nuestra plataforma de cursos de programación y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración