Authentification sur un site Web
Objectif de l’activité
Réaliser un module d’authentification d’un utilisateur sur un site Web.
Le dispositif doit permettre à un utilisateur de s’authentifier de manière sécurisée :
- il demande un formulaire de connexion ou d’enregistrement s’il n’est pas encore enregistré,
- il remplit le formulaire avec son identifiant (son email par exemple) et son mot de passe,
- il accède à des pages à accès limité sur le site,
- il peut se déconnecter,
- il est automatiquement déconnecté après un certain temps sans activité.
Principe de fonctionnement

Moyens
Logiciel
Langage : Python
Serveur Web : mode « serveur de développement » de Flask
Gestionnaire de Base de données : SQLite (fichier .db)
Accès à la base de données : module Python sqlite3(sqlite3)
Hachage cryptographique : module Flask werkzeug.security(fonctions generate_password_hashet check_password_hash)
Chiffrement symétrique : module Python cryptography (cryptography)
Structure du module
La structure usuelle des fichiers sur un serveur basé sur Flask est la suivante :
- logs
- erreurs.log
- …
- scripts
- user.py
- …
- static
- img
- css
- js
- templates
- base.html
- signin.html
- …
- index.py
Mise en place du serveur
ATTENTION : tous les chemins doivent être sans espace et sans accent !!
L’interface utilisateur
base.html qui doit servir de base aux pages Web.<!DOCTYPE html>
<html lang="fr-FR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<div>
{% block content %}{%- endblock %}
</div>
</body>
</html>
index.html :Comme toutes les pages du site, elle est basée sur la structure de base.html :
{% extends "base.html" %}
{% block title %}Mon site Web avec authentification{% endblock %}
{% block content %}
Bonjour !
{% endblock %}
index.py sur la structure suivante :# Le Framework (générateur de pages HTML)
from flask import Flask, render_template
# Application Flask
app = Flask(__name__)
# Pages HTML
pageACCUEIL = "index.html"
############################################################################################
@app.route("/")
def index():
return render_template(pageACCUEIL)
# Script de débuggage
if __name__ == "__main__":
app.run(host='0.0.0.0', debug = True)
ATTENTION : ce fichier va beaucoup évoluer au cours de l’activité !
Tester le bon fonctionnement :
- exécuter le script Python
- dans un navigateur Web, taper
localhost:5000
Si tout s’est bien passé, voici ce qui doit apparaître :

Attention : l’exécution du script de cette manière là lance un serveur de développement. Pour un véritable serveur, il faut utiliser WSGI.
Inscription d’un utilisateur
Formulaire d’inscription, coté client
signup.html contenant 3 champs (‘nom’, ’email’ et ‘mot de passe’), encapsulés dans une balise <div> :{% extends "base.html" %}
{% block title %}Inscription{% endblock %}
{% block content %}
<div>
<h1>Inscription</h1>
<!-- Mettre ici le formulaire -->
</div>
{% endblock %}
Pour la création d’un formulaire en HTML, voir sur MDN Web Docs
ATTENTION à bien configurer le formulaire pour qu’il envoie les données selon la méthode POST.
L’action à réaliser à la soumission du formulaire doit être un URL pointant sur la même route que celle pour accéder au formulaire.
pageSIGNUP = "signup.html"
@app.route("/signup")
def signup():
return render_template(pageSIGNUP)
localhost:5000/signup
Récupération des informations coté serveur
Une fois que l’utilisateur aura soumis le formulaire, il faut, coté serveur, récupérer les informations saisies, vérifier leur validité, et les enregistrer dans la base de données…
L’action réalisée à la soumission du formulaire sera une requête de type POST, avec la même URL, contenant l’intégralité des valeurs des champs du formulaire.
Avec Flask, il existe 2 façons de distinguer le type de requête sur une même URL :
- une seule route prenant en compte les deux méthodes :
@app.route('/signup', methods=['GET', 'POST'])
def signup():
if request.method == 'POST' :
...
else:
...
- deux routes différentes, une pour chaque méthode :
@app.route('/signup')
def signup():
...
@app.route('/signup', methods=['POST'])
def signup_post():
...
Dans un premier temps, la réponse à cette requête sera la page d’accueil, enrichie du nom de l’utilisateur.
Pour récupérer les informations figurant dans une requête (quelle que soit la méthode employée, GET ou POST), il faut utiliser la variable de contexte
flask.request(à importer !!), qui contient un attributform, de type dictionnaire, contenant les valeurs des champs du formulaire.Par exemple, pour récupérer la valeur du champ ‘nom’ :
nom = request.form.get('nom')La méthode de dictionnaire
getpermet d’obtenir une valeur à partir d’une clé du dictionnaire, sans provoquer d’erreur si la clé n’est pas présente. Dans ce cas,getrenvoieNone, ou bien la valeur par défaut passée en argumentrequest.form.get('nom', 'nom_par_defaut')

