Aller au contenu

Authentification et contrôle d'accès

Jusqu’ici, n’importe qui pouvait ouvrir n’importe quelle page et accomplir n’importe quelle action. Une application doit généralement garantir un certain contrôle sur l’accès aux données et sur la portée des actions permises à ses utilisateurs. Elle doit donc être capable de reconnaître et distinguer les utilisateurs (l’authentification), puis de vérifier s’ils ont les droits nécessaires pour accomplir l’action demandée (l’autorisation).

Faisons un court rappel du fonctionnement d’une session.

Le serveur conserve les données de session dans un espace qui lui appartient, et le navigateur ne détient qu’un identifiant transporté par le témoin PHPSESSID. C’est exactement ce qu’il faut pour l’authentification : l’état d’authentification — par exemple l’identifiant de l’utilisateur et son rôle — est conservé côté serveur. Le navigateur ne conserve que l’identifiant qui permet au serveur de retrouver cette session.

Cet identifiant demeure toutefois sensible : si quelqu’un vole un identifiant de session valide, il peut potentiellement reprendre la session de l’utilisateur.

Se connecter revient donc à stocker dans la session l’information nécessaire pour reconnaître l’utilisateur aux requêtes suivantes.

<?php
session_start();
// Après vérification du mot de passe :
$_SESSION['utilisateur_id'] = 42;
$_SESSION['role'] = 'etudiant';

À la requête suivante, il suffit de regarder si cette donnée existe.

<?php
session_start();
$estConnecte = isset($_SESSION['utilisateur_id']);
  1. Navigateur vers Serveur PHP : POST du formulaire de connexion (courriel et mot de passe)
  2. Serveur PHP : Vérification en BD, puis identité stocké en session
  3. Serveur PHP vers Navigateur : 302 · Redirection et dépôt du témoin PHPSESSID
  4. Navigateur vers Serveur PHP : Demande d'une page protégée (renvoie PHPSESSID)
  5. Serveur PHP : Session relue : l'utilisateur est reconnu
  6. Serveur PHP vers Navigateur : 200 · La page s'affiche
Une connexion, puis une page protégée

Un témoin (cookie) est une petite valeur que le serveur demande au navigateur de conserver et de renvoyer avec les requêtes auxquelles ce témoin s’applique. setcookie le dépose, $_COOKIE le relit.

<?php
setcookie('langue', 'fr', [
'expires' => time() + 30 * 24 * 3600, // 30 jours
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
// 'secure' => true, en HTTPS
]);
$langue = $_COOKIE['langue'] ?? 'fr';

Sa portée se règle par ses options :

  • expires fixe sa durée de vie (sans elle, il disparaît à la fermeture du navigateur);
  • path et domain limitent les adresses qui le reçoivent.
OptionEffet
httponlyLe script (JavaScript) de la page ne peut pas lire le témoin, ce qui réduit notamment le risque de vol par un script malveillant.
secureLe témoin n’est envoyé que lors d’une connexion HTTPS. Sa transmission bénéficie donc du chiffrement fourni par HTTPS.
samesiteContrôle dans quelles situations un témoin peut accompagner une requête provenant d’un autre site. Cela contribue notamment à réduire les attaques CSRF.

Les limites découlent de sa nature : le témoin vit chez le client, donc il est visible, modifiable et supprimable par lui ; sa taille est réduite (environ 4 Ko) ; et il voyage à chaque requête.

Vérification Pourquoi ranger role=admin dans un témoin serait-il une faille ?

Parce que le visiteur contrôle ses propres témoins : il peut en modifier la valeur dans son navigateur et se déclarer administrateur. Le serveur, lui, ne pourrait pas distinguer ce témoin falsifié d’un vrai. Le rôle doit venir de la base de données et être conservé en session, ou relu à chaque requête.

Le courriel sert d’identifiant, donc il doit être unique. Le rôle prépare la section suivante.

