Authentification



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.

  1. L'utilisateur envoie ses identifiants (nom d'utilisateur et mot de passe) au serveur (HTTPS recommandé voire obligatoire).
  2. 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).
  3. 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.
  4. 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.

Télécharger le code

# 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 :

Télécharger le code

# 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

Télécharger le code

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

Télécharger le code


@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>

Télécharger le code

@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("/")

Programme

  • NSI 1ère : interaction client/serveur, requêtes HTTP, réponses du serveur : distinguer ce qui est mémorisé dans le client et retransmis au serveur
  • NSI terminale : sécurisation des communications (un peu)