Discovery — Identifier l'utilisateur
Utiliser le service Discovery pour résoudre l'organisation d'un utilisateur avant l'authentification.
Discovery — Identifier l'utilisateur
Le service Discovery est le gardien de la pré-authentification. Il résout l'organisation d'un utilisateur (ou d'un hostname) sans jamais exposer d'informations sensibles comme les realm IDs Keycloak.
Pourquoi cette étape
Avant toute authentification OAuth2, votre Wakapp doit savoir vers quel realm Keycloak rediriger l'utilisateur. Dans une architecture multi-tenant, chaque Network a son propre realm. Discovery résout cette correspondance de façon sécurisée, côté serveur, sans que votre code n'ait à connaître les identifiants internes de Keycloak.
C'est aussi le seul service public accessible sans token — par définition, car l'utilisateur n'est pas encore authentifié.
Concepts clés
- Subdomain : chaîne courte identifiant un Network ou Customer (
app.test,acme-prod). Peut contenir des points. - WID : identifiant court (6 chars alphanum) utilisé pour les entités (
NET001,ACM001). - Anti-énumération : toutes les réponses négatives ont une latence normalisée (~80ms) pour empêcher le timing-based user enumeration.
- Realm : concept Keycloak — 1 Network = 1 realm. Le realm ID n'est jamais exposé aux Wakapps (VULN-002).
Les 3 patterns disponibles
Pattern 1 — start-login par hostname (recommandé)
Le flow recommandé pour les Wakapps mono-tenant. Discovery résout automatiquement le client_id Keycloak à partir du hostname.
httpPOST {DISCOVERY_API_URL}/discover/start-login Content-Type: application/json { "host": "app.test", "redirectUri": "https://app.test.wakastart-dev.app/api/auth/callback", "state": "550e8400-e29b-41d4-a716-446655440000", "codeChallenge": "<base64url(SHA256(code_verifier))>", "codeChallengeMethod": "S256" }
json{ "keycloakUrl": "https://auth.example.com/realms/<realm>/protocol/openid-connect/auth?client_id=<resolved>&redirect_uri=...&state=...&code_challenge=...&code_challenge_method=S256" }
Avantages :
- Zéro connaissance du realm ou du
client_idcôté Wakapp. - Discovery résout host → Network → App → client_id automatiquement.
- Élimine le couplage avec la convention de nommage Keycloak (auto-généré du WID, ex:
7OWWAA).
Contraintes : le hostname doit correspondre au subdomain enregistré en base pour le Network cible.
Pattern 2 — start-login par email (Microsoft-style)
L'utilisateur saisit son email. Discovery identifie son organisation et retourne la même keycloakUrl.
httpPOST {DISCOVERY_API_URL}/discover/start-login Content-Type: application/json { "email": "john@acme.com", "redirectUri": "https://app.test.wakastart-dev.app/api/auth/callback", "state": "550e8400-e29b-41d4-a716-446655440000", "codeChallenge": "<base64url(SHA256(code_verifier))>", "codeChallengeMethod": "S256" }
Filtrage de sécurité : Discovery ne retourne les comptes que si l'email est associé à l'App correspondant à l'Origin de la requête (app.url_front). Un utilisateur avec le même email sur une autre app ne remontera pas.
Quand utiliser : si votre Wakapp héberge plusieurs tenants sur le même hostname, ou si vous implémentez une page de login commune type portail.
Ne fournissez jamais
hostsimultanément — la réponse sera400 invalid_request.
Pattern 3 — Lookup direct (informationnel)
Pour afficher des infos avant le login (logo, nom de l'organisation, IDP type) ou après login pour le catalogue d'apps.
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 :
| Champ | Usage |
|---|---|
network_subdomain / customer_subdomain | Identifie le tenant logique |
app_client_id | Keycloak clientId (utile si vous voulez forcer un appId dans start-login) |
keycloak_org_alias | Alias de l'organisation Keycloak (customer.wid), utilisable comme kc_idp_hint |
idp_type | OIDC ou SAML (si IDP fédéré configuré) |
network_realm_idn'est jamais exposé (VULN-002). Le realm est résolu server-side par Discovery.
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 (logo, nom) au chargement de la page de login. La réponse est { result: null } si le subdomain est inconnu — même format, latence normalisée.
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 applications accessibles pour une entité donnée. À utiliser après login pour construire un catalogue d'apps ou un menu de navigation inter-apps.
Extraction du subdomain
Le subdomain peut être multi-label (contient des points). Convention de nommage :
text{subdomain}.{platform-domain} app.test.wakastart-dev.app └──┬───┘ └──────┬─────┘ subdomain platform domain
Fonction d'extraction recommandée (TypeScript) :
typescriptfunction extractSubdomain(hostname: string): string { if (!hostname) return ""; // Méthode 1 : variable d'env (recommandée) const suffix = process.env.NEXT_PUBLIC_PLATFORM_DOMAIN_SUFFIX; // ex: "wakastart-dev.app" if (suffix && hostname.endsWith(`.${suffix}`)) { return hostname.slice(0, -(suffix.length + 1)); } // Méthode 2 : fallback — strip les 2 derniers labels DNS const labels = hostname.split("."); if (labels.length <= 2) return ""; // localhost ou domaine simple return labels.slice(0, -2).join("."); } // Exemples (suffix = "wakastart-dev.app") : // "app.test.wakastart-dev.app" → "app.test" // "acme.wakastart-dev.app" → "acme" // "localhost" → "" (mode dev local)
Règle de validation DB :
network.subdomainaccepte^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$— alphanum + tirets + points, commence et finit par alphanum.
Subdomain vs email — quand utiliser quoi
| Critère | host (hostname) | email |
|---|---|---|
| Wakapp mono-tenant | Recommandé | Non nécessaire |
| Portail multi-tenant | Non adapté | Recommandé |
| L'utilisateur connaît son org | Recommandé | Optionnel |
| L'utilisateur ne connaît que son email | Non applicable | Recommandé |
| Sécurité (résistance à l'énumération) | Meilleure | Bonne (filtrage Origin) |
Anti-énumération et throttling
| Mesure | Détail |
|---|---|
| Réponse uniforme | { users: [] } ou { result: null } que l'entité existe ou non |
| Latence normalisée | ~80ms minimum sur /discover/start-login (padding si DB plus rapide) |
| Rate limiting court | 5 req/s par IP |
| Rate limiting moyen | 20 req/10s par IP |
| Rate limiting long | 100 req/60s par IP |
Règle UX impérative : affichez toujours un message générique comme "Vérifiez votre email et réessayez", jamais "Cet email n'existe pas".
Gestion d'erreurs
| Status | Body | Cause | Action |
|---|---|---|---|
400 | invalid_request | email + host fournis simultanément, ou aucun | Vérifier le body |
400 | invalid_request | Body absent ou malformé | Vérifier Content-Type et payload |
404 | not_found | Aucun Network ne correspond | Afficher message générique |
429 | Too Many Requests | Rate limit dépassé | Backoff exponentiel, header Retry-After |
Bonnes pratiques
- Appelez
start-loginune seule fois par tentative de login — ne retentez pas automatiquement sur404. - Extrayez le subdomain depuis
window.location.hostnameavec la fonctionextractSubdomain, ne le hardcodez jamais. - Stockez
client_idetrealm(extraits de lakeycloakUrlretournée) en sessionStorage avant le redirect vers Keycloak. - Ne loggez jamais les adresses email passées à Discovery — elles sont des données personnelles.
- Testez localement avec
subdomain=""et implémentez un champ de saisie manuelle pour le mode dev.
Pièges classiques
- Mauvaise extraction du subdomain : envoyer
host="app"au lieu dehost="app.test"→404 not_found. Voir la fonctionextractSubdomainci-dessus. appIdhardcodé : ne pas envoyerappIdsauf si vous gérez plusieurs apps sur le même hostname. Discovery résout depuis le host automatiquement.email+hostsimultanés :400 invalid_request. Choisir l'un ou l'autre.- Réponse vide traitée comme erreur :
{ users: [] }est une réponse valide signifiant "non trouvé". Ne pas déclencher un crash applicatif.
Aller plus loin
- Authentification — OAuth2 PKCE : utiliser la
keycloakUrlretournée parstart-login - Pièges classiques : diagnostic des erreurs fréquentes de Discovery