[/private]Remarque : l’URL de la page est également
/signup, mais contrairement au formulaire précédent, cette page a été obtenue par une requête POST.
Enregistrement dans la base de données
Pour des raisons de sécurité, il est fortement déconseillé de stocker des mots de passe dans une base de données d’utilisateur : si la base de données devenait accessible à une personne malveillante ou maladroite, les informations de connexion complètes seraient alors connues.
On préférera mémoriser leurs empreintes, en utilisant un algorithme de hachage.
Pour cela, on peut utiliser les fonctions
generate_password_hashetcheck_password_hashdu module Flaskwerkzeug.security.from werkzeug.security import generate_password_hash, check_password_hash def hash_mdp(mdp): return generate_password_hash(mdp)
Pour la base de données, on peut utiliser SQLite, qui présente l’avantage d’éviter d’installer un serveur de base de données. Les notions sont identiques aux SGBD comme MySQL :
- Création d’une connexion à la base :
db = sqlite3.connect('fichier.db')- Création d’un curseur :
cur = db.cursor()- Envoi d’une requête à l’aide du curseur :
cur.execute(requete, arguments)- Récupération de la réponse :
r = cur.fetchall()
ou bien confirmation des changements :db.commit()- Fermeture de la connexion :
db.close()
Pour l’utiliser avec Flask, il faut prendre quelques précautions, et notamment fermer la connexion à la fin du contexte de l’application (concrètement, à la fin de la requête HTTP).
index.py. les instructions et les fonctions permettant d’accéder à la base de données et de fermer la connexion.
À présent, dans chaque fonction de traitement des requêtes, on peut utiliser get_db() pour obtenir la connexion actuelle ouverte à la base de données.
Pour exécuter des requêtes SQL, on créé deux fonctions dédiées :
def read_db(query, args=(), one=False):
cur = get_db().execute(query, args)
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv
def write_db(query, args=()):
db = get_db()
cur = db.execute(query, args)
db.commit()
La fonction read_db renvoie une unique « ligne » (si one==True), ou bien un tuple de « lignes », de type sqlite3.Row, un type d’objet pouvant se comporter comme une liste ET comme un dictionnaire :
Exemple : si r est une ligne sqlite3.Row comportant 3 champs 'nom', 'email' et 'mdp' :
>>> # Accès par le nom du champ >>> r['nom'] Blaise >>> # Accès par l'indice du champ >>> r[0] Blaise >>> # Itération sur tous les champs >>> for v in r: ... print(v) Blaise blaise.pascal@blaisepascal.fr 1234
Les requêtes doivent être passées sous forme d’une chaîne de caractères, les arguments placés dans un tuple. Leurs emplacements dans la chaîne de caractères sont repérés par le caractère ?.
Exemples :
read_db("SELECT * FROM users WHERE email=?", (email,), one = True)
write_db("INSERT INTO users VALUES (?,?,?)", (nom, email, hmdp))
Avant de pouvoir ajouter des utilisateurs à la base, il faut créer une table, selon le schéma suivant :
users(nom:TEXT, email:TEXT, mdp:TEXT)
user.sql contenant les instructions SQL permettant de créer cette base (on pourra s’aider d’une application tierce comme DB Browser pour SQLite).Pour initialiser la base de données, il suffira d’exécuter (une seule fois) la fonction suivante (ou bien utiliser DB Browser pour SQLite) :
def init_db():
with app.app_context():
db = get_db()
with app.open_resource('user.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
get_user, prenant en paramètre une adresse email, et renvoyant un objet sqlite3.Row comportant l’intégralité des informations sur un utilisateur.Si l’utilisateur n’est pas dans la base de données ou que l’adresse email fournie vaut
None, la fonction renvoie None.
Modifier la fonction signup_post pour :
- vérifier si l’utilisateur (son email) existe ou pas dans la base de données :
- s’il existe déjà, on renvoie à la page de connexion, nommée
pageSIGNIN(pas encore créée …), qui devra proposer le champ ’email’ prérempli (attributvaluede la balise<input>) ; - sinon, on l’enregistre dans la base de données et on renvoie la page d’accueil ;
- s’il existe déjà, on renvoie à la page de connexion, nommée
- si aucun email n’a été saisi, on renvoie la page d’accueil.
Connexion d’un utilisateur
signin.html et la route flask associée /signin, contenant un formulaire de connexion, avec les champs ’email’ et ‘mot de passe’.Penser à prévoir que les champs ‘nom’ et ’email’ peuvent être préremplis.
Créer une fonction signin_post pour gérer la route /signin après soumission du formulaire de connexion (méthode POST).
- Si l’utilisateur (son email) est dans la base de données, on compare les empreintes de mot de passe (celui saisi dans le formulaire et celui présent dans la base de données), à l’aide de la fonction
check_password_hash.- Si les empreintes sont identiques, on renvoie la page d’accueil, en y affichant le nom de l’utilisateur qui vient de se connecter (pour cela, modifier le fichier
index.html). - Sinon on renvoie à nouveau la page de connexion, avec le champ ’email’ prérempli.
- Si les empreintes sont identiques, on renvoie la page d’accueil, en y affichant le nom de l’utilisateur qui vient de se connecter (pour cela, modifier le fichier
- Sinon on renvoie la page de connexion à nouveau.
Pour intégrer une variable de type dictionnaire dans un modèle (template), on peut utiliser la notation
{{ user.nom }}plutôt que{{ user['nom'] }}, plus compacte.Dans ce cas, pour éviter une erreur si l’objet n’est pas défini (ou s’il vaut
None), on encapsule sa référence dans un bloc Jinja{% if ... %}...{% endif %}:{% if user %}value="{{ user.email }}"{% endif %}
Création d’une session
Juste après la connexion d’un utilisateur, la page d’accueil affiche son nom :

Mais lorsque l’on demande à nouveau cette page, le nom de l’utilisateur n’y figure plus !

HTTP est un protocole sans état qui n’enregistre pas l’état d’une session de communication entre deux requêtes successives. La communication est formée de paires requête-réponse indépendantes et chaque paire requête-réponse est traitée comme une transaction indépendante, sans lien avec les requêtes précédentes ou suivantes.
Cela est problématique lorsque les utilisateurs veulent interagir avec une page de façon cohérente (par exemple avec un panier d’achat sur un site de commerce en ligne).
Bien que le cœur d’HTTP soit sans état, les cookies permettent l’utilisation de sessions avec des états…
Pour créer une session, il faut, lors de la requête de connexion, déposer un cookie sur le navigateur de l’utilisateur qui vient de se connecter. Lors des requêtes suivantes, le cookie est lu et l’utilisateur est automatiquement « reconnecté ».

Un cookie HTTP est un petit ensemble de données qu’un serveur envoie au navigateur web de l’utilisateur. Le navigateur peut alors le stocker localement, puis le renvoyer à la prochaine requête vers le même serveur.
Pour déposer un cookie avec flask, il faut auparavant créer un objet réponse (à partir d’un template par exemple), lui ajouter un cookie (à l’aide de la méthode set_cookie), avant de retourner cette réponse.
resp = make_response(render_template(pageXXXX)) # objet "réponse HTTP"
resp.set_cookie('email', email)
return resp
Pour lire un cookie (lors des requêtes suivantes), on utilise la propriété cookies de la requête :
email = request.cookies.get('email')
Sécurisation
La méthode précédente présente un énorme inconvénient : n’importe qui peut se faire passer pour l’utilisateur inscrit sur le site Web, car le cookie n’est pas chiffré.
localhost:5000, puis vérifier que l’utilisateur est bien déconnecté.
'email' et de valeur 'blaise.pascal@blaisepascal.fr' (ou l’adresse choisie et enregistrée précédemment) et constater que c’est suffisant pour se connecter en tant que 'Blaise', sans avoir saisi de mot de passe !
Il va donc falloir chiffrer la valeur du cookie de session !!
Pour chiffrer un texte en Python (chiffrement symétrique), on peut utiliser la bibliothèque
cryptography.from cryptography.fernet import Fernet
Générer une clé : lancer generate_key une fois, relever la valeur de la clé et la renseigner dans le code (cette clé doit être unique pour toute l’application… et secrète !) :
#key = Fernet.generate_key() # à n'exécuter qu'une seule fois ! key = b'vfl-Er72RQVsVzRf7ESWwvut2Ops3zP3pBWA3wjc41I=' f = Fernet(key) # enregistrement de la clé
L’utilisation du module est simple, comme le montre le petit exemple ci-dessous :
mess_chiff = f.encrypt("message à chiffrer".encode()).decode('utf-8') # Le message à chiffrer doit être converti au format bytes, avec .encode() mess = f.decrypt(mess_chiff.encode()).decode('utf-8') # on convertit également le message chiffré en bytes car les cookies sont nativement au format str
connected_user_id permettant d’obtenir l’adresse email de l’utilisateur connecté (s’il y en a un), ou None si aucun utilisateur n’est connecté.REMARQUE : inutile de fournir l’objet
request en paramètre, car il s’agit d’une variable globale (de portée limitée au contexte d’application).ATTENTION : il est impératif d’utiliser une unique clé pour l’application. En générer une nouvelle à chaque session rendrait les cookies indéchiffrables !
/ en utilisant cette nouvelle fonction.
Déconnexion d’un utilisateur
/logout, permettant à un utilisateur de se déconnecter. Pour supprimer un cookie, utiliser la méthode resp.delete_cookie('nom') (resp étant une réponse HTTP, comme celle utilisée pour set_cookie). Après déconnexion, le site affiche la page d’accueil.ATTENTION : il faut utiliser ici la fonction
redirect('route')(penser à importerredirectdepuisflask) plutôt querender_template(pageXXX), car la deuxième solution génèrerai le code HTML AVANT la suppression du cookie, donc avant la « déconnexion » de l’utilisateur !
Menu d’authentification
Le menu d’authentification, qui permet à un utilisateur de s’enregistrer, se connecter, se déconnecter …, n’est pas toujours le même selon qu’un utilisateur est déjà connecté ou pas.
Pour que ce menu apparaisse sur toutes les pages du site, on l’inclue dans une balise <header> du fichier base.html :
<!DOCTYPE html>
<html lang="fr-FR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
</head>
<body>
<header>{% include "header.html" %}</header>
<div>
{% block content %}{%- endblock %}
</div>
</body>
</html>
Mais il faut également injecter automatiquement une variable représentant l’utilisateur (de type sqlite3.Row ou bien None) dans le contexte de chaque modèle (template) du site. Pour cela, on utilise un processeur de contexte.
Un processeur de contexte est une fonction qui s’exécute avant que le modèle ne soit rendu et qui renvoie un dictionnaire dont les clés et les valeurs sont fusionnées avec le contexte du modèle, pour tous les modèles de l’application.
Pour que l’utilisateur connecté soit automatiquement injecté dans les contexte des modèles, on utilise la variable globale flask.g (permettant de stocker des variables globale, de portée limitée au contexte d’application) et les fonctions définies précédemment :
@app.context_processor
def inject_user():
user = getattr(g, 'user', None)
if user is None:
g.user = connected_user_id()
return dict(user = g.user)
À partir de là, la variable user sera disponible pour tous les modèles du site ! (on peut retirer tous les user = ... des fonctions render_template).
Créer un fichier header.html en ajoutant, dans une balise <div> :
- un bouton ‘Connexion’ (uniquement si aucun utilisateur n’est connecté)
- un bouton ‘Déconnexion’ (uniquement si un utilisateur est connecté)
- un bouton ‘Profil’ (uniquement si un utilisateur est connecté)
- un bouton ‘Enregistrement’ (uniquement si aucun utilisateur n’est connecté)
Pour les boutons, utiliser une balise <a>.
Pour laisser à flask la tâche de créer l’URL correct vers une route, il est conseillé d’utiliser la fonction
url_for, directement dans les modèles :Par exemple, pour générer l’URL vers la route
'signup':<a href="{{ url_for('signup') }}">Profil</a>où
'signup'est le nom de la fonction Python, pas de la route !
Ressources : https://www.digitalocean.com/community/tutorials/how-to-add-authentication-to-your-app-with-flask-login-fr
https://flask.palletsprojects.com/en/2.0.x/patterns/sqlite3/
https://fr.wikipedia.org/wiki/Protocole_sans_état
Pour aller plus loin …

