WakaStart

Bonnes pratiques

Sécurité, performance, architecture et observabilité : les règles d'or pour une intégration Wakapp robuste.

Version v1.06 min de lecture

Bonnes pratiques

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


Sécurité

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.


Performance

9. Cacher /me — un seul appel au login

typescript
// BIEN : stocker dans un React context, ne jamais re-fetcher à la navigation const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); useEffect(() => { // Appelé une seule fois au mount du provider (= au login) fetchMe().then(setUser); }, []); return <AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>; }; // MAL : re-fetcher /me à chaque page/composant const UserPage = () => { const { data: user } = useQuery({ queryKey: ["me"], queryFn: fetchMe }); // refetch sur chaque mount };

10. Refresh proactif avant l'expiration

typescript
// BIEN : vérifier exp - now < 60s et rafraîchir avant l'expiration const REFRESH_THRESHOLD = 60; // secondes function scheduleTokenRefresh(expiresIn: number) { const refreshIn = Math.max(0, expiresIn - REFRESH_THRESHOLD) * 1000; setTimeout(async () => { await refreshTokens(); }, refreshIn); } // MAL : rafraîchir après un 401 (le call a déjà échoué, UX dégradée) catch (err) { if (err.status === 401) await refreshTokens(); // trop tard }

11. Pagination — ne jamais charger tout

typescript
// BIEN : paginer, limit max = 100 const { data } = await apiGet("config/users", { page: "1", limit: "50" }); // MAL : tenter de tout charger const { data } = await apiGet("config/users", { page: "1", limit: "10000" }); // timeout

12. TanStack Query — staleTime adapté à la criticité

typescript
// Données qui changent rarement (branding, runtime-config) : 5 minutes useQuery({ queryKey: ["runtime-config"], queryFn: fetchRuntimeConfig, staleTime: 5 * 60 * 1000, }); // Données qui changent souvent (users, invitations) : 30s useQuery({ queryKey: ["users", page], queryFn: () => fetchUsers(page), staleTime: 30_000, });

Architecture

13. Proxy serveur pour tous les appels API

Le browser ne doit jamais appeler directement la Public API.

text
Browser → /api/proxy/[...path] (route Next.js) → Public API └── lit les cookies HttpOnly └── ajoute Authorization + x-enriched-token └── forward la réponse

14. Séparer Discovery et API

typescript
// BIEN : Discovery utilisé une seule fois, avant l'auth const { keycloakUrl } = await discoverStartLogin({ host }); window.location.assign(keycloakUrl); // MAL : appeler Discovery après chaque login ou sur chaque page useEffect(() => { fetchDiscovery(email); // inutile après auth }, []);

15. Respecter la hiérarchie dans votre modèle de données

typescript
// Vos entités doivent refléter la hiérarchie Wakastart interface AppContext { partnerId: string; // pid du wakaToken networkId: string; // nid customerId: string; // cid appId: string; // votre app (WID) }

16. Utiliser les WIDs dans les URLs

text
BIEN : /users/WKST05 (lisible, stable) MAL : /users/550e8400-... (UUID interne)

Gestion d'erreurs

17. Réagir différemment à 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(); }

Observabilité

18. Propager le request-id / x-trace-id

typescript
// Inclure l'ID de trace dans toutes vos requêtes API const traceId = crypto.randomUUID(); headers["x-trace-id"] = traceId; // Logger côté frontend avec le même ID logger.info("API call", { traceId, url, method });

19. Métriques recommandées

Exposez des métriques Prometheus depuis votre Wakapp :

text
wakapp_api_calls_total{endpoint, status} wakapp_token_refresh_total{success} wakapp_invitations_created_total{customer} wakapp_invitations_accepted_total{customer} wakapp_auth_flow_duration_seconds (histogram)

20. Structured logging

typescript
// BIEN : JSON structuré, parsable par Loki / CloudWatch logger.info({ event: "user_login_success", userId: user.wid, customerId: user.customer.wid, adminLevel: user.adminLevel, duration_ms: Date.now() - startTime, }); // MAL : texte libre impossible à filtrer console.log(`User ${email} logged in successfully in ${elapsed}ms`);

Tests

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 aux tests (sk_test_...) avec des droits minimaux.


Checklist 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)
  • Refresh proactif implémenté (threshold 60s)
  • Rate limit géré (backoff exponentiel)
  • 401 → refresh → login (pas de boucle infinie)
  • 403 → message explicite à l'utilisateur
  • 429 → backoff + Retry-After respecté
  • /me chargé une fois, mis en cache, invalidé au refresh
  • runtime-config chargé une fois au démarrage
  • Checklist Keycloak complète (voir chapitre 03)
  • Test avec différents niveaux admin (WakaAdmin, CustomerAdmin, User)
  • Pas d'INTERNAL_API_SECRET dans le pod

Cette page vous a-t-elle été utile ?