Mejora de la Seguridad con el Hashing de Contraseñas
En nuestras implementaciones anteriores, almacenamos las contraseñas en texto plano, lo cual es un riesgo de seguridad significativo. En este paso, mejoraremos nuestro sistema de autenticación implementando el hashing de contraseñas utilizando el algoritmo bcrypt.
¿Por qué hacer Hashing de Contraseñas?
Almacenar contraseñas en texto plano presenta varios riesgos de seguridad:
- Si tu base de datos se ve comprometida, los atacantes pueden usar inmediatamente las contraseñas
- Los usuarios a menudo reutilizan contraseñas en múltiples sitios, por lo que una brecha puede afectar a otros servicios
- Viola las mejores prácticas de seguridad y puede infringir las regulaciones de protección de datos
El hashing de contraseñas resuelve estos problemas al:
- Convertir las contraseñas en cadenas de longitud fija que no se pueden revertir
- Agregar una "sal" para evitar que los atacantes usen tablas de búsqueda precalculadas (tablas arcoíris)
- Asegurar que incluso si dos usuarios tienen la misma contraseña, sus hashes serán diferentes
Instalación de los Paquetes Requeridos
Primero, necesitamos instalar el paquete bcrypt:
pip install bcrypt
Deberías ver una salida que confirme la instalación:
Collecting bcrypt
Downloading bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl (593 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 593.8/593.8 KB 10.5 MB/s eta 0:00:00
Installing collected packages: bcrypt
Successfully installed bcrypt-4.0.1
Creación de un Sistema de Registro de Usuarios
Mejoremos nuestro servidor para incluir el registro de usuarios con hashing de contraseñas. Actualiza el archivo server.py:
code ~/project/auth_demo/server.py
Reemplaza el código existente con:
from flask import Flask, request, jsonify
import jwt
import datetime
import os
import bcrypt
app = Flask(__name__)
## Secret key for signing JWT tokens
SECRET_KEY = os.urandom(24)
## Store users as a dictionary with username as key and a dict of hashed password and roles as value
users = {}
def hash_password(password):
"""Hash a password using bcrypt"""
## Convert password to bytes if it's a string
if isinstance(password, str):
password = password.encode('utf-8')
## Generate a salt and hash the password
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password, salt)
return hashed
def check_password(password, hashed):
"""Verify a password against its hash"""
## Convert password to bytes if it's a string
if isinstance(password, str):
password = password.encode('utf-8')
## Check if the password matches the hash
return bcrypt.checkpw(password, hashed)
def generate_token(username, role):
"""Generate a JWT token for the authenticated user"""
## Token expires after 30 minutes
expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
payload = {
'username': username,
'role': role,
'exp': expiration
}
## Create the JWT token
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return token
def verify_token(token):
"""Verify the JWT token"""
try:
## Decode and verify the token
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload
except jwt.ExpiredSignatureError:
## Token has expired
return None
except jwt.InvalidTokenError:
## Invalid token
return None
@app.route('/register', methods=['POST'])
def register():
data = request.get_json()
## Extract registration data
username = data.get('username')
password = data.get('password')
## Basic validation
if not username or not password:
return jsonify({"status": "error", "message": "Username and password are required"}), 400
## Check if the username already exists
if username in users:
return jsonify({"status": "error", "message": "Username already exists"}), 409
## Hash the password and store the user
hashed_password = hash_password(password)
users[username] = {
'password': hashed_password,
'role': 'user' ## Default role
}
return jsonify({
"status": "success",
"message": f"User {username} registered successfully"
})
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
## Extract username and password from the request
username = data.get('username')
password = data.get('password')
## Check if the user exists
if username not in users:
return jsonify({"status": "error", "message": "Invalid credentials"}), 401
## Check if the password is correct
if check_password(password, users[username]['password']):
## Generate a JWT token
token = generate_token(username, users[username]['role'])
return jsonify({
"status": "success",
"message": f"Welcome {username}!",
"token": token
})
else:
return jsonify({"status": "error", "message": "Invalid credentials"}), 401
@app.route('/protected', methods=['GET'])
def protected():
## Get the token from the Authorization header
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({"status": "error", "message": "Missing or invalid token"}), 401
## Extract the token
token = auth_header.split(' ')[1]
## Verify the token
payload = verify_token(token)
if payload:
return jsonify({
"status": "success",
"message": f"You are viewing protected content, {payload['username']}!",
"role": payload['role']
})
else:
return jsonify({"status": "error", "message": "Invalid or expired token"}), 401
@app.route('/admin', methods=['GET'])
def admin():
## Get the token from the Authorization header
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({"status": "error", "message": "Missing or invalid token"}), 401
## Extract the token
token = auth_header.split(' ')[1]
## Verify the token
payload = verify_token(token)
if not payload:
return jsonify({"status": "error", "message": "Invalid or expired token"}), 401
## Check if user has admin role
if payload['role'] != 'admin':
return jsonify({"status": "error", "message": "Admin access required"}), 403
return jsonify({
"status": "success",
"message": f"Welcome to the admin panel, {payload['username']}!"
})
## Add an admin user for testing
admin_password = hash_password("admin123")
users["admin"] = {
'password': admin_password,
'role': 'admin'
}
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
Este servidor mejorado:
- Utiliza bcrypt para el hashing seguro de contraseñas
- Incluye un endpoint de registro de usuarios
- Agrega control de acceso basado en roles (administrador vs. usuarios regulares)
- Pre-crea un usuario administrador para pruebas
Actualización del Cliente
Ahora, actualicemos nuestro cliente para probar estas nuevas características. Actualiza el archivo client.py:
code ~/project/auth_demo/client.py
Reemplaza el código existente con:
import requests
import json
## Server URL
BASE_URL = "http://localhost:5000"
def register(username, password):
"""Register a new user"""
response = requests.post(
f"{BASE_URL}/register",
json={"username": username, "password": password}
)
print(f"Registration response: {response.status_code}")
print(response.json())
return response.status_code == 200
def login(username, password):
"""Authenticate with the server using username and password"""
response = requests.post(
f"{BASE_URL}/login",
json={"username": username, "password": password}
)
print(f"Login response: {response.status_code}")
data = response.json()
print(data)
## Return the token if login was successful
return data.get('token') if response.status_code == 200 else None
def access_protected(token=None):
"""Access a protected resource using JWT token"""
headers = {}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(f"{BASE_URL}/protected", headers=headers)
print(f"Protected resource response: {response.status_code}")
print(response.json())
return response
def access_admin(token=None):
"""Access the admin panel using JWT token"""
headers = {}
if token:
headers['Authorization'] = f'Bearer {token}'
response = requests.get(f"{BASE_URL}/admin", headers=headers)
print(f"Admin panel response: {response.status_code}")
print(response.json())
return response
if __name__ == "__main__":
## Test user registration
print("=== Registering a new user ===")
register("testuser", "testpass123")
## Try to register with the same username (should fail)
print("\n=== Registering with existing username ===")
register("testuser", "differentpass")
## Test regular user login and access
print("\n=== Regular user login ===")
user_token = login("testuser", "testpass123")
if user_token:
print("\n=== Regular user accessing protected resource ===")
access_protected(user_token)
print("\n=== Regular user trying to access admin panel ===")
access_admin(user_token)
## Test admin login and access
print("\n=== Admin login ===")
admin_token = login("admin", "admin123")
if admin_token:
print("\n=== Admin accessing protected resource ===")
access_protected(admin_token)
print("\n=== Admin accessing admin panel ===")
access_admin(admin_token)
Este cliente actualizado:
- Prueba la funcionalidad de registro de usuarios
- Demuestra el control de acceso basado en roles
- Prueba tanto la autenticación de usuarios regulares como la de administradores
Ejecución del Sistema de Autenticación Mejorado
Ahora, ejecutemos nuestro sistema de autenticación mejorado. Primero, detén cualquier proceso de servidor en ejecución con Ctrl+C, luego inicia el servidor actualizado:
python ~/project/auth_demo/server.py
Abre una nueva pestaña de terminal o usa la segunda pestaña existente para ejecutar el cliente:
cd ~/project/auth_demo
python client.py
Deberías ver una salida que demuestra:
- Registro exitoso de usuario
- Registro fallido con un nombre de usuario existente
- Inicio de sesión de usuario regular y acceso a recursos protegidos
- Intento fallido de un usuario regular de acceder al panel de administración
- Inicio de sesión de administrador y acceso tanto a recursos protegidos como al panel de administración
Esta salida confirma que nuestro sistema de autenticación mejorado con hashing de contraseñas y control de acceso basado en roles funciona correctamente.
Ventajas de Seguridad
Nuestro sistema mejorado ahora proporciona:
- Almacenamiento Seguro de Contraseñas: Las contraseñas se hash utilizando bcrypt, una función de hashing lenta diseñada para resistir ataques de fuerza bruta.
- Control de Acceso Basado en Roles: Diferentes usuarios pueden tener diferentes niveles de acceso según sus roles.
- Autenticación Basada en Tokens: Continúa proporcionando los beneficios de los tokens JWT que implementamos anteriormente.
- Registro de Usuarios: Permite la creación dinámica de usuarios con almacenamiento seguro de contraseñas.
Estas mejoras hacen que nuestro sistema de autenticación sea mucho más robusto y adecuado para aplicaciones del mundo real.