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 PlusHashing 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.
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