WakaStart
Premiers pas

Discovery — Identifier l'utilisateur

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

Version v1.06 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 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.

http
POST {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_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-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 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 n'est jamais exposé (VULN-002). Le realm est résolu server-side par Discovery.


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-dev.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-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.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

MesureDé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 court5 req/s par IP
Rate limiting moyen20 req/10s par IP
Rate limiting long100 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

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