WakaStart

Guide Développeur Wakapp

Comment intégrer une Wakapp avec la plateforme WakaStart : authentification, discovery, API, droits et bonnes pratiques.

Version v1.0

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

┌─────────────┐         ┌──────────────────┐         ┌──────────────┐
│   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 :

ModeUsageHeader
JWT BearerApps utilisateur (frontend)Authorization: Bearer <token>
API KeyServices backend, scriptsx-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

http
GET /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 logique
  • app_client_id — Keycloak clientId à passer au start-login (voir §2)
  • keycloak_org_alias — alias de l'organisation Keycloak du customer (utilisable comme kc_idp_hint)
  • idp_typeOIDC ou SAML (si IDP fédéré configuré)

VULN-002 (sprint IAM-S0)network_realm_id n'est plus exposé dans les réponses publiques. Le realm est résolu server-side par le BFF lors du start-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 Origin correspondant à 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

http
GET /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

http
GET /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-login qui résout le realm + le client_id à partir du host et retourne une URL Keycloak prête à suivre. Aucune Wakapp ne détient INTERNAL_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

http
POST {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 :

StatusBodyCause
400invalid_requestemail + host fournis tous les deux, ou aucun, ou body manquant
404not_foundAucun network ne match (latency padded ~80ms — anti-énumération)
429rate limit5/s · 20/10s · 100/60s par IP (hérité de Discovery)

Choix email vs host :

  • hostflow recommandé pour les Wakapps mono-tenant : extraire le subdomain depuis window.location.hostname.
  • email — flow Microsoft-style : utilisateur saisit son mail, Discovery résout son network.
  • Ne fournir qu'un des deux (invalid_request sinon).

Pas de appId à hardcoder — Discovery résout automatiquement le client_id depuis 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 :

typescript
function 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.subdomain peut 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 :

typescript
const { 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 codetokens chez Keycloak (server-side) :

http
POST 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 :

http
POST {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ètreValeur
Client IDla valeur de apps.client_id en DB (auto-généré du WID, ex: 7OWWAA)
Client authenticationOFF ← public client, indispensable pour PKCE sans secret
Standard flowON
Direct access grantsOFF (recommandé)
PKCE MethodS256
Valid redirect URIshttps://app.<tenant>.<platform>/*
Web originshttps://app.<tenant>.<platform> (pour CORS)

Client authentication: ON cause Invalid client or Invalid client credentials au 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 :

  1. Realm — feature Organizations activée : Console Keycloak → realm → Realm settings → General → "Organizations enabled"
  2. Client OIDC — scope organization attaché comme default : Automatisé via npx prisma db seed (ws-serv-config)
  3. User — membre d'une organisation Keycloak : Automatisé via npx ts-node prisma/backfill-keycloak-orgs.ts (idempotent)

2.6 Rafraîchissement

http
POST /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

http
POST /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.

http
GET /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éeJWT (cookie)/me (API)
Identité (email, sub)ouioui
Admin levelouioui
User levelouioui
Multi-tenant (pid/nid/cid)ouioui
Waka rolesnonoui
HDS rolesnonoui
Team rightsnonoui
App rights (188 codes)nonoui
Featuresnonoui
Profilsnonoui

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ôlePérimètre
CONFIGConfiguration plateforme
EXPLOITExploitation/opérations
DPOProtection des données
AUDITAudit et conformité
BILLINGFacturation
CYBERCybersé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 Keycloak
  • x-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) :

typescript
const 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

http
GET /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 CustomerAdmin ne voit que les données de son customer
  • Un PartnerAdmin voit tout son partner
  • Un WakaAdmin voit tout

Vous n'avez pas besoin de filtrer manuellement — le backend s'en charge.

5.4 Runtime config

http
GET /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

http
POST /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=50 max 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)

EndpointUsage
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

EndpointServiceUsage
POST /discover/start-loginDiscovery (public)Résout realm + client_id depuis le host, retourne keycloakUrl
POST keycloak/realms/{realm}/protocol/openid-connect/tokenKeycloakÉchange codetokens (PKCE, public client)
POST /api/auth/enrichws-back-wakastartEnrichit un access_token Keycloak en JWT HS256
POST /api/auth/token/refreshws-back-wakastartRefresh access_token + refresh_token + enriched
POST /api/auth/logoutws-back-wakastartInvalide le refresh_token côté Keycloak

Profil & droits

EndpointUsage
GET /meProfil complet + rôles + appRights
POST /token/authorizeVérification combinée de droits
POST /token/verifyDécoder et vérifier un token

CRUD (exemples)

EndpointUsage
GET /config/users?page=1&limit=50Lister les utilisateurs
GET /config/users/:widDétail utilisateur
GET /apps/:appWid/runtime-configConfig 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 organization est absent du token Keycloak → vérifier que le client OIDC a le scope organization attaché et que le user est membre d'une organisation Keycloak (cf. §2.5.2)
  • Le user en DB n'a pas de keycloak_id correspondant au sub du 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.

CauseDiagnosticFix
Mauvaise extraction du subdomainLogs 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 DBSELECT subdomain FROM networks WHERE wid='...'Mettre à jour le subdomain en DB
appId envoyé hardcodé ne match pas app.client_idLogs 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

CauseDiagnosticFix
Client OIDC en mode Client authentication: ONConsole Keycloak → Client → Capability configBasculer OFF (public client + PKCE)
Le frontend envoie un mauvais client_id au /token endpointLe 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 clientConsole Keycloak → Client → Access settingsAjouter l'URL exacte

9.3 « Token enrichi manquant » sur tous les appels API

CauseDiagnosticFix
Cookie wakastart_token absentDevTools → Application → CookiesL'appel /api/auth/enrich a échoué — vérifier la réponse HTTP
Token Keycloak sans claim organizationDé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'
CauseFix dans le Dockerfile de la Wakapp
COPY .next ./.next sans --chownAjouter --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_id en 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 organization attaché au client comme default scope (automatique via npx prisma db seed)
  • Une organisation Keycloak existe par customer (alias = customer.wid) (automatique via npx ts-node prisma/backfill-keycloak-orgs.ts)
  • Les users actifs sont membres de leur organisation respective

Côté Wakapp

  • Implémenter extractSubdomain qui strip le suffix plateforme (cf. §2.3)
  • Page login : POST /discover/start-login avec { host, redirectUri, codeChallenge, ... }pas d'appId hardcodé
  • Avant le redirect Keycloak : extraire client_id + realm depuis la keycloakUrl retournée et les stocker en sessionStorage
  • Handler /api/auth/callback : POST Keycloak /token avec PKCE + lecture sessionStorage pour client_id/realm
  • Après l'échange Keycloak : POST {WAKASTART_BACKEND_URL}/api/auth/enrich avec Authorization: Bearer <access_token>
  • Stocker les 3 cookies HttpOnly : keycloak_token, wakastart_token, refresh_token
  • Sur tout appel API proxifié, forwarder x-enriched-token depuis le cookie wakastart_token
  • Appeler GET /me après login et stocker dans un context
  • Implémenter useAppRights() pour l'affichage conditionnel
  • Charger runtime-config pour 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_SECRET dans le pod Wakapp