Authentification — Auth0
Auth0 est le fournisseur d'identité (OAuth 2.0 + OpenID Connect). Point
crucial : il n'y a pas de table users. L'API vérifie le JWT à chaque
requête et indexe tout sur le sub Auth0.
La frontière (port auth)
Comme tout SDK tiers, Auth0 vit derrière un port dans
packages/lib :
packages/lib/src/auth/port.ts— interfaceIdentityDirectory+ types domaine.packages/lib/src/auth/adapter.auth0.ts— implémentation Auth0 : JWKS + Management API (refresh de profil, liste paginée, suspension).
Le middleware API (apps/api/src/middleware/) consomme le port, extrait le sub
et le pose dans c.var.user. Le profil complet (nom, email, avatar) est
récupéré à la demande via la Management API et mis en cache Redis.
Interface IdentityDirectory
interface IdentityDirectory {
verifyJwt(args: { token: string }): Promise<VerifiedJwt>
getUser(args: { sub: string }): Promise<User | null>
listUsers(args: { page?: number; perPage?: number; query?: string }): Promise<ListUsersResult>
setUserBlocked(args: { sub: string; blocked: boolean }): Promise<void>
}
User expose les champs optionnels blocked et lastLogin (renseignés par
Auth0 ; absents sur les entrées qui ne les ont pas). ListUsersResult vaut
{ users: User[]; total: number }.
Middleware admin
adminMiddleware protège les routes /admin/*. Sa signature est :
adminMiddleware(args: { env: Env; auth: IdentityDirectory })
Il doit être monté après authMiddleware (qui pose c.var.user). La
résolution de l'email suit deux étapes :
- Depuis le token JWT —
c.var.user.emailsi présent (les access tokens Auth0 avec audience = l'API n'incluent pas le claimemailpar défaut, donc ce champ est souvent absent). - Depuis la Management API — si l'email est absent du token, le middleware
appelle
args.auth.getUser({ sub })(résultat mis en cache parIdentityDirectory) pour le résoudre.
Retourne 403 si aucun email ne peut être résolu ou s'il n'est pas dans
l'allowlist ADMIN_EMAILS.
authMiddleware → adminMiddleware → handler
parseAdminEmails(raw) (dans apps/api/src/env.ts) normalise la chaîne en un
Set<string> bas-de-casse sans espaces.
Côté web — la frontière client
apps/web/middleware.ts combine Auth0 (sur /auth/*, /app/*) et
next-intl (routes publiques). Règle stricte : les Server Components / Server
Actions appellent getEnv() puis passent des valeurs concrètes en props aux
Client Components — on n'importe jamais env.ts depuis un fichier
"use client" (le garde server-only casserait au build). Voir
Conventions → Env.
Tokens d'email (claim / tracking)
Les liens envoyés par email (claim d'une fiche, suivi d'un tracking) n'utilisent
pas Auth0 : ils portent un JWT signé séparément (HS256, CLAIM_SECRET).
La signature est vérifiée sans création de compte. C'est ce qui permet de
notifier un prestataire scrapé avant même qu'il ait un compte. Voir
Cold start & claim.