Magic Link Authentication
Authentification passwordless pour les clients finaux. L'acheteur saisit son email, reçoit un lien à usage unique, clique, et accède à
/account(historique de commandes, reçus PDF, status d'abonnement). Aucun mot de passe, aucune table users séparée, aucun OAuth.
Concept
Trackily Commerce n'a pas de système de comptes clients au sens classique : pas d'inscription, pas de mot de passe à mémoriser, pas de reset password. À la place :
- Le buyer va sur
/account(lien dans le footer de la landing, dans le reçu, dans les emails). - Il colle son email dans le formulaire et clique "Get my link".
- Trackily génère un token signé HMAC (TTL 30 minutes, single-use), l'enregistre côté DB, et envoie un email contenant
https://landing-domain.com/account/auth?token=<token>. - Le buyer clique le lien :
/account/authvalide le token, set un cookie de session HMAC-signé (TTL 30 jours), et redirige vers/account. - Le cookie sert d'authentification pour
/api/account/me,/api/account/orders/:id/receipt.pdf, et les autres endpoints customer.
Ce flow est stateless côté session : pas de table sessions à maintenir. Le cookie est un blob email|expires.signature signé avec SECRETS_MASTER_KEY — on vérifie la signature à chaque request, on lit l'email signé. Si le timestamp dépasse expires, le cookie est invalide.
Pourquoi ce design
Trois critères justifient l'absence de mot de passe :
- Volume faible. Un client e-commerce visite
/account1 ou 2 fois par an, max. La friction d'un password reset à chaque visite serait pire que la friction du magic-link. - Sécurité. Pas de password = pas de fuite de password = pas de credential stuffing depuis HaveIBeenPwned. Le seul vecteur d'attaque, c'est l'accès au mailbox du buyer — c'est aussi le vecteur de tous les password reset par email, donc on ne perd rien.
- Pas de surface admin. Pas de page "register / login / forgot", pas de validation de mot de passe complexe, pas de hashing à maintenir. -200 lignes de code vs un système classique.
Endpoints HTTP
| Méthode | Route | Auth | Rôle |
|---|---|---|---|
POST |
/api/account/magic-link |
publique | génère le token + envoie le mail |
GET |
/account/auth?token=<token> |
publique | consomme le token, set cookie, redirige /account |
GET |
/account |
publique (renvoie HTML) | sert public/account.html |
GET |
/api/account/me |
cookie | retourne { email, total_orders, total_spent, orders[] } |
POST |
/api/account/logout |
cookie | clear cookie |
GET |
/api/account/orders/:id/receipt.pdf |
cookie + email match | génère / sert le PDF du reçu |
POST /api/account/magic-link
POST /api/account/magic-link HTTP/1.1
Content-Type: application/json
{ "email": "marie@example.com" }
Réponse toujours 200 { ok: true }, même si l'email n'existe pas en DB. C'est volontaire (anti-énumération) : un attaquant ne doit pas pouvoir tester quels emails ont des comptes en spamming l'endpoint.
Rate-limited via pixelRateLimit (par défaut ~10/minute par IP).
GET /account/auth?token=<token>
GET /account/auth?token=AbCdEfGhIjKlMnOpQrStUvWxYz HTTP/1.1
Si le token est valide et non-expiré :
- Consomme le token (marqué
consumed_at = NOW()côté DB — single-use) - Set le cookie
tly_customer_session(httpOnly, sameSite=Lax, secure si HTTPS) - Redirige 302 vers
/account
Si invalide / expiré :
<p>This sign-in link is invalid or has expired.
<a href="/account">Request a new one</a>.</p>
GET /api/account/me
Retourne le profil + lifetime stats si cookie valide :
{
"ok": true,
"email": "marie@example.com",
"total_orders": 4,
"total_spent": "187.50",
"orders": [
{
"id": 142,
"order_number": "TR-000142",
"total": "59.00",
"currency": "EUR",
"payment_status": "paid",
"created_at": "2026-04-22T14:22:11.421Z"
}
]
}
Si pas de cookie / cookie invalide : 401 { ok: false, error: "not_authenticated" }.
SMTP utilisé
Le magic-link envoie via le SMTP server marqué is_default=true (ou le premier is_active=true si aucun n'est default). C'est intentionnellement séparé du SMTP "marketing" : tu peux brancher Postmark pour les magic-links (excellente délivrabilité transactionnelle) et Mailgun pour les sequences marketing.
Pour vérifier la config :
{ "name": "list_email_smtp_servers", "arguments": {} }
Si aucun SMTP n'est configuré, l'endpoint loggue le lien dans la console serveur (dev fallback) au lieu de l'envoyer par mail :
[customer-magic-link] No SMTP configured. Link for marie@example.com: https://brand.com/account/auth?token=AbCd...
Le template d'email
Hard-coded dans server.js (volontairement simple, pas de templating loader pour réduire le risque d'injection) :
<p>Hi,</p>
<p>Click the link below to access your order history.
This link expires in 30 minutes and can only be used once.</p>
<p><a href="<LINK>" style="display:inline-block;padding:10px 18px;
background:#6366f1;color:#fff;text-decoration:none;border-radius:6px;
font-weight:600">Open my account</a></p>
<p style="color:#94a3b8;font-size:12px">
If you didn't request this, just ignore this email.</p>
Subject : Your sign-in link. From / Reply-To : pris du SMTP server utilisé.
La page /account
Servie depuis public/account.html — un seul fichier, autonomous, qui :
- Si pas de cookie : affiche le form "Enter your email" qui POST sur
/api/account/magic-link - Si cookie valide : appelle
GET /api/account/me, affiche la liste des orders avec lien "Download PDF" par order
Pas de framework JS, juste du fetch + vanilla. Compatible avec n'importe quelle landing brandée — le branding (brand.color, brand.logo_url) est récupéré via un endpoint séparé.
Domaines custom
Si l'opérateur a configuré un domaine custom pour la landing (example-shop.com), le buyer arrive sur https://example-shop.com/account. Le magic-link envoie un lien sur le même domaine (la route /api/account/magic-link lit req.get('origin') ou req.get('host')) — pas de cross-domain weirdness.
Conséquence : le cookie est par-domaine. Un buyer qui achète via shop-fr.com puis via shop-de.com aura deux sessions séparées.
Sécurité
| Vecteur | Mitigation |
|---|---|
| Token guessing | 32 bytes random base64 → 256 bits d'entropie |
| Token replay | consumed_at côté DB → single-use enforcé |
| Token expiration | TTL 30 minutes hard-coded |
| Cookie tampering | HMAC-SHA256 sur `email |
| Cookie theft via XSS | httpOnly cookie → JS ne peut pas le lire |
| MITM | secure cookie quand req.protocol === 'https' |
| CSRF sur logout | OK : POST simple, cookie sent → server clear cookie. Pas d'action critique sur logout. |
| Email enumeration | endpoint retourne toujours 200 |
Voir aussi
- SMTP — d'où part le lien
- API — index — autres endpoints HTTP de Trackily
- Admin UI — index — vue d'ensemble (le magic-link est côté public, pas dans l'admin)