Guide Développeur Wakapp
Comment intégrer une Wakapp avec la plateforme WakaStart : authentification, discovery, API, droits et bonnes pratiques.
Guide Développeur Wakapp
Comment intégrer une Wakapp avec la plateforme WakaStart : authentification, discovery, API, droits et bonnes pratiques.
Table des matières
- Architecture
- 1. Discovery — Identifier l'utilisateur
- 2. Authentification — OAuth2 PKCE (server-side resolution)
- 3. Récupérer le contexte utilisateur — /me
- 4. Contrôle d'accès
- 5. Appeler l'API WakaStart
- 6. Bonnes pratiques
- 7. Référence rapide des endpoints
- 8. Erreurs courantes
- 9. Pièges classiques d'intégration
Architecture
┌─────────────┐ ┌──────────────────┐ ┌──────────────┐
│ Wakapp │────────>│ Discovery │ │ Keycloak │
│ (frontend) │ │ :3002 │ │ :8080 │
│ │ │ Pré-auth, │ │ OAuth2 OIDC │
│ │ │ lookup email/ │ │ │
│ │────────>│ subdomain │────────>│ │
│ │ └──────────────────┘ └──────────────┘
│ │ │
│ │<───────────────────────────────────────────┘ (code PKCE)
│ │
│ │ ┌──────────────────┐
│ │────────>│ Public API │
│ │ │ :3005 │
│ │ │ CRUD multi- │
│ │ │ tenant │
└─────────────┘ └──────────────────┘
│
│ POST /token/exchange → access_token + enriched_token
│ GET /me → profil + droits + rôles
│ GET /api/... → données métier
Deux modes d'accès :
| Mode | Usage | Header |
|---|---|---|
| JWT Bearer | Apps utilisateur (frontend) | Authorization: Bearer <token> |
| API Key | Services backend, scripts | x-api-key: sk_... |
1. Discovery — Identifier l'utilisateur
Avant toute authentification, votre Wakapp doit identifier l'organisation de l'utilisateur via le service Discovery.
1.1 Lookup par email
httpGET /discover/email?email=john@acme.com
json{ "users": [{ "user_email": "john@acme.com", "customer_wid": "ACM001", "customer_name": "Acme Corp", "customer_subdomain": "acme", "network_wid": "NET001", "network_subdomain": "acme-prod", "partner_wid": "PTR001", "idp_id": "uuid-idp", "idp_type": "OIDC", "keycloak_org_alias": "ACM001", "app_wid": "APP001", "app_name": "Acme Dashboard", "app_client_id": "acme-app", "app_url_front": "https://acme.example.com" }] }
Champs essentiels :
network_subdomain/customer_subdomain— identifie le tenant logiqueapp_client_id— Keycloak clientId à passer au start-login (voir §2)keycloak_org_alias— alias de l'organisation Keycloak du customer (utilisable commekc_idp_hint)idp_type—OIDCouSAML(si IDP fédéré configuré)
VULN-002 (sprint IAM-S0) —
network_realm_idn'est plus exposé dans les réponses publiques. Le realm est résolu server-side par le BFF lors dustart-login. Aucune Wakapp n'a besoin de connaître les realmIds.
Filtrage par app courante — Le service renvoie en priorité les comptes liés à l'app du caller (détectée via le header
Origincorrespondant àapp.url_front). Un attaquant qui posséderait le même email sur une autre app ne verra pas remonter ces comptes.
1.2 Lookup par subdomain
httpGET /discover/subdomain?subdomain=acme-prod
json{ "subdomain": "acme-prod", "result": { "type": "network", "network_wid": "NET001", "network_subdomain": "acme-prod", "partner_wid": "PTR001", "app_wid": "APP001", "app_client_id": "acme-app", "app_url_front": "https://acme.example.com" } }
Utile pour pré-remplir le contexte au chargement de la page de login. Comme pour /discover/email, network_realm_id n'est jamais exposé (VULN-002).
1.3 Lookup d'applications
httpGET /discover/apps/customer?customerId={uuid_or_wid} GET /discover/apps/user?userId={uuid} GET /discover/apps/network?networkId={uuid}
Retourne les apps accessibles. Utilisez-le après login pour afficher le catalogue d'apps.
1.4 Anti-énumération
IMPORTANT : Le service retourne toujours
{ users: [] }ou{ result: null }que l'email/subdomain existe ou non. Ne révélez jamais à l'utilisateur si son email est connu. Affichez toujours un message générique : "Vérifiez votre email et réessayez".
2. Authentification — OAuth2 PKCE (server-side resolution)
Sprint IAM-S0 / IAM-C4 — depuis mai 2026 — Le realm Keycloak n'est plus construit côté frontend. Discovery expose un endpoint public
POST /discover/start-loginqui résout le realm + le client_id à partir du host et retourne une URL Keycloak prête à suivre. Aucune Wakapp ne détientINTERNAL_API_SECRET.
Important architectural — Le BFF wakastart (
ws-back-wakastart) est dédié à l'app wakastart elle-même. Les Wakapps tierces ne passent PAS par lui pour le start-login. Elles parlent directement à Discovery, qui est le service IAM public mutualisé.
2.1 Flow complet
Wakapp frontend Wakapp /api/auth/start-login Discovery (public) Keycloak
─────────────── ──────────────────────────── ────────────────── ────────
1. User arrive sur l'app
2. Génère PKCE (verifier + S256)
3. POST /api/auth/start-login ───────►
POST → Discovery /discover/start-login ──►
résout host → network → app
construit URL Keycloak avec
?client_id=<app.client_id>
◄── { keycloakUrl } ──┤
◄── { keycloakUrl } ──────┤
4. Frontend EXTRAIT le `client_id` et le `realm` depuis keycloakUrl
et les stocke en sessionStorage (pour le callback step 7).
5. window.location.assign(keycloakUrl)
6. Keycloak login (password ou SSO IDP) ──────────────────────────────────►
Keycloak redirect ────────────────────────────────────────────── ◄──────
→ /api/auth/callback?code=xxx&state=xxx
7. POST /token (Keycloak directement) ────────────────────────────────────►
grant_type=authorization_code, client_id=<from sessionStorage>,
code, redirect_uri, code_verifier
← { access_token (RS256), refresh_token, expires_in, ... }
8. POST {WAKASTART_BACKEND_URL}/api/auth/enrich (BFF wakastart, public)
Authorization: Bearer {access_token}
← { token: <enriched HS256>, payload: { adminLevel, wadl, wakaRoles, ... } }
9. Set cookies HttpOnly :
- keycloak_token (RS256, vérifiable backend)
- wakastart_token (HS256 enrichi)
- refresh_token
10. Frontend lit /me, navigue vers le dashboard
2.2 Appel /discover/start-login
httpPOST {DISCOVERY_API_URL}/discover/start-login Content-Type: application/json { "host": "app.test", // OU "email": "john@acme.com" "redirectUri": "https://app.test.wakastart-dev.app/api/auth/callback", "state": "<uuid>", // CSRF "codeChallenge": "<base64url SHA256 du code_verifier>", "codeChallengeMethod": "S256" // OPTIONNEL : // "appId": "<client_id>" — utile UNIQUEMENT si plusieurs apps partagent // le même hostname. Sinon Discovery résout // automatiquement depuis le host (relation // Network → App 1:1 garantit l'unicité). }
Réponse 200 :
json{ "keycloakUrl": "https://auth.example.com/realms/<realm>/protocol/openid-connect/auth?client_id=<resolved>&..." }
Erreurs :
| Status | Body | Cause |
|---|---|---|
| 400 | invalid_request | email + host fournis tous les deux, ou aucun, ou body manquant |
| 404 | not_found | Aucun network ne match (latency padded ~80ms — anti-énumération) |
| 429 | rate limit | 5/s · 20/10s · 100/60s par IP (hérité de Discovery) |
Choix email vs host :
host— flow recommandé pour les Wakapps mono-tenant : extraire le subdomain depuiswindow.location.hostname.email— flow Microsoft-style : utilisateur saisit son mail, Discovery résout son network.- Ne fournir qu'un des deux (
invalid_requestsinon).
Pas de
appIdà hardcoder — Discovery résout automatiquement leclient_iddepuis le host.
2.3 Pattern URL & extraction du subdomain
Le subdomain Wakapp peut être multi-label (contient des points). La convention chez Wakastart :
{subdomain}.{platform-domain}
└─ ex: app.test.wakastart-dev.app
└────┬───┘ └──────┬─────┘
subdomain platform domain
Côté frontend Wakapp, fonction d'extraction recommandée :
typescriptfunction extractSubdomain(hostname: string): string { if (!hostname) return ""; // 1. Prefer explicit env var (configurable per deployment) const suffix = process.env.NEXT_PUBLIC_PLATFORM_DOMAIN_SUFFIX; // ex: "wakastart-dev.app" if (suffix && hostname.endsWith(`.${suffix}`)) { return hostname.slice(0, -1 * (suffix.length + 1)); } // 2. Fallback: strip the last 2 DNS labels const labels = hostname.split("."); if (labels.length <= 2) return ""; return labels.slice(0, -2).join("."); }
Exemples (suffix = wakastart-dev.app) :
app.test.wakastart-dev.app→"app.test"app.acme.wakastart-dev.app→"app.acme"wakatest.wakastart.app→"wakatest"localhost→""(mode dev local — demander le subdomain manuellement)
DB :
network.subdomainpeut contenir des points (app.test). La regex de validation côté ws-serv-config est^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$.
2.4 Callback — exchange + enrich
Au retour de Keycloak (/api/auth/callback?code=xxx&state=xxx) :
Étape 1 — Extraire client_id et realm depuis la keycloakUrl reçue à l'étape 2.2. Stockez-les en sessionStorage AVANT le redirect :
typescriptconst { keycloakUrl } = await response.json(); const parsed = new URL(keycloakUrl); const realm = parsed.pathname.match(/\/realms\/([^/]+)\//)?.[1]; const clientId = parsed.searchParams.get("client_id"); if (realm) sessionStorage.setItem("login_realm", realm); if (clientId) sessionStorage.setItem("login_client_id", clientId); window.location.assign(keycloakUrl);
Étape 2 — Échange code → tokens chez Keycloak (server-side) :
httpPOST keycloak/realms/{realm}/protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &client_id={clientId} ← celui stocké en sessionStorage &code={code} ← reçu en query param &redirect_uri={redirectUri} ← MÊME valeur qu'au start-login &code_verifier={codeVerifier} ← stocké en sessionStorage avant redirect
→ { access_token (RS256), refresh_token, expires_in, ... }
Étape 3 — Enrichissement obligatoire :
httpPOST {WAKASTART_BACKEND_URL}/api/auth/enrich Authorization: Bearer {access_token}
→ { token: "<enriched HS256>", payload: { adminLevel, wakaRoles, hdsRoles, wgrl, pid, nid, cid, aid, ... } }
Le token enrichi est obligatoire pour appeler les services downstream (sinon 401 « Token enrichi manquant »). Stockez-le dans un cookie HttpOnly nommé wakastart_token.
2.5 Pré-requis Keycloak — Client OIDC + Organizations
2.5.1 Client OIDC dédié à la Wakapp
| Paramètre | Valeur |
|---|---|
| Client ID | la valeur de apps.client_id en DB (auto-généré du WID, ex: 7OWWAA) |
| Client authentication | OFF ← public client, indispensable pour PKCE sans secret |
| Standard flow | ON |
| Direct access grants | OFF (recommandé) |
| PKCE Method | S256 |
| Valid redirect URIs | https://app.<tenant>.<platform>/* |
| Web origins | https://app.<tenant>.<platform> (pour CORS) |
Client authentication: ONcauseInvalid client or Invalid client credentialsau token exchange. Vérifie ce switch en premier en cas de 401 sur/api/auth/callback.
2.5.2 Organizations (sprint IAM-C4)
ws-serv-token rejette tout token Keycloak qui ne contient pas le claim organization (ou kc_org legacy). Trois conditions :
- Realm — feature Organizations activée : Console Keycloak → realm → Realm settings → General → "Organizations enabled"
- Client OIDC — scope
organizationattaché comme default : Automatisé vianpx prisma db seed(ws-serv-config) - User — membre d'une organisation Keycloak : Automatisé via
npx ts-node prisma/backfill-keycloak-orgs.ts(idempotent)
2.6 Rafraîchissement
httpPOST /api/auth/token/refresh Content-Type: application/json { "refreshToken": "xxx", "accessToken": "xxx" }
Le BFF gère le rafraîchissement transparent si vous appelez /api/proxy/... avec un access_token expiré.
Bonne pratique : rafraîchir proactivement quand exp - now < 60s, pas après un 401.
2.7 Logout
httpPOST /api/auth/logout Content-Type: application/json { "refreshToken": "xxx" }
Invalide le refresh token côté Keycloak. Pensez à supprimer aussi les cookies HttpOnly côté frontend.
3. Récupérer le contexte utilisateur — /me
Après l'authentification, appelez GET /me pour obtenir le profil complet de l'utilisateur, ses rôles et ses droits applicatifs.
httpGET /api/me Authorization: Bearer <access_token>
3.1 Réponse
json{ "id": "uuid", "wid": "WKST05", "email": "john@acme.com", "firstName": "John", "lastName": "Doe", "keycloakId": "uuid-keycloak", "isActive": true, "adminLevel": "CustomerAdmin", "userLevel": "ADMIN", "wakaRoles": ["CONFIG", "EXPLOIT"], "hdsRoles": [], "customer": { "id": "uuid", "wid": "ACM001", "name": "Acme Corp" }, "partner": { "id": "uuid", "wid": "PTR001", "name": "Wakastellar" }, "network": { "id": "uuid", "wid": "NET001", "name": "Production" }, "teams": [ { "id": "uuid", "wid": "TEAM01", "name": "DevOps", "teamLevel": "ADMIN", "appRights": "rw" } ], "profiles": [ { "id": "uuid", "wid": "PRF001", "name": "Administrateur" } ], "appRights": [ "partners.read", "users.read", "users.write", "apps.read", "infra.read", "infra.deploy" ], "features": [], "lastLoginAt": "2026-04-14T10:30:00Z", "createdAt": "2026-01-15T08:00:00Z" }
3.2 Quand appeler /me
- Au login (après token exchange) — stocker en mémoire (React context, store)
- Après un refresh token — recharger pour détecter des changements de droits
- Jamais à chaque navigation — utiliser les données en cache
3.3 Ce qui est dans /me vs dans le JWT
| Donnée | JWT (cookie) | /me (API) |
|---|---|---|
| Identité (email, sub) | oui | oui |
| Admin level | oui | oui |
| User level | oui | oui |
| Multi-tenant (pid/nid/cid) | oui | oui |
| Waka roles | non | oui |
| HDS roles | non | oui |
| Team rights | non | oui |
| App rights (188 codes) | non | oui |
| Features | non | oui |
| Profils | non | oui |
Le JWT ne contient que le minimum pour l'identification et le routage. Tout le reste est dans /me.
4. Contrôle d'accès
4.1 Côté backend — toujours vérifié par le token service
Le backend vérifie les droits à chaque requête :
- Le token enrichi est vérifié cryptographiquement (signature HS256)
- Les droits sont résolus côté serveur via les profils en DB
- Ne faites jamais confiance au frontend pour le contrôle d'accès
4.2 Côté frontend — affichage conditionnel
Les droits du /me servent uniquement à masquer/afficher des éléments d'UI.
Pattern React recommandé :
typescript// Stocker les droits dans un contexte const AuthContext = createContext<{ user: User | null }>({ user: null }); // Hook pour vérifier les droits function useAppRights() { const { user } = useContext(AuthContext); const appRights = user?.appRights ?? []; return { hasRight: (code: string) => appRights.includes(code), hasAnyRight: (...codes: string[]) => codes.some(c => appRights.includes(c)), hasAllRights: (...codes: string[]) => codes.every(c => appRights.includes(c)), }; } // Utilisation dans un composant function AppToolbar() { const { hasRight, hasAnyRight } = useAppRights(); return ( <div> {hasRight("apps.create") && <button>Nouvelle app</button>} {hasRight("apps.delete") && <button>Supprimer</button>} {hasAnyRight("infra.deploy", "infra.write") && <button>Déployer</button>} {hasRight("audit.read") && <Link to="/audit">Audit</Link>} </div> ); }
4.3 Hiérarchie des niveaux admin
WakaAdmin → Accès total plateforme
OwnerAdmin → Accès total plateforme (propriétaire)
AppsAdmin → Applications, profils
NetworkAdmin → Réseaux, clients
CustomerAdmin → Clients, utilisateurs, équipes
User → Accès basique
None → Aucun accès admin
Les niveaux sont hiérarchiques : un NetworkAdmin a aussi les droits de CustomerAdmin et User.
4.4 Rôles métier (Waka)
| Rôle | Périmètre |
|---|---|
| CONFIG | Configuration plateforme |
| EXPLOIT | Exploitation/opérations |
| DPO | Protection des données |
| AUDIT | Audit et conformité |
| BILLING | Facturation |
| CYBER | Cybersécurité |
4.5 Droits applicatifs (appRights)
Les droits suivent le pattern {ressource}.{action} :
partners.read users.create infra.deploy
partners.write users.update infra.services.scale
apps.delete teams.members.add audit.read
profiles.assign api-keys.revoke billing.manage
5. Appeler l'API WakaStart
5.1 Authentification des requêtes
Une requête vers les services downstream doit porter deux tokens :
Authorization: Bearer <access_token>— RS256 émis par Keycloakx-enriched-token: <wakastart_token>— HS256 émis par ws-serv-token (contient les rôles métier)
Sans x-enriched-token, les services répondent 401 « Token enrichi manquant ou invalide ».
Frontend (JWT Bearer + enriched token) :
typescript// Lecture des cookies HttpOnly côté serveur (Next.js / SSR proxy) const accessToken = cookieStore.get('keycloak_token')?.value; const enrichedToken = cookieStore.get('wakastart_token')?.value; const response = await fetch('https://api.wakastart.app/api/config/users', { headers: { 'Authorization': `Bearer ${accessToken}`, 'x-enriched-token': enrichedToken, // OBLIGATOIRE depuis IAM-S0 'Content-Type': 'application/json', }, });
Pattern recommandé — déléguez la propagation des tokens à un proxy serveur (route Next.js
/api/proxy/[...path]) qui lit les cookies HttpOnly et ajoute les headers automatiquement.
Backend (API Key) :
typescriptconst response = await fetch('https://api.wakastart.app/api/config/users', { headers: { 'x-api-key': 'sk_live_...', 'Content-Type': 'application/json', }, });
API keys remplacent à elles seules le couple Bearer + enriched-token.
5.2 Pagination
httpGET /api/config/users?page=1&limit=50
json{ "data": [...], "meta": { "total": 142, "page": 1, "limit": 50, "totalPages": 3 } }
5.3 Multi-tenant
Les données sont automatiquement filtrées selon le scope du token :
- Un
CustomerAdminne voit que les données de son customer - Un
PartnerAdminvoit tout son partner - Un
WakaAdminvoit tout
Vous n'avez pas besoin de filtrer manuellement — le backend s'en charge.
5.4 Runtime config
httpGET /api/apps/{appWid}/runtime-config
json{ "theme": { "id": "uuid", "name": "Default", "cssUrl": "https://..." }, "languages": [ { "code": "fr", "name": "Français", "isDefault": true, "i18nUrl": "https://..." } ], "branding": { "logoLightUrl": "https://...", "logoDarkUrl": "https://...", "faviconUrl": "https://..." } }
5.5 Vérification de droits depuis un backend Wakapp
httpPOST /api/token/authorize x-api-key: sk_... Content-Type: application/json { "token": "eyJ...", "checks": { "adminLevel": { "requiredLevel": "CustomerAdmin" }, "roles": { "roles": ["CONFIG", "AUDIT"], "mode": "any" } } }
json{ "authorized": true, "details": { "adminLevel": { "hasLevel": true, "actualLevel": "WakaAdmin" }, "roles": { "hasRoles": true, "matchedRoles": ["CONFIG"] } } }
6. Bonnes pratiques
Sécurité
- Tokens en cookies HttpOnly — jamais en localStorage
- PKCE obligatoire — pas de flow implicite
- Pas de secrets côté client — les API keys sont pour les backends uniquement
- Vérifiez les droits côté serveur — le frontend masque, le backend bloque
- Messages d'erreur génériques — ne révélez pas si un email existe
Performance
- Cachez /me — appelez une fois au login, stockez dans un React context
- Rafraîchissez proactivement — avant l'expiration, pas après un 401
- Utilisez runtime-config — un seul appel pour thème + langues + branding
- Pagination — ne chargez jamais tout (
limit=50max recommandé)
Architecture
- Séparez discovery et API — le discovery est pré-auth, l'API est post-auth
- Respectez la hiérarchie — Partner > Network > Customer > User
- Utilisez les WIDs — identifiants courts (6 chars) pour les URLs, pas les UUIDs
- Gérez les 401 — redirect vers login, ne bouclez pas sur le refresh
Anti-patterns
- Ne stockez pas les droits dans un cookie (trop gros, pas sécurisé)
- Ne hardcodez pas les niveaux admin dans le frontend (utilisez
/me) - Ne cachez pas les données sensibles côté client (audit logs, credentials)
- Ne faites pas de discovery sur chaque page (une fois au login suffit)
- Ne dupliquez pas la logique d'autorisation (le backend est la source de vérité)
7. Référence rapide des endpoints
Discovery (pré-auth, publics)
| Endpoint | Usage |
|---|---|
GET /discover/email?email= | Trouver l'organisation par email |
GET /discover/subdomain?subdomain= | Trouver l'organisation par subdomain |
GET /discover/apps/customer?customerId= | Apps d'un customer |
GET /discover/apps/user?userId= | Apps d'un utilisateur |
Auth
| Endpoint | Service | Usage |
|---|---|---|
POST /discover/start-login | Discovery (public) | Résout realm + client_id depuis le host, retourne keycloakUrl |
POST keycloak/realms/{realm}/protocol/openid-connect/token | Keycloak | Échange code → tokens (PKCE, public client) |
POST /api/auth/enrich | ws-back-wakastart | Enrichit un access_token Keycloak en JWT HS256 |
POST /api/auth/token/refresh | ws-back-wakastart | Refresh access_token + refresh_token + enriched |
POST /api/auth/logout | ws-back-wakastart | Invalide le refresh_token côté Keycloak |
Profil & droits
| Endpoint | Usage |
|---|---|
GET /me | Profil complet + rôles + appRights |
POST /token/authorize | Vérification combinée de droits |
POST /token/verify | Décoder et vérifier un token |
CRUD (exemples)
| Endpoint | Usage |
|---|---|
GET /config/users?page=1&limit=50 | Lister les utilisateurs |
GET /config/users/:wid | Détail utilisateur |
GET /apps/:appWid/runtime-config | Config runtime (thème, langues, branding) |
8. Erreurs courantes
401 — Token invalide ou expiré
json{ "statusCode": 401, "message": "Token enrichi manquant ou invalide", "error": "Unauthorized" }
Action : Tentez un refresh. Si le refresh échoue aussi → redirect vers login.
Cas spécifique « Token enrichi manquant » — causes fréquentes :
- Le claim
organizationest absent du token Keycloak → vérifier que le client OIDC a le scopeorganizationattaché et que le user est membre d'une organisation Keycloak (cf. §2.5.2)- Le user en DB n'a pas de
keycloak_idcorrespondant ausubdu JWT- ws-serv-token ne joint pas la DB ou son schéma Prisma local n'est pas synchro avec ws-serv-config
403 — Droits insuffisants
json{ "statusCode": 403, "message": "Niveau d'administration insuffisant. Requis: CustomerAdmin, Actuel: User", "error": "Forbidden" }
Action : L'utilisateur n'a pas les droits. Affichez un message explicite et ne retentez pas.
429 — Rate limit
json{ "statusCode": 429, "message": "Too Many Requests" }
Action : Attendez et retentez avec un backoff exponentiel.
402 — Crédits insuffisants
json{ "statusCode": 402, "message": "Insufficient credits" }
Action : Redirigez vers la page d'achat de crédits.
9. Pièges classiques d'intégration
9.1 « Aucun realm n'est associé à ce sous-domaine »
Le start-login retourne 404 not_found.
| Cause | Diagnostic | Fix |
|---|---|---|
| Mauvaise extraction du subdomain | Logs Discovery : host="app" au lieu de "app.test" | Adopter extractSubdomain qui strip le suffixe plateforme (cf. §2.3) |
network.subdomain NULL ou différent en DB | SELECT subdomain FROM networks WHERE wid='...' | Mettre à jour le subdomain en DB |
appId envoyé hardcodé ne match pas app.client_id | Logs Discovery : appId="wakatest-app" mais DB a client_id="7OWWAA" | Ne plus envoyer appId — Discovery résout depuis le host |
9.2 « Invalid client or Invalid client credentials » au callback
| Cause | Diagnostic | Fix |
|---|---|---|
Client OIDC en mode Client authentication: ON | Console Keycloak → Client → Capability config | Basculer OFF (public client + PKCE) |
Le frontend envoie un mauvais client_id au /token endpoint | Le sessionStorage login_client_id vaut un fallback type "wakastart" | Extraire client_id depuis la keycloakUrl retournée par Discovery avant le redirect |
Le redirect_uri envoyé ≠ exact Valid redirect URIs du client | Console Keycloak → Client → Access settings | Ajouter l'URL exacte |
9.3 « Token enrichi manquant » sur tous les appels API
| Cause | Diagnostic | Fix |
|---|---|---|
Cookie wakastart_token absent | DevTools → Application → Cookies | L'appel /api/auth/enrich a échoué — vérifier la réponse HTTP |
Token Keycloak sans claim organization | Décoder le JWT — chercher "organization" | Activer Organizations sur le realm + attacher le scope au client |
Header x-enriched-token pas forwardé | Capturer une requête : header présent ? | Lire le cookie wakastart_token et le mettre dans x-enriched-token |
9.4 Build Next.js du container Wakapp échoue par EACCES
Error: EACCES: permission denied, mkdir '/app/.next/cache/images'
| Cause | Fix dans le Dockerfile de la Wakapp |
|---|---|
COPY .next ./.next sans --chown | Ajouter --chown=nextjs:nodejs à tous les COPY du stage production |
Checklist d'intégration
Côté Keycloak (sprint IAM-C4 — pré-requis indispensable)
- Realm dédié au network créé (1 realm = 1 network)
- Realm — feature « Organizations » activée (Realm settings → General)
- Client OIDC créé (Client ID =
app.client_iden DB, généralement = WID auto-généré) - Client OIDC en mode public (Client authentication: OFF) avec PKCE S256
- Standard Flow ON, Direct access grants OFF
- Valid redirect URIs inclut l'URL frontale de la Wakapp +
/* - Web origins inclut le hostname (pour CORS)
- Scope
organizationattaché au client comme default scope (automatique vianpx prisma db seed) - Une organisation Keycloak existe par customer (alias =
customer.wid) (automatique vianpx ts-node prisma/backfill-keycloak-orgs.ts) - Les users actifs sont membres de leur organisation respective
Côté Wakapp
- Implémenter
extractSubdomainqui strip le suffix plateforme (cf. §2.3) - Page login : POST
/discover/start-loginavec{ host, redirectUri, codeChallenge, ... }— pas d'appIdhardcodé - Avant le redirect Keycloak : extraire
client_id+realmdepuis lakeycloakUrlretournée et les stocker en sessionStorage - Handler
/api/auth/callback: POST Keycloak/tokenavec PKCE + lecture sessionStorage pourclient_id/realm - Après l'échange Keycloak : POST
{WAKASTART_BACKEND_URL}/api/auth/enrichavecAuthorization: Bearer <access_token> - Stocker les 3 cookies HttpOnly :
keycloak_token,wakastart_token,refresh_token - Sur tout appel API proxifié, forwarder
x-enriched-tokendepuis le cookiewakastart_token - Appeler
GET /meaprès login et stocker dans un context - Implémenter
useAppRights()pour l'affichage conditionnel - Charger
runtime-configpour le thème et les langues - Gérer les erreurs 401 (refresh), 403 (droits), 429 (rate limit)
- Tester avec différents niveaux admin (WakaAdmin, CustomerAdmin, User)
- Vérifier l'anti-énumération sur la page de login (réponse normalisée)
- Valider que le backend bloque bien les actions non autorisées
- Ne JAMAIS détenir
INTERNAL_API_SECRETdans le pod Wakapp