Files
varlog/docs/auth-magic-link.md
T
2026-05-09 13:24:36 +02:00

3.8 KiB

Authentification — Lien magique

Mécanisme d'authentification par email sans mot de passe. L'utilisateur reçoit un lien à usage unique, valide un temps limité, qui ouvre une session PHP.

Fichiers concernés

Fichier Rôle
public/login/index.php Formulaire de demande + génération du token
public/login/magic.php Consommation du token + ouverture de session
Table BDD auth_magic_links Persistance des tokens

Configuration (.env)

Variable Défaut Description
MAGIC_LINK_TTL_MINUTES 30 Durée de validité du lien
MAGIC_COOLDOWN_MINUTES 5 Délai minimal entre deux demandes pour le même email
MAGIC_WINDOW_HOURS 12 Fenêtre glissante pour le plafond
MAGIC_MAX_PER_WINDOW 5 Nombre maximal de liens émis par fenêtre
id          UUID        PRIMARY KEY (gen_random_uuid())
email       TEXT        NOT NULL
token       TEXT        NOT NULL UNIQUE
created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
expires_at  TIMESTAMPTZ NOT NULL
consumed_at TIMESTAMPTZ NULL      -- NULL = non consommé
ip          TEXT
user_agent  TEXT
return_to   TEXT        NULL      -- chemin relatif de redirection post-login

Phase 1 — Demande du lien (index.php, POST)

[Formulaire email] → POST /login
        │
        ├─ Validation CSRF
        ├─ Validation syntaxe email
        │
        ├─ Purge BDD : DELETE liens expirés ou consommés pour cet email
        │
        ├─ Cooldown : un lien < MAGIC_COOLDOWN_MINUTES → refus
        ├─ Plafond  : >= MAGIC_MAX_PER_WINDOW liens dans MAGIC_WINDOW_HOURS → refus
        │
        ├─ Génération token : random_bytes(32) → base64url (43 chars, URL-safe)
        ├─ INSERT auth_magic_links (expires_at = NOW() + TTL)
        │
        └─ [stub] Envoi email : /login/magic.php?token=<token>

Le token est un base64url sans padding (+/ remplacés par -_, = supprimés) — il ne contient que des caractères [A-Za-z0-9\-_].

L'envoi de l'email est actuellement un stub : le code construit $magicUrl mais l'appel SMTP n'est pas implémenté (index.php:142).

Phase 2 — Consommation du lien (magic.php, GET)

[Clic sur le lien] → GET /login/magic.php?token=<token>
        │
        ├─ Validation format token (regex [A-Za-z0-9\-\_])
        │
        ├─ BEGIN TRANSACTION
        │   ├─ SELECT ... FOR UPDATE  (verrou anti double-consommation)
        │   ├─ Token inconnu         → 400
        │   ├─ consumed_at non null  → "Lien déjà utilisé"
        │   ├─ expires_at dépassé    → "Lien expiré"
        │   ├─ UPDATE consumed_at = NOW()
        │   └─ COMMIT
        │
        ├─ session_start() + session_regenerate_id(true)
        ├─ $_SESSION['user_email'] = email
        │
        └─ Redirection 303 → return_to (validé : doit commencer par /)

Le FOR UPDATE garantit qu'un token ne peut pas être consommé deux fois en cas de double-clic ou de requêtes concurrentes.

Sécurités notables

  • CSRF sur le formulaire de demande
  • Rate limiting double (cooldown + plafond glissant) par email
  • Token URL-safe généré par CSPRNG (random_bytes)
  • Verrou transactionnel (FOR UPDATE) à la consommation
  • session_regenerate_id(true) — prévient la fixation de session
  • return_to filtré : seuls les chemins relatifs (commençant par /) sont acceptés

État actuel — ce qui reste à câbler

L'envoi SMTP n'est pas implémenté. Le token est correctement généré et persisté en BDD, mais le mail n'est pas envoyé. Voir public/login/index.php:142 :

$magicUrl = url('/login/magic.php') . '?token=' . urlencode($token);
/* envoyer_mail_smtp(...) ou mail(...) */