Skip to content

How-to : Authentification par mot de passe avec Argon2id ​

Référence FT : FT331 (NENE2-FT/pwdlog) — Inscription et connexion utilisateur avec hachage Argon2id, mot de passe/hash jamais exposé dans les réponses, prévention de l'énumération d'utilisateurs (même 401 pour mauvais mot de passe et email inconnu), re-hachage lors de la migration d'algorithme, 14 tests / 40 assertions PASS.

Ce guide montre comment créer une authentification sécurisée par mot de passe : stocker les mots de passe de manière sûre avec Argon2id, ne jamais exposer les credentials dans les réponses, et empêcher les attaquants d'énumérer les adresses email enregistrées.

Schéma ​

sql
CREATE TABLE users (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    email         TEXT    NOT NULL UNIQUE,
    password_hash TEXT    NOT NULL,
    created_at    TEXT    NOT NULL
);

password_hash stocke la chaîne de sortie complète d'Argon2id (ex. $argon2id$v=19$m=65536,...). Ne jamais stocker en clair ni utiliser MD5/SHA-1.

Endpoints ​

MéthodeCheminDescription
POST/registerInscrire un nouvel utilisateur
POST/loginS'authentifier et retourner les données utilisateur

Inscription ​

php
POST /register
{"email": "alice@example.com", "password": "correct-horse"}

→ 201
{"id": 1, "email": "alice@example.com", "created_at": "2026-05-27T09:00:00Z"}

password et password_hash ne sont JAMAIS retournés dans la réponse — même masqués ou tronqués.

Validation ​

php
POST /register  {"email": "alice@example.com", "password": "court"}
→ 422  // mot de passe trop court (minimum 8 caractères)

POST /register  {"email": "pas-un-email", "password": "correct-horse"}
→ 422  // format email invalide

POST /register  {"email": "alice@example.com"}
→ 400  // champ password manquant

POST /register  {"email": "alice@example.com", "password": "battery-staple"}
// (après qu'alice est déjà inscrite)
→ 409  {"type": ".../email-taken", "detail": "Email already registered"}

Connexion ​

php
POST /login
{"email": "alice@example.com", "password": "correct-horse"}

→ 200
{"id": 1, "email": "alice@example.com", "created_at": "..."}
// password_hash non retourné

Prévention de l'énumération d'utilisateurs ​

php
// Mauvais mot de passe pour email connu
POST /login  {"email": "alice@example.com", "password": "mauvais"}
→ 401  {"type": ".../invalid-credentials", "detail": "Invalid email or password"}

// Email inconnu
POST /login  {"email": "fantome@example.com", "password": "n'importe"}
→ 401  {"type": ".../invalid-credentials", "detail": "Invalid email or password"}

Les deux cas retournent le même 401 avec le message detail identique. Retourner 404 pour un email inconnu permettrait aux attaquants de sonder la base de données utilisateurs.

php
// Test : même chaîne detail
$this->assertSame($wrongPasswordBody['detail'], $unknownEmailBody['detail']);

Implémentation ​

Stockage du mot de passe — Argon2id ​

php
// Inscription
$hash = password_hash($plaintext, PASSWORD_ARGON2ID);
// Stocke : $argon2id$v=19$m=65536,t=4,p=1$...

// Ne jamais stocker :
// md5($plaintext)          — réversible en secondes
// sha1($plaintext)         — attaque par table arc-en-ciel
// $plaintext               — stockage en clair

password_hash(PASSWORD_ARGON2ID) de PHP :

  • Génère automatiquement un sel aléatoire par hash
  • Stocke l'algorithme, les paramètres, le sel et le condensé dans une seule chaîne
  • Résiste au brute force GPU (résistant à la mémoire)

Vérification — Temps constant ​

php
$row = $this->repo->findByEmail($email);

if ($row === null || !password_verify($plaintext, $row['password_hash'])) {
    // Même réponse que l'email soit inconnu ou le mot de passe incorrect
    return $this->problems->create('invalid-credentials', 'Invalid email or password', 401);
}

password_verify() est à temps constant et fonctionne sur toutes les familles d'algorithmes (bcrypt, Argon2id, etc.).

Re-hachage lors de la migration d'algorithme ​

Lors de la mise à niveau de bcrypt vers Argon2id, re-hacher à la connexion réussie :

php
if (password_needs_rehash($row['password_hash'], PASSWORD_ARGON2ID)) {
    $newHash = password_hash($plaintext, PASSWORD_ARGON2ID);
    $this->repo->updateHash($row['id'], $newHash);
}

Les utilisateurs sont silencieusement migrés vers l'algorithme plus fort la prochaine fois qu'ils se connectent — pas de réinitialisation de mot de passe forcée requise.

Ne jamais retourner les credentials ​

php
private function toPublic(array $user): array
{
    // Supprimer explicitement les champs sensibles
    unset($user['password_hash']);
    return $user;
}

Appliquer toPublic() à chaque réponse : inscription 201, connexion 200, et tout endpoint de profil.


À ne pas faire ​

Anti-patternRisque
Retourner 404 pour email inconnu à la connexionÉnumération d'utilisateurs : l'attaquant découvre quels emails sont enregistrés
Retourner un message detail différent pour mauvais mot de passe vs email inconnuRévèle quelle condition a échoué
Stocker le mot de passe en MD5 ou SHA-1Attaque par table arc-en-ciel casse tous les mots de passe en quelques heures
Stocker le mot de passe en bcrypt sans chemin de migrationImpossible de passer à un algorithme plus fort sans réinitialisation forcée
Retourner password_hash dans n'importe quelle réponseLe hash peut être utilisé pour un brute force hors ligne
Ignorer password_needs_rehash() à la connexionLes hashes faibles hérités persistent indéfiniment même après la mise à niveau d'algorithme
Utiliser === pour comparer les hashesAttaque de timing révèle les octets du hash ; toujours utiliser password_verify()

Publié sous licence MIT.