Aller au contenu

Authentication

Version française
Back to index


Two authentication mechanisms

The Racines application uses two distinct authentication systems depending on the role:

Role Mechanism Session storage
Admin Email + Password → JWT HS256 → HttpOnly Cookie Secure cookie (7 days)
Speaker Raw access code → DB lookup → React context localStorage
Player Per-language access code → DB lookup → localStorage localStorage

Admin Authentication

Password hashing (PBKDF2)

Passwords are never stored in plain text. The algorithm used is PBKDF2 (Password-Based Key Derivation Function 2).

Parameters: - Hash algorithm: SHA256 - Iterations: 100,000 - Salt: random (generated by crypto.randomBytes(32)) - Derived key length: 64 bytes

Format stored in the password_hash column:

pbkdf2$100000$[salt_hex]$[hash_hex]

Comparison: constant-time comparison via crypto.timingSafeEqual() to prevent timing attacks.

Source code: src/lib/auth.ts

JWT generation

After password verification, a JWT token is generated:

// src/lib/auth.ts
const token = await new SignJWT({ admin_id: admin.id })
  .setProtectedHeader({ alg: 'HS256' })
  .setExpirationTime('7d')
  .sign(secret);

JWT payload: - admin_id: UUID of the admin in the admins table - exp: expiration timestamp (D+7)

Signing key: JWT_SECRET (environment variable, minimum 32 characters)

The token is stored in an HttpOnly cookie:

Attribute Value Role
Name auth-token Cookie identifier
HttpOnly true Inaccessible from JavaScript (XSS protection)
Secure true in production HTTPS only
SameSite Lax Basic CSRF protection
Max-Age 604800 (7 days) Automatic expiration
Path / Valid on all routes

Protection middleware

Admin routes are protected via wrappers in src/lib/auth-middleware.ts:

// Usage in an API route
export const GET = withAuth(async (request, { admin_id }) => {
  // admin_id is verified and extracted from the JWT
  return Response.json({ data: ... });
});

withAuth(handler): verifies the auth-token cookie, decodes the JWT, injects admin_id into the handler.

requireAuth(request): version for Server Components — returns the JWT payload or null.

Complete flow

1. POST /api/auth/login
   Body: { email, password }

2. Email lookup → admins table
3. PBKDF2 password verification
4. If OK → SignJWT({ admin_id })
5. Set-Cookie: auth-token=...

6. Subsequent admin requests:
   Header Cookie: auth-token=...
   → Middleware verifies JWT → extracts admin_id

Logout

POST /api/auth/logout → deletes the cookie server-side (Max-Age=0).


Speaker Authentication

The speaker authenticates via a raw access code (no JWT).

Flow

1. POST /api/speaker/login
   Body: { accessCode: "RACINES-XXXXX-2025" }

2. SELECT * FROM speakers WHERE access_code = $1 AND is_active = true
3. If found → returns { id, name, languageId, languageCode, languageName, accessCode }
4. The client stores this data in localStorage (key: "speaker_session")

React context

src/contexts/SpeakerContext.tsx manages the speaker session state: - Loading from localStorage at startup - Exposing data via the useSpeakerContext() hook - Logout: deletion from localStorage

Session format (localStorage):

{
  "id": "speaker-uuid",
  "name": "Amadou Diallo",
  "languageId": "language-uuid",
  "languageCode": "pul",
  "languageName": "Pular",
  "accessCode": "RACINES-XXXXX-2025"
}

Player Authentication (language unlocking)

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. If found → returns { languageId }
4. The client stores the ID in localStorage (via LanguageContext)

src/contexts/LanguageContext.tsx manages unlocked languages: - unlockedLanguages: array of unlocked language IDs (localStorage) - isAdmin: boolean if the user is an admin (localStorage flag, not server-side secure for players) - isLanguageUnlocked(id): returns true if the language is in unlockedLanguages or if isAdmin


Creating an administrator account

# On the server (SSH access required)
cd /app
npm run create-admin

# The script prompts for:
# - Email: admin@example.com
# - Password: [hidden]
# - Confirmation: [hidden]
# 
# Generates the PBKDF2 hash and inserts it into the admins table

Source code: src/lib/create-admin.ts


Security summary

Threat Mitigation
SQL injection Parameterized queries (no string concatenation)
XSS → token theft HttpOnly cookie (JS cannot read the cookie)
CSRF SameSite=Lax + Origin verification
Timing attacks timingSafeEqual() comparison
Brute force Rate limiting (to configure via Traefik or middleware)
Password storage PBKDF2 100k iterations + random salt

Next steps