CREATE TABLE utilisateurs (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(100) NOT NULL,
courriel VARCHAR(150) NOT NULL UNIQUE,
empreinte VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL DEFAULT 'etudiant',
cree_le TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Toute la logique liée à l’authentification est encapsulée dans une classe, ce qui facilite l’organisation et l’évolution du code.

service/Auth.php
<?php
class Auth
{
public function __construct(private PDO $pdo) {}
public function inscrire(string $nom, string $courriel, string $motDePasse): bool
{
$empreinte = password_hash($motDePasse, PASSWORD_DEFAULT);
try {
$requete = $this->pdo->prepare(
'INSERT INTO utilisateurs (nom, courriel, empreinte)
VALUES (:nom, :courriel, :empreinte)'
);
$requete->execute([
'nom' => $nom,
'courriel' => $courriel,
'empreinte' => $empreinte,
]);
return true;
} catch (PDOException $e) {
// MySQL / MariaDB : 1062 signifie qu'une valeur UNIQUE existe déjà.
if (($e->errorInfo[1] ?? null) === 1062) {
return false;
}
// Une autre erreur de base de données ne doit pas être masquée.
throw $e;
}
}
public function connecter(string $courriel, string $motDePasse): bool
{
$requete = $this->pdo->prepare(
'SELECT id, nom, empreinte, role FROM utilisateurs WHERE courriel = :courriel'
);
$requete->execute(['courriel' => $courriel]);
$utilisateur = $requete->fetch(PDO::FETCH_ASSOC);
if ($utilisateur === false || !password_verify($motDePasse, $utilisateur['empreinte'])) {
return false;
}
// Nouvel identifiant de session : protège contre la fixation de session.
session_regenerate_id(true);
$_SESSION['utilisateur_id'] = $utilisateur['id'];
$_SESSION['nom'] = $utilisateur['nom'];
$_SESSION['role'] = $utilisateur['role'];
return true;
}
public function deconnecter(): void
{
$_SESSION = [];
// Faire également expirer le témoin de session dans le navigateur.
if (ini_get('session.use_cookies')) {
reinitialiserTemoin();
}
session_destroy();
}
private function reinitialiserTemoin(): void
{
$parametres = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 42000,
'path' => $parametres['path'],
'domain' => $parametres['domain'],
'secure' => $parametres['secure'],
'httponly' => $parametres['httponly'],
'samesite' => $parametres['samesite'] ?? 'Lax',
]);
}
}

Un mot de passe ne se conserve jamais en clair, ni chiffré de façon réversible. On en conserve une empreinte produite par une fonction de hachage adaptée aux mots de passe. Cette empreinte ne se déchiffre pas : pour retrouver le mot de passe, un attaquant devrait essayer des mots de passe candidats. En revanche, il est facile de vérifier si un mot de passe saisi correspond à l’empreinte.

PHP fournit deux fonctions qui font tout le travail.

<?php
// À l'inscription : produire l'empreinte.
$empreinte = password_hash($motDePasse, PASSWORD_DEFAULT);
// À la connexion : comparer sans jamais déchiffrer.
if (password_verify($motDePasseSaisi, $empreinte)) {
// Le mot de passe est bon.
}

password_hash ajoute lui-même un sel aléatoire à chaque appel : deux personnes ayant le même mot de passe obtiennent des empreintes différentes. Ce sel est inclus dans la chaîne produite, il n’y a donc pas de colonne supplémentaire à prévoir. Réservez au moins 255 caractères à la colonne, puisque l’algorithme par défaut peut changer avec les versions de PHP.

<?php
session_start();
require 'config/connexion.php'; // fournit $pdo
require 'service/Auth.php';
$auth = new Auth($pdo);
$erreur = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$courriel = trim($_POST['courriel'] ?? '');
$motDePasse = $_POST['motDePasse'] ?? '';
if ($courriel === '' || $motDePasse === '') {
$erreur = 'Veuillez remplir tous les champs.';
} elseif ($auth->connecter($courriel, $motDePasse)) {
header('Location: index.php');
exit;
} else {
$erreur = 'Courriel ou mot de passe invalide.';
}
}
// ... Code de l'interface (UI)

Savoir qui est connecté ne suffit pas : encore faut-il savoir ce que la personne a le droit de faire. On attribue donc un rôle à chaque compte, et certaines pages ou actions exigent un rôle particulier.

modeles/SessionUtilisateur.php
<?php
class SessionUtilisateur
{
public static function estConnecte(): bool
{
return isset($_SESSION['utilisateur_id']);
}
public static function role(): ?string
{
return $_SESSION['role'] ?? null;
}
public static function aLeRole(string $role): bool
{
return self::role() === $role;
}
}

L’affichage s’adapte au rôle, ce qui évite de proposer des actions interdites.

<?php if (SessionUtilisateur::aLeRole('admin')): ?>
<a href="zone-admin.php">Gérer les utilisateurs</a>
<?php endif; ?>

La vérification se fait dans la page, au tout début, avant la moindre sortie. Un fichier commun évite de la réécrire partout.

includes/protection.php
<?php
require_once __DIR__ . '/../modeles/SessionUtilisateur.php';
function exigerConnexion(): void
{
if (!SessionUtilisateur::estConnecte()) {
header('Location: connexion.php');
exit;
}
}
function exigerRole(string $role): void
{
exigerConnexion();
if (!SessionUtilisateur::aLeRole($role)) {
http_response_code(403);
exit('Accès refusé.');
}
}

Chaque page commence alors par une seule ligne.

<?php
// travaux-admin.php (page réservé aux admins)
session_start();
require 'includes/protection.php';
exigerRole('admin');
// À partir d'ici, la personne est connectée et administrateur.
  1. session_start() en tout premier, avant toute sortie.
  2. La protection : exigerConnexion() ou exigerRole(...).
  3. La lecture des paramètres et la validation de la requête.
  4. L’appel au modèle pour obtenir les données.
  5. L’inclusion de la vue, qui échappe tout ce qu’elle affiche.

L’authentification permet au serveur de savoir qui envoie une requête. Elle ne garantit cependant pas que cette personne a réellement voulu déclencher l’action.

Le navigateur joint automatiquement les témoins applicables à une requête. Un autre site pourrait donc tenter de faire envoyer à votre navigateur une requête vers une application où vous êtes déjà connecté. C’est une attaque de type CSRF (Cross-Site Request Forgery).

Pour les formulaires qui modifient des données, on peut ajouter un jeton aléatoire conservé dans la session et inclus dans le formulaire.

<?php
function jetonCsrf(): string
{
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf'];
}

Le formulaire renvoie ce jeton avec les autres données.

<input
type="hidden"
name="csrf"
value="<?= htmlspecialchars(jetonCsrf()) ?>"
>

Le serveur vérifie alors le jeton avant d’effectuer l’action.

<?php
$jetonRecu = $_POST['csrf'] ?? '';
$jetonAttendu = $_SESSION['csrf'] ?? '';
if ($jetonAttendu === '' || !hash_equals($jetonAttendu, $jetonRecu)) {
http_response_code(403);
exit('Requête refusée.');
}
// Le traitement de la requête peut continuer.
Vérification Pourquoi une session valide ne suffit-elle pas à empêcher une attaque CSRF ?

Parce que le navigateur peut joindre automatiquement le témoin de session à une requête. Le serveur reconnaît alors l’utilisateur, même si celui-ci n’a pas volontairement déclenché l’action. Le jeton CSRF ajoute une information secrète qu’un autre site ne connaît pas.

  • L’état d’authentification vit côté serveur, en session ; le navigateur ne détient que l’identifiant PHPSESSID, qui doit lui aussi être protégé.
  • Un témoin conserve une donnée chez le client. Les options httponly, secure et samesite réduisent certains risques, mais les décisions d’accès doivent être vérifiées côté serveur.
  • Les mots de passe se conservent en empreinte, avec password_hash et password_verify, jamais en clair ni avec md5 ou sha1.
  • À la connexion, session_regenerate_id(true) protège contre la fixation de session. À la déconnexion, on détruit la session et on fait expirer son témoin.
  • Un rôle conservé en session permet de décider des droits ; l’affichage peut s’y adapter, mais la vérification se fait toujours côté serveur.
  • Chaque page protégée commence par exiger la connexion ou le rôle, puis redirige (302) ou refuse l’accès (403).
  • Une requête sensible envoyée avec une session valide n’est pas nécessairement volontaire : un jeton CSRF permet de vérifier qu’elle provient bien d’un formulaire de l’application.

Quiz. Où doit-on conserver le rôle qui décide des droits d'un utilisateur ?

Quiz. Comment vérifie-t-on un mot de passe saisi à la connexion ?

Quiz. Pourquoi appeler session_regenerate_id(true) au moment de la connexion ?

Quiz. Un utilisateur connecté demande une page réservée aux enseignants. Que renvoyer ?

Quiz. Pourquoi ajoute-t-on un jeton CSRF à un formulaire qui modifie des données ?