Améliorer la sécurité avec le hachage des mots de passe
Dans nos implémentations précédentes, nous avons stocké les mots de passe en texte clair, ce qui constitue un risque de sécurité important. Dans cette étape, nous allons améliorer notre système d'authentification en implémentant le hachage des mots de passe à l'aide de l'algorithme bcrypt.
Pourquoi hacher les mots de passe ?
Stocker les mots de passe en texte clair présente plusieurs risques de sécurité :
- Si votre base de données est compromise, les attaquants peuvent immédiatement utiliser les mots de passe
- Les utilisateurs réutilisent souvent les mots de passe sur plusieurs sites, de sorte qu'une violation peut affecter d'autres services
- Cela viole les meilleures pratiques de sécurité et peut enfreindre les réglementations en matière de protection des données
Le hachage des mots de passe résout ces problèmes en :
- Convertissant les mots de passe en chaînes de longueur fixe qui ne peuvent pas être inversées
- Ajoutant un "sel" pour empêcher les attaquants d'utiliser des tables de recherche précalculées (tables arc-en-ciel)
- S'assurant que même si deux utilisateurs ont le même mot de passe, leurs hachages seront différents
Installation des paquets requis
Tout d'abord, nous devons installer le paquet bcrypt :
pip install bcrypt
Vous devriez voir une sortie confirmant l'installation :
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
Création d'un système d'enregistrement des utilisateurs
Améliorons notre serveur pour inclure l'enregistrement des utilisateurs avec le hachage des mots de passe. Mettez à jour le fichier server.py :
code ~/project/auth_demo/server.py
Remplacez le code existant par :
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)
Ce serveur amélioré :
- Utilise bcrypt pour le hachage sécurisé des mots de passe
- Inclut un point de terminaison (endpoint) d'enregistrement des utilisateurs
- Ajoute un contrôle d'accès basé sur les rôles (administrateur par rapport aux utilisateurs réguliers)
- Crée au préalable un utilisateur administrateur pour les tests
Mise à jour du client
Maintenant, mettons à jour notre client pour tester ces nouvelles fonctionnalités. Mettez à jour le fichier client.py :
code ~/project/auth_demo/client.py
Remplacez le code existant par :
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)
Ce client mis à jour :
- Teste la fonctionnalité d'enregistrement des utilisateurs
- Démontre le contrôle d'accès basé sur les rôles
- Teste à la fois l'authentification des utilisateurs réguliers et des administrateurs
Exécution du système d'authentification amélioré
Exécutons maintenant notre système d'authentification amélioré. Tout d'abord, arrêtez tous les processus serveur en cours d'exécution avec Ctrl+C, puis démarrez le serveur mis à jour :
python ~/project/auth_demo/server.py
Ouvrez un nouvel onglet de terminal ou utilisez le deuxième onglet existant pour exécuter le client :
cd ~/project/auth_demo
python client.py
Vous devriez voir une sortie démontrant :
- L'enregistrement réussi d'un utilisateur
- L'échec de l'enregistrement avec un nom d'utilisateur existant
- La connexion d'un utilisateur régulier et l'accès aux ressources protégées
- L'échec de la tentative d'un utilisateur régulier d'accéder au panneau d'administration
- La connexion de l'administrateur et l'accès aux ressources protégées et au panneau d'administration
Cette sortie confirme que notre système d'authentification amélioré avec le hachage des mots de passe et le contrôle d'accès basé sur les rôles fonctionne correctement.
Avantages de la sécurité
Notre système amélioré fournit désormais :
- Stockage sécurisé des mots de passe : Les mots de passe sont hachés à l'aide de bcrypt, une fonction de hachage lente conçue pour résister aux attaques par force brute.
- Contrôle d'accès basé sur les rôles : Différents utilisateurs peuvent avoir différents niveaux d'accès en fonction de leurs rôles.
- Authentification basée sur des jetons : Continue de fournir les avantages des jetons JWT que nous avons implémentés précédemment.
- Enregistrement des utilisateurs : Permet la création dynamique d'utilisateurs avec un stockage sécurisé des mots de passe.
Ces améliorations rendent notre système d'authentification beaucoup plus robuste et adapté aux applications du monde réel.