Authentification
- Procédure d'authentification par login/mot de passe
- Exemple d'authentification - basique
- En pratique
Procédure d'authentification par login/mot de passe
L'authentification permet de vérifier l'identité d'un utilisateur à l'origine d'une requête. Elle est traditionnellement réalisée en utilisant un nom d'utilisateur et un mot de passe.
- L'utilisateur envoie ses identifiants (nom d'utilisateur et mot de passe) au serveur (HTTPS recommandé voire obligatoire).
- Le serveur vérifie les identifiants en les comparant à ceux stockés dans une base de données (même nom d'utilisateur, et comparaison des hashs des mots de passe).
- Si les identifiants sont valides, le serveur crée une session pour l'utilisateur, enregistre l'identifiant de l'utilisateur dans la session, et renvoie un cookie de session au navigateur.
- Le navigateur envoie le cookie de session avec chaque requête suivante, permettant au serveur d'identifier l'utilisateur.
Le fait d'enregistrer l'association entre l'utilisateur dans les données de session (donc côté serveur) permet d'avoir confiance dans l'authentification (le client ne peut pas modifier ces données). Reste le fait qu'une personne tierce en possession de l'identifiant de session peut usurper l'identité de l'utilisateur. Il est donc important de sécuriser la transmission du cookie de session (HTTPS) et de le protéger contre le vol (voir section sécurité : HTTPOnly, Secure, SameSite..).
Exemple d'authentification - basique
En reprenant le module sessions de la section précédente, nous allons créer un
système d'authentification basique, en utilisant une base d'utilisateurs stockée
"en dur" dans le code.
Hashage de mot de passe
Le mot de passe est un élément sensible, et il est important de ne pas le stocker en clair. Pour cela, on utilise un algorithme de hashage qui permet de transformer le mot de passe en une chaîne de caractères fixe, appelée hash. Le hash est une représentation unique du mot de passe, et il est impossible de retrouver le mot de passe à partir du hash.
Pour un hashage sécurisé, il est recommandé d'utiliser des algorithmes de
hashage résistants aux attaques par force brute, comme bcrypt ou argon2. Ces
algorithmes sont plus lents et nécessitent des ressources supplémentaires, mais
ils rendent les attaques par force brute beaucoup plus difficiles.
Pour éviter les attaques par dictionnaire, il est également recommandé d'utiliser un "sel" (salt) : une chaîne de caractères aléatoire ajoutée au mot de passe avant le hashage. Cela rend le hash unique même si deux utilisateurs ont le même mot de passe. Le sel doit être stocké avec le hash, mais il n'est pas nécessaire de le rendre secret.
Nous utiliserons ici simplement l'algorithme sha256 pour le hashage, qui ne
répond pas à ces critères de sécurité, mais qui est simple à mettre en oeuvre.
# Création d'un hash de mot de passe
import hashlib
def hash_password(password: str) -> str:
"""
Renvoie le hash sha256 du mot de passe.
"""
return hashlib.sha256(password.encode()).hexdigest()
Liste des utilisateurs :
# Un dictionnaire par utilisateur, contenant nom, prénom et sha256 du mot de passe
USERS = {
"alice": {
"nom": "Alice",
"prenom": "Dupont",
# évidement aucun intérêt ici d'avoir le hash si le mdp est dans le code
# c'est juste pour l'exemple :)
"password_hash": hash_password("password")
},
"bob": {
"nom": "Bob",
"prenom": "Martin",
# idem !
"password_hash": hash_password("supersecret")
}
}
Vue de login
from bottle import route, run, request, redirect
@route("/login", method=["GET", "POST"])
def login():
"""
Authentifie l'utilisateur et crée une session.
Reçoit le nom d'utilisateur et le mot de passe en POST.
La page de redirection est spécifiée dans le paramètre "next" de l'URL.
Rajouter un template et une logique pour afficher les erreurs.
"""
if request.method == "POST":
# on récupère le nom d'utilisateur et le mot de passe
username = request.forms.get("username")
password = request.forms.get("password")
next = request.query.get("next", "/")
# on vérifie si l'utilisateur existe et si le mot de passe est correct
if (username in USERS
and USERS[username]["password_hash"] == hash_password(password)):
# on crée une session pour l'utilisateur
session_id, session_data, created = get_or_create_session()
session_data["user"] = username
session_data["nom"] = USERS[username]["nom"]
session_data["prenom"] = USERS[username]["prenom"]
save_session(session_id, session_data)
return redirect(next)
else:
return "Nom d'utilisateur ou mot de passe incorrect."
return '''
<form action="" method="post">
Nom d'utilisateur: <input name="username" type="text" />
Mot de passe: <input name="password" type="password" />
<input value="Se connecter" type="submit" />
</form>
'''
Page protégée - utilisation des données utilisateur
@route("/")
def index():
"""
Page protégée, accessible uniquement si l'utilisateur est authentifié.
"""
_, session, _ = get_or_create_session()
if "user" not in session:
return redirect("/login?next=/")
En pratique
Vue de login, protection de la calculatrice
Créez (à partir de l'exemple ci-dessus) une vue de login /login qui utilise un
template, et qui affiche le formulaire de login, et un messages d'erreur si le
login ou le mot de passe est incorrect.
Protégez la page /calculatrice pour qu'elle ne soit accessible qu'aux
utilisateurs authentifiés.
Correction
Template de login (télécharger le code) :
% setdefault("username", "")
% setdefault("password", "")
% setdefault("erreur", None)
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connexion</title>
</head>
<body>
<h1>Merci de vous connecter</h1>
<form action="" method="post">
<label for="username">Nom d'utilisateur:</label>
<input type="text" id="username" name="username" required
minlength="3" value="{{ username }}">
<br>
<label for="password">Mot de passe:</label>
<input type="password" id="password" name="password" required
minlength="8" value="{{ password }}">
<br>
<button type="submit">Se connecter</button>
</form>
% if erreur is not None:
<p class="erreur">{{ erreur }}</p>
% end
</body>
</html>
@route("/login", method=["GET", "POST"])
def login():
"""
Authentifie l'utilisateur et crée une session.
Reçoit le nom d'utilisateur et le mot de passe en POST.
La page de redirection est spécifiée dans le paramètre "next" de l'URL.
Rajouter un template et une logique pour afficher les erreurs.
"""
if request.method == "POST":
# on récupère le nom d'utilisateur et le mot de passe
username = request.forms.get("username")
password = request.forms.get("password")
next = request.query.get("next", "/")
# on vérifie si l'utilisateur existe et si le mot de passe est correct
if (username in USERS
and USERS[username]["password_hash"] == hash_password(password)):
# on crée une session pour l'utilisateur
session_id, session_data, created = get_or_create_session()
session_data["user"] = username
session_data["nom"] = USERS[username]["nom"]
session_data["prenom"] = USERS[username]["prenom"]
save_session(session_id, session_data)
return redirect(next)
else:
return template("login.html",
username=username,
password=password,
erreur="Nom d'utilisateur ou mot de passe incorrect.")
Protection de la page /calculatrice :
# Calculatrice
@get('/calculatrice')
def calculatrice():
# Protection de la page
_, session, _ = get_or_create_session()
if "user" not in session:
return redirect("/login?next=/calculatrice")
# ... reste de la vue calculatrice
Déconnexion
Ajoutez une vue de déconnexion /logout qui :
- affiche un message de confirmation et un bouton si la page est appelée en
GET - détruit la session de l'utilisateur et le redirige vers la page d'accueil si
la page est appelée en
POST
Pour détruire la session, vous pouvez utiliser le code ci-dessous, à rajouter dans
le module sessions :
def delete_session() -> None:
"""
Supprime la session associée à l'identifiant de session.
"""
# on récupère l'identifiant de session dans le cookie
session_id = request.get_cookie("session_id", None)
# on vérifie que l'identifiant de session ne contient que des caractères
# alphanumériques
if session_id is None or not session_id.isalnum():
return
session_file = path.join(SESSIONS_PATH, session_id + ".json")
response.delete_cookie("session_id")
try:
os.remove(session_file)
except FileNotFoundError:
pass
Correction
Template de déconnexion (télécharger le code) :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logout</title>
</head>
<body>
<h1>Déconnexion</h1>
<form action="/logout" method="POST">
<p>Êtes-vous sûr de vouloir vous déconnecter ?</p>
<button type="submit">Déconnexion</button>
</form>
<p><a href="/">Retour à la page d'accueil</a></p>
</body>
</html>
Route de déconnexion (télécharger le code) :
from sessions import get_or_create_session, save_session, delete_session
# ...
@route("/logout", method=["GET", "POST"])
def logout():
"""
Déconnecte l'utilisateur et détruit la session.
"""
if request.method == "POST":
delete_session()
return redirect("/")