WakaStart
Sécurité

Bonnes pratiques — Sécurité

Tokens, secrets, HTTPS, CSP, logs : les règles de sécurité non négociables pour une Wakapp en production.

Version v1.03 min de lecture

Bonnes pratiques — Sécurité

Ce chapitre compile les patterns de sécurité éprouvés et les anti-patterns observés sur des intégrations réelles. À lire avant de commencer, à relire avant de merger.


1. Tokens en cookies HttpOnly uniquement

typescript
// BIEN : cookie HttpOnly, jamais accessible depuis JavaScript cookieStore.set("keycloak_token", token, { httpOnly: true, secure: true }); // MAL : accessible depuis JS, vulnérable aux XSS localStorage.setItem("token", token); sessionStorage.setItem("token", token); // acceptable UNIQUEMENT pour pkce_verifier pendant le flow

2. PKCE obligatoire — pas de flow implicite

Le flow implicite (response_type=token) est déprécié et interdit. PKCE S256 est l'unique flow supporté.

3. Pas de secrets côté client

typescript
// BIEN : API key dans une variable d'env côté serveur const apiKey = process.env.WAKASTART_API_KEY; // server-only // MAL : exposé dans le bundle frontend const apiKey = "sk_live_..."; // visible dans les DevTools

4. Backend vérifie toujours, frontend masque

typescript
// BIEN côté backend NestJS Wakapp @UseGuards(AppRightGuard) @RequireAppRight("users.delete") async deleteUser(@Param("id") id: string) { ... } // BIEN côté frontend (masquage UI uniquement) <Protected right="users.delete"> <DeleteButton /> </Protected> // MAL : vérification uniquement côté frontend if (user?.appRights.includes("users.delete")) { await deleteUser(id); // l'API l'autorisera de toute façon si le backend ne vérifie pas }

5. Messages d'erreur génériques côté login

typescript
// BIEN : ne révèle pas si l'email est connu catch (err) { setError("Impossible de se connecter. Vérifiez vos identifiants et réessayez."); } // MAL : révèle l'existence du compte catch (err) { if (err.status === 404) setError("Cet email n'existe pas dans notre système."); }

6. HTTPS en production, CSP renforcée

typescript
// next.config.ts const securityHeaders = [ { key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" }, { key: "X-Content-Type-Options", value: "nosniff" }, { key: "X-Frame-Options", value: "DENY" }, { key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self' 'unsafe-inline'; ..." }, { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, ];

7. Ne jamais logger les tokens

typescript
// BIEN : redacter le header Authorization dans les logs logger.info("API call", { url: apiUrl, headers: { ...headers, Authorization: "[REDACTED]", "x-enriched-token": "[REDACTED]" }, }); // MAL : token complet dans les logs logger.debug("Request headers", { headers }); // expose le Bearer

8. Ne jamais détenir INTERNAL_API_SECRET

Ce secret n'est utilisé qu'entre services internes Wakastellar. Aucune Wakapp tierce n'en a besoin et n'en aura jamais besoin.


Gestion des erreurs 401, 403, 429

typescript
async function apiFetch(url: string, options: RequestInit) { const res = await fetch(url, options); switch (res.status) { case 401: // Token expiré → tenter refresh, puis redirect login const refreshed = await tryRefreshTokens(); if (!refreshed) window.location.assign("/login"); break; case 403: // Droits insuffisants → afficher message explicite, ne pas retenter throw new ForbiddenError(await res.json()); case 429: // Rate limit → backoff exponentiel const retryAfter = parseInt(res.headers.get("Retry-After") ?? "2", 10); await sleep(retryAfter * 1000); return apiFetch(url, options); // 1 seul retry case 402: // Crédits insuffisants → rediriger vers achat router.push("/billing/credits"); break; default: if (!res.ok) throw new ApiError(res.status, await res.json()); } return res.json(); }

Tests de sécurité

21. Ne jamais tester contre la production

Utilisez un Customer de test dédié (avec CUSTOMER_WID=TST001) et une App de test.

22. Mocker les réponses Discovery et enrich en tests unitaires

typescript
// jest.setup.ts jest.mock("@/lib/api/discovery", () => ({ startLogin: jest.fn().mockResolvedValue({ keycloakUrl: "https://auth.test/realms/test/protocol/openid-connect/auth?client_id=TEST", }), }));

23. Tests d'intégration avec un compte service

Pour les tests e2e, utilisez une clé API dédiée (sk_test_...) avec des droits minimaux.


Checklist sécurité pré-production

  • Tokens stockés en cookies HttpOnly (jamais localStorage)
  • Proxy serveur implémenté (browser ne contacte pas directement l'API)
  • PKCE S256 (pas de flow implicite)
  • HTTPS avec HSTS et CSP
  • Logs sans tokens (Bearer et x-enriched-token redactés)
  • 401 → refresh → login (pas de boucle infinie)
  • 403 → message explicite à l'utilisateur
  • 429 → backoff + Retry-After respecté
  • Pas d'INTERNAL_API_SECRET dans le pod
  • Test avec différents niveaux admin (WakaAdmin, CustomerAdmin, User)

Aller plus loin