Security
Best Practices — Security
Tokens, secrets, HTTPS, CSP, logs: the non-negotiable security rules for a Wakapp in production.
Version v1.03 min read
Best Practices — Security
This chapter compiles proven security patterns and anti-patterns observed on real integrations. Read before starting, re-read before merging.
1. Tokens in HttpOnly cookies only
typescript// GOOD: HttpOnly cookie, never accessible from JavaScript cookieStore.set("keycloak_token", token, { httpOnly: true, secure: true }); // BAD: accessible from JS, vulnerable to XSS localStorage.setItem("token", token); sessionStorage.setItem("token", token); // acceptable ONLY for pkce_verifier during the flow
2. PKCE mandatory — no implicit flow
The implicit flow (response_type=token) is deprecated and forbidden. PKCE S256 is the only supported flow.
3. No client-side secrets
typescript// GOOD: API key in a server-side env variable const apiKey = process.env.WAKASTART_API_KEY; // server-only // BAD: exposed in the frontend bundle const apiKey = "sk_live_..."; // visible in DevTools
4. Backend always verifies, frontend only hides
typescript// GOOD on the NestJS Wakapp backend side @UseGuards(AppRightGuard) @RequireAppRight("users.delete") async deleteUser(@Param("id") id: string) { ... } // GOOD on the frontend side (UI hiding only) <Protected right="users.delete"> <DeleteButton /> </Protected> // BAD: verification only on the frontend side if (user?.appRights.includes("users.delete")) { await deleteUser(id); // the API will allow it anyway if the backend doesn't check }
5. Generic error messages on login
typescript// GOOD: does not reveal whether the email is known catch (err) { setError("Unable to log in. Check your credentials and try again."); } // BAD: reveals the account's existence catch (err) { if (err.status === 404) setError("This email does not exist in our system."); }
6. HTTPS in production, hardened CSP
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. Never log tokens
typescript// GOOD: redact the Authorization header in logs logger.info("API call", { url: apiUrl, headers: { ...headers, Authorization: "[REDACTED]", "x-enriched-token": "[REDACTED]" }, }); // BAD: full token in the logs logger.debug("Request headers", { headers }); // exposes the Bearer
8. Never hold INTERNAL_API_SECRET
This secret is only used between internal Wakastellar services. No third-party Wakapp needs it, and never will.
Handling 401, 403, 429 errors
typescriptasync function apiFetch(url: string, options: RequestInit) { const res = await fetch(url, options); switch (res.status) { case 401: // Token expired → try refresh, then redirect to login const refreshed = await tryRefreshTokens(); if (!refreshed) window.location.assign("/login"); break; case 403: // Insufficient rights → show an explicit message, do not retry throw new ForbiddenError(await res.json()); case 429: // Rate limit → exponential backoff const retryAfter = parseInt(res.headers.get("Retry-After") ?? "2", 10); await sleep(retryAfter * 1000); return apiFetch(url, options); // only 1 retry case 402: // Insufficient credits → redirect to purchase router.push("/billing/credits"); break; default: if (!res.ok) throw new ApiError(res.status, await res.json()); } return res.json(); }
Security testing
21. Never test against production
Use a dedicated test Customer (with CUSTOMER_WID=TST001) and a test App.
22. Mock Discovery and enrich responses in unit tests
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. Integration tests with a service account
For e2e tests, use a dedicated API key (sk_test_...) with minimal rights.
Pre-production security checklist
- Tokens stored in HttpOnly cookies (never localStorage)
- Server-side proxy implemented (the browser does not contact the API directly)
- PKCE S256 (no implicit flow)
- HTTPS with HSTS and CSP
- Logs without tokens (Bearer and x-enriched-token redacted)
- 401 → refresh → login (no infinite loop)
- 403 → explicit message to the user
- 429 → backoff + Retry-After respected
- No
INTERNAL_API_SECRETin the pod - Tested with different admin levels (WakaAdmin, CustomerAdmin, User)
Going further
- Performance & Observability: cache, retry, metrics, tracing
- Common pitfalls: diagnosing frequent errors