Flask
Tutorial Flask: Hash con bcrypt
Aprende a implementar hashing seguro y verificación de contraseñas con bcrypt en Python y Flask para proteger tus aplicaciones web.
Aprende Flask y certifícateHash de contraseñas
El almacenamiento seguro de contraseñas es uno de los aspectos más críticos en el desarrollo de aplicaciones web. Nunca debemos guardar contraseñas en texto plano en nuestra base de datos, ya que esto representa un riesgo de seguridad enorme si los datos son comprometidos.
La solución profesional consiste en utilizar funciones de hash criptográficas específicamente diseñadas para contraseñas. Estas funciones transforman la contraseña original en una cadena de caracteres irreversible, conocida como hash.
Instalación de bcrypt
Para trabajar con hashing seguro en Flask, utilizaremos la librería bcrypt
, que implementa el algoritmo bcrypt diseñado específicamente para contraseñas:
pip install bcrypt
Esta librería nos proporciona las herramientas necesarias para generar hashes seguros y verificar contraseñas de manera eficiente.
Configuración básica en Flask
Primero, importamos bcrypt en nuestra aplicación Flask y configuramos los parámetros básicos:
from flask import Flask
import bcrypt
app = Flask(__name__)
# Configuración del factor de trabajo (rounds)
BCRYPT_ROUNDS = 12
El factor de trabajo (rounds) determina cuántas iteraciones realizará el algoritmo. Un valor más alto significa mayor seguridad pero también mayor tiempo de procesamiento. El valor 12 es considerado seguro para aplicaciones modernas.
Generación de hashes
Para generar un hash seguro de una contraseña, utilizamos el método hashpw()
de bcrypt:
def hash_password(password):
"""
Genera un hash seguro para una contraseña
"""
# Convertir la contraseña a bytes
password_bytes = password.encode('utf-8')
# Generar salt y hash
salt = bcrypt.gensalt(rounds=BCRYPT_ROUNDS)
hashed = bcrypt.hashpw(password_bytes, salt)
# Retornar como string para almacenar en base de datos
return hashed.decode('utf-8')
El proceso incluye varios pasos importantes:
- Codificación a bytes: bcrypt requiere que la contraseña esté en formato bytes
- Generación de salt: Un valor aleatorio único que se combina con la contraseña
- Hashing: La transformación criptográfica irreversible
- Decodificación: Convertir el resultado a string para almacenamiento
Integración con modelos SQLAlchemy
En una aplicación real, integraremos el hashing de contraseñas directamente en nuestros modelos de usuario:
from flask_sqlalchemy import SQLAlchemy
import bcrypt
db = SQLAlchemy(app)
class Usuario(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False)
def set_password(self, password):
"""
Establece la contraseña hasheada para el usuario
"""
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt(rounds=12)
self.password_hash = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
def check_password(self, password):
"""
Verifica si la contraseña proporcionada es correcta
"""
password_bytes = password.encode('utf-8')
hash_bytes = self.password_hash.encode('utf-8')
return bcrypt.checkpw(password_bytes, hash_bytes)
Esta implementación encapsula la lógica de hashing dentro del modelo, manteniendo la seguridad y facilitando el uso en toda la aplicación.
Ejemplo práctico de registro
Veamos cómo implementar un endpoint de registro que utiliza hashing seguro:
from flask import request, jsonify
@app.route('/registro', methods=['POST'])
def registro_usuario():
data = request.get_json()
# Validar datos de entrada
if not data.get('email') or not data.get('password'):
return jsonify({'error': 'Email y contraseña requeridos'}), 400
# Verificar si el usuario ya existe
usuario_existente = Usuario.query.filter_by(email=data['email']).first()
if usuario_existente:
return jsonify({'error': 'El usuario ya existe'}), 409
# Crear nuevo usuario con contraseña hasheada
nuevo_usuario = Usuario(email=data['email'])
nuevo_usuario.set_password(data['password'])
# Guardar en base de datos
db.session.add(nuevo_usuario)
db.session.commit()
return jsonify({'mensaje': 'Usuario registrado exitosamente'}), 201
Consideraciones de seguridad
Al implementar hashing de contraseñas, debemos tener en cuenta varios aspectos importantes:
- Tiempo constante: bcrypt está diseñado para tomar un tiempo predecible, evitando ataques de timing
- Salt único: Cada contraseña genera un salt diferente, previniendo ataques de rainbow tables
- Factor de trabajo adaptable: Podemos ajustar la complejidad según las necesidades de seguridad
- Resistencia a fuerza bruta: El algoritmo está optimizado para ser costoso computacionalmente
Manejo de errores
Es importante implementar un manejo robusto de errores al trabajar con hashing:
def hash_password_safe(password):
"""
Versión segura del hashing con manejo de errores
"""
try:
if not password or len(password.strip()) == 0:
raise ValueError("La contraseña no puede estar vacía")
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt(rounds=BCRYPT_ROUNDS)
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
except Exception as e:
# Log del error para debugging
app.logger.error(f"Error al hashear contraseña: {str(e)}")
raise ValueError("Error al procesar la contraseña")
Esta implementación garantiza que los errores de hashing se manejen de manera apropiada sin exponer información sensible al usuario final.
Verificación segura
La verificación de contraseñas es el proceso complementario al hashing que nos permite comprobar si una contraseña proporcionada por el usuario coincide con el hash almacenado en la base de datos. Este proceso debe realizarse de manera segura para mantener la integridad del sistema de autenticación.
A diferencia del hashing, que es un proceso unidireccional, la verificación utiliza el mismo algoritmo para hashear la contraseña de entrada y compararla con el hash almacenado, pero sin revelar nunca la contraseña original.
Proceso de verificación con bcrypt
El método checkpw()
de bcrypt realiza la verificación automática de contraseñas de forma segura:
def verify_password(stored_hash, password):
"""
Verifica si una contraseña coincide con su hash almacenado
"""
try:
# Convertir ambos valores a bytes
password_bytes = password.encode('utf-8')
hash_bytes = stored_hash.encode('utf-8')
# Verificar la contraseña
return bcrypt.checkpw(password_bytes, hash_bytes)
except Exception as e:
# En caso de error, siempre retornar False por seguridad
return False
La función checkpw()
extrae automáticamente el salt del hash almacenado y realiza la misma operación de hashing sobre la contraseña de entrada, comparando los resultados de manera segura.
Implementación en endpoints de login
Un endpoint de autenticación típico utiliza la verificación de contraseñas para validar las credenciales del usuario:
@app.route('/login', methods=['POST'])
def login_usuario():
data = request.get_json()
# Validar datos de entrada
if not data.get('email') or not data.get('password'):
return jsonify({'error': 'Credenciales incompletas'}), 400
# Buscar usuario en la base de datos
usuario = Usuario.query.filter_by(email=data['email']).first()
# Verificar existencia del usuario y contraseña
if usuario and usuario.check_password(data['password']):
return jsonify({
'mensaje': 'Login exitoso',
'usuario_id': usuario.id
}), 200
else:
return jsonify({'error': 'Credenciales inválidas'}), 401
Protección contra ataques de timing
Una consideración importante en la verificación segura es evitar que los atacantes puedan determinar si un usuario existe basándose en el tiempo de respuesta. Para esto, debemos asegurar que la verificación tome un tiempo similar independientemente del resultado:
@app.route('/login', methods=['POST'])
def login_seguro():
data = request.get_json()
if not data.get('email') or not data.get('password'):
return jsonify({'error': 'Credenciales incompletas'}), 400
# Buscar usuario
usuario = Usuario.query.filter_by(email=data['email']).first()
# Siempre realizar verificación, incluso si el usuario no existe
if usuario:
password_valida = usuario.check_password(data['password'])
else:
# Realizar hash dummy para mantener tiempo constante
bcrypt.checkpw(b'dummy_password', b'$2b$12$dummy.hash.to.maintain.timing')
password_valida = False
if usuario and password_valida:
return jsonify({
'mensaje': 'Login exitoso',
'usuario_id': usuario.id
}), 200
else:
return jsonify({'error': 'Credenciales inválidas'}), 401
Verificación con límites de intentos
Para mejorar la seguridad del sistema, podemos implementar un mecanismo que limite los intentos de login fallidos:
from datetime import datetime, timedelta
class Usuario(db.Model):
# ... campos existentes ...
intentos_fallidos = db.Column(db.Integer, default=0)
bloqueado_hasta = db.Column(db.DateTime, nullable=True)
def esta_bloqueado(self):
"""
Verifica si la cuenta está temporalmente bloqueada
"""
if self.bloqueado_hasta and datetime.utcnow() < self.bloqueado_hasta:
return True
return False
def registrar_intento_fallido(self):
"""
Registra un intento de login fallido
"""
self.intentos_fallidos += 1
# Bloquear después de 5 intentos fallidos
if self.intentos_fallidos >= 5:
self.bloqueado_hasta = datetime.utcnow() + timedelta(minutes=15)
db.session.commit()
def resetear_intentos(self):
"""
Resetea los intentos fallidos después de un login exitoso
"""
self.intentos_fallidos = 0
self.bloqueado_hasta = None
db.session.commit()
Endpoint de login con protección avanzada
Integrando todas las medidas de seguridad, nuestro endpoint de login queda así:
@app.route('/login', methods=['POST'])
def login_protegido():
data = request.get_json()
if not data.get('email') or not data.get('password'):
return jsonify({'error': 'Credenciales incompletas'}), 400
usuario = Usuario.query.filter_by(email=data['email']).first()
# Verificar si la cuenta está bloqueada
if usuario and usuario.esta_bloqueado():
return jsonify({
'error': 'Cuenta temporalmente bloqueada por múltiples intentos fallidos'
}), 423
# Verificar credenciales
if usuario and usuario.check_password(data['password']):
# Login exitoso
usuario.resetear_intentos()
return jsonify({
'mensaje': 'Login exitoso',
'usuario_id': usuario.id
}), 200
else:
# Login fallido
if usuario:
usuario.registrar_intento_fallido()
return jsonify({'error': 'Credenciales inválidas'}), 401
Verificación en operaciones sensibles
Además del login inicial, es recomendable re-verificar la contraseña para operaciones sensibles como cambio de contraseña o eliminación de cuenta:
@app.route('/cambiar-password', methods=['POST'])
def cambiar_password():
data = request.get_json()
usuario_id = data.get('usuario_id') # En una app real vendría del token de sesión
# Obtener usuario
usuario = Usuario.query.get(usuario_id)
if not usuario:
return jsonify({'error': 'Usuario no encontrado'}), 404
# Verificar contraseña actual
if not usuario.check_password(data.get('password_actual', '')):
return jsonify({'error': 'Contraseña actual incorrecta'}), 401
# Establecer nueva contraseña
nueva_password = data.get('nueva_password')
if not nueva_password or len(nueva_password) < 8:
return jsonify({'error': 'La nueva contraseña debe tener al menos 8 caracteres'}), 400
usuario.set_password(nueva_password)
db.session.commit()
return jsonify({'mensaje': 'Contraseña actualizada exitosamente'}), 200
Logging de eventos de seguridad
Para mantener un registro de auditoría, es importante registrar los eventos relacionados con la verificación de contraseñas:
import logging
# Configurar logger de seguridad
security_logger = logging.getLogger('security')
security_logger.setLevel(logging.INFO)
def login_con_auditoria():
data = request.get_json()
email = data.get('email', '')
usuario = Usuario.query.filter_by(email=email).first()
if usuario and usuario.check_password(data.get('password', '')):
# Login exitoso
security_logger.info(f"Login exitoso para usuario: {email}")
usuario.resetear_intentos()
return jsonify({'mensaje': 'Login exitoso'}), 200
else:
# Login fallido
security_logger.warning(f"Intento de login fallido para: {email}")
if usuario:
usuario.registrar_intento_fallido()
return jsonify({'error': 'Credenciales inválidas'}), 401
Esta implementación nos permite monitorear patrones de ataques y responder proactivamente a amenazas de seguridad.
Otras lecciones de Flask
Accede a todas las lecciones de Flask y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Flask
Introducción Y Entorno
Instalación Y Configuración Flask Con Venv
Introducción Y Entorno
Rutas Endpoints Rest Get
Api Rest
Respuestas Con Esquemas Flask Marshmallow
Api Rest
Rutas Endpoints Rest Post, Put Y Delete
Api Rest
Manejo De Errores Y Códigos De Estado Http
Api Rest
Ejercicios de programación de Flask
Evalúa tus conocimientos de esta lección Hash con bcrypt con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.