WakaStart
Premiers pas

Discovery — Identifier l'utilisateur

Utiliser le service Discovery pour résoudre l'organisation d'un utilisateur avant l'authentification.

Version v1.07 min de lecture

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 que votre code n'ait besoin de connaître les identifiants internes de Keycloak — à noter toutefois que network_realm_id (déprécié) reste exposé dans certaines réponses pour rétro-compatibilité, voir plus bas.

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 : la latence est normalisée (~80ms) sur POST /discover/start-login et sur les routes /internal/* (~50ms). Les lookups directs GET /discover/email et GET /discover/subdomain n'ont aucun padding de latence — voir la section dédiée plus bas.
  • Realm : concept Keycloak — 1 Network = 1 realm. Le champ network_realm_id a été réintroduit dans les réponses de /discover/subdomain et /discover/email (décision produit assumée pour la rétro-compatibilité, VULN-002) ; il est marqué @deprecated mais bien présent — ce n'est pas un champ masqué.

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.

http
POST {DISCOVERY_API_URL}/discover/start-login Content-Type: application/json { "host": "app.test", "redirectUri": "https://app.test.wakastart.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_id cô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.

http
POST {DISCOVERY_API_URL}/discover/start-login Content-Type: application/json { "email": "john@acme.com", "redirectUri": "https://app.test.wakastart.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 email ET host simultanément — la réponse sera 400 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.

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 :

ChampUsage
network_subdomain / customer_subdomainIdentifie le tenant logique
app_client_idKeycloak clientId (utile si vous voulez forcer un appId dans start-login)
keycloak_org_aliasAlias de l'organisation Keycloak (customer.wid), utilisable comme kc_idp_hint
idp_typeOIDC ou SAML (si IDP fédéré configuré)

network_realm_id est réintroduit et exposé par /discover/subdomain et /discover/email (VULN-002 : décision produit acceptée pour la rétro-compatibilité). Le champ est marqué @deprecated mais bien présent dans la réponse — ce n'est pas un champ masqué.


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 (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

http
GET /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.app └──┬───┘ └──────┬─────┘ subdomain platform domain

Fonction d'extraction recommandée (TypeScript) :

typescript
function 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.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.app") : // "app.test.wakastart.app" → "app.test" // "acme.wakastart.app" → "acme" // "localhost" → "" (mode dev local)

Règle de validation DB : network.subdomain accepte ^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$ — alphanum + tirets + points, commence et finit par alphanum.


Subdomain vs email — quand utiliser quoi

Critèrehost (hostname)email
Wakapp mono-tenantRecommandéNon nécessaire
Portail multi-tenantNon adaptéRecommandé
L'utilisateur connaît son orgRecommandéOptionnel
L'utilisateur ne connaît que son emailNon applicableRecommandé
Sécurité (résistance à l'énumération)MeilleureBonne (filtrage Origin)

Anti-énumération et throttling

[!WARNING] L'anti-énumération n'est pas uniforme sur tout le service. POST /discover/start-login et les routes /internal/* normalisent la latence des réponses négatives. GET /discover/email et GET /discover/subdomain retournent en revanche le payload tenant complet dès qu'il y a un match, sans aucun padding de latence — l'existence d'un compte ou d'un tenant reste observable via le contenu de la réponse sur ces deux routes. État transitoire assumé pendant la migration des auth wakapps ; ne vous appuyez pas sur ces deux endpoints pour un flow qui exige une résistance forte à l'énumération.

MesureDétail
Réponse uniforme (forme JSON){ users: [] } ou { result: null } — la clé est toujours présente, que l'entité existe ou non
Latence normalisée~80ms sur /discover/start-login, ~50ms sur /internal/*. Absente sur GET /discover/email et GET /discover/subdomain
Rate limiting court5 requêtes / 60 s par IP
Rate limiting moyen20 requêtes / 600 s (10 min) par IP
Rate limiting long100 requêtes / 3600 s (1 h) 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

StatusBodyCauseAction
400invalid_requestemail + host fournis simultanément, ou aucunVérifier le body
400invalid_requestBody absent ou malforméVérifier Content-Type et payload
404not_foundAucun Network ne correspondAfficher message générique
429Too Many RequestsRate limit dépasséBackoff exponentiel, header Retry-After

Bonnes pratiques

  • Appelez start-login une seule fois par tentative de login — ne retentez pas automatiquement sur 404.
  • Extrayez le subdomain depuis window.location.hostname avec la fonction extractSubdomain, ne le hardcodez jamais.
  • Stockez client_id et realm (extraits de la keycloakUrl retourné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 de host="app.test" → 404 not_found. Voir la fonction extractSubdomain ci-dessus.
  • appId hardcodé : ne pas envoyer appId sauf si vous gérez plusieurs apps sur le même hostname. Discovery résout depuis le host automatiquement.
  • email + host simultané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