Bonnes pratiques
Sécurité, performance, architecture et observabilité : les règles d'or pour une intégration Wakapp robuste.
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.
textBrowser → /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
textBIEN : /users/WKST05 (lisible, stable) MAL : /users/550e8400-... (UUID interne)
Gestion d'erreurs
17. Réagir différemment à 401, 403, 429
typescriptasync 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 :
textwakapp_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_SECRETdans le pod