Authentification
Deux mécanismes d'authentification
L'application Racines utilise deux systèmes d'authentification distincts selon le rôle :
| Rôle | Mécanisme | Stockage session |
|---|---|---|
| Admin | Email + Mot de passe → JWT HS256 → Cookie HttpOnly | Cookie sécurisé (7 jours) |
| Locuteur | Code d'accès brut → vérification BD → contexte React | localStorage |
| Joueur | Code d'accès par langue → vérification BD → localStorage | localStorage |
Authentification Admin
Hachage des mots de passe (PBKDF2)
Les mots de passe ne sont jamais stockés en clair. L'algorithme utilisé est PBKDF2 (Password-Based Key Derivation Function 2).
Paramètres :
- Algorithme de hash : SHA256
- Itérations : 100 000
- Sel : aléatoire (généré par crypto.randomBytes(32))
- Longueur de la clé dérivée : 64 octets
Format stocké dans la colonne password_hash :
pbkdf2$100000$[sel_hex]$[hash_hex]
Comparaison : comparaison en temps constant via crypto.timingSafeEqual() pour prévenir les attaques par timing.
Code source : src/lib/auth.ts
Génération du JWT
Après vérification du mot de passe, un token JWT est généré :
// src/lib/auth.ts
const token = await new SignJWT({ admin_id: admin.id })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('7d')
.sign(secret);
Payload du JWT :
- admin_id : UUID de l'admin dans la table admins
- exp : timestamp d'expiration (J+7)
Clé de signature : JWT_SECRET (variable d'environnement, minimum 32 caractères)
Cookie de session
Le token est stocké dans un cookie HttpOnly :
| Attribut | Valeur | Rôle |
|---|---|---|
| Name | auth-token |
Identifiant du cookie |
| HttpOnly | true |
Inaccessible depuis JavaScript (protection XSS) |
| Secure | true en production |
HTTPS uniquement |
| SameSite | Lax |
Protection CSRF basique |
| Max-Age | 604800 (7 jours) |
Expiration automatique |
| Path | / |
Valide sur toutes les routes |
Middleware de protection
Les routes admin sont protégées via des wrappers dans src/lib/auth-middleware.ts :
// Usage dans une API route
export const GET = withAuth(async (request, { admin_id }) => {
// admin_id est vérifié et extrait du JWT
return Response.json({ data: ... });
});
withAuth(handler) : vérifie le cookie auth-token, décode le JWT, injecte admin_id dans le handler.
requireAuth(request) : version pour les Server Components — retourne le payload JWT ou null.
Flux complet
1. POST /api/auth/login
Body: { email, password }
2. Vérification email → table admins
3. Vérification mot de passe PBKDF2
4. Si OK → SignJWT({ admin_id })
5. Set-Cookie: auth-token=...
6. Requêtes admin suivantes :
Header Cookie: auth-token=...
→ Middleware vérifie JWT → extrait admin_id
Déconnexion
POST /api/auth/logout → supprime le cookie côté serveur (Max-Age=0).
Authentification Locuteur
Le locuteur s'authentifie via un code d'accès brut (pas de JWT).
Flux
1. POST /api/speaker/login
Body: { accessCode: "RACINES-XXXXX-2025" }
2. SELECT * FROM speakers WHERE access_code = $1 AND is_active = true
3. Si trouvé → retourne { id, name, languageId, languageCode, languageName, accessCode }
4. Le client stocke ces données dans localStorage (clé: "speaker_session")
Contexte React
src/contexts/SpeakerContext.tsx gère l'état de session locuteur :
- Chargement depuis localStorage au démarrage
- Exposition des données via useSpeakerContext() hook
- Déconnexion : suppression du localStorage
Format de la session (localStorage) :
{
"id": "uuid-du-locuteur",
"name": "Amadou Diallo",
"languageId": "uuid-de-la-langue",
"languageCode": "pul",
"languageName": "Pular",
"accessCode": "RACINES-XXXXX-2025"
}
Authentification Joueur (déverrouillage de langue)
1. POST /api/languages/verify-code
Body: { code: "RACINES-XXXXX-2025" }
2. SELECT id FROM languages WHERE access_code = $1 AND is_active = true
3. Si trouvé → retourne { languageId }
4. Le client stocke l'ID dans localStorage (via LanguageContext)
src/contexts/LanguageContext.tsx gère les langues déverrouillées :
- unlockedLanguages : tableau d'IDs de langues déverrouillées (localStorage)
- isAdmin : boolean si l'utilisateur est admin (flag localStorage, non sécurisé côté serveur pour les joueurs)
- isLanguageUnlocked(id) : retourne true si la langue est dans unlockedLanguages ou si isAdmin
Créer un compte administrateur
# Sur le serveur (accès SSH requis)
cd /app
npm run create-admin
# Le script demande :
# - Email : admin@example.com
# - Mot de passe : [masqué]
# - Confirmation : [masqué]
#
# Génère le hash PBKDF2 et l'insère dans la table admins
Code source : src/lib/create-admin.ts
Résumé de sécurité
| Menace | Mitigation |
|---|---|
| Injection SQL | Requêtes paramétrées (jamais de concaténation) |
| XSS → vol de token | Cookie HttpOnly (JS ne peut pas lire le cookie) |
| CSRF | SameSite=Lax + vérification Origin |
| Timing attacks | Comparaison timingSafeEqual() |
| Brute force | Rate limiting (à configurer via Traefik ou middleware) |
| Stockage mot de passe | PBKDF2 100k itérations + sel aléatoire |