Skip to content

Authentification

English version
Retour au sommaire


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)

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

Étapes suivantes