Socle du hub d'identité : ce que le hub contournait, porté par @eqs (D-067 à D-081) + deux correctifs de sécurité #157
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "marc/hub-socle"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Comme convenu : tout ce que le hub d'identité (EQS/hub, serveur d'autorisation OAuth 2.1 / OIDC sur better-auth 1.7.7, en ligne sur https://hub.dev.equilibre-structures.fr) a dû contourner, porté par le socle. Chaque changement a son changeset (minor → 0.24.0), sa décision et ses tests ; chaque option est à son défaut d'aujourd'hui (testé). Rien n'est versionné ni publié.
Sécurité (à lire en premier)
retourSur(@eqs/platform-next) :/<TAB>/ailleurspassait le filtre et le navigateur partait surhttps://ailleurs— la page/connexiondu gabarit l'utilise. Caractères de contrôle, blancs, invisibles, antislashs,//et formes encodées (à toute profondeur) refusés ; ce qui reste est revérifié en même origine. 38 charges testées. (2ab6a26)porteAuthjugeait le chemin brut :GET /api/auth/x/../oauth2/get-clientsatteignait better-auth (401 au lieu de 404). Il juge maintenant le chemin normalisé ; test HTTP brut qui échoue sans le correctif. (chasse 51)ROUTES_AUTH_FERMEES_SANS_REGISTERd'avant 0.23.0 aurait rouvertget-access-token,delete-useret la liste des clients fermés par la chasse 46. Toutes les listes sont maintenant construites parroutesAuthFermeessur la liste blanche de #145.Ce que la branche apporte
@eqs/authroutesAuthFermees,…_SANS_REGISTER,…_SERVEUR_OIDC(inscription et administration jamais rouvertes)jwks,oauth-provider;two-factor+eqs auth prisma(TOTP de bout en bout)/.well-known/*à la racine (opt-in), CORS public par chemin,champsUtilisateurBETTER_AUTH_SECRETS/sign-in/email, sans pouvoir la couper),IP_ADDRESS_HEADERSREDIS_URL: sessions et compteurs partagés entre répliques (prouvé à deux instances)@Authentifie()/withApi({ authentifie }): « toute personne connectée », reconnu par la conformitérequestIdDe(ctx))urlDeConnexion,PageConnexion)HUB_ISSUER/HUB_AUDIENCE/HUB_JWKS_URL, permissions = rôles locaux ∩ scopes du jeton,Principal.agent, trois contrôles de conformité,hubDeTest()Déjà réglé par
mainpendant l'attente, donc non refait :oauthProvider()sans transtypage (#145), proxys de confiance (#137), dispenses dans les harnais (#149).Vérifications
pnpm checkcomplet vert sur la tête de branche (auth 351, testing 270, cli 139, platform-next 134, platform-nest 73, http 55, config 48, telemetry 57, errors 38 tests),check:docs0 problème, gitleaks surorigin/main..HEADsans fuite.origin/devfusionné (conflitETAT.mdseulement).Questions
BETTER_AUTH_SECRETS,IP_ADDRESS_HEADERS,REDIS_URL,HUB_ISSUER,HUB_AUDIENCE,HUB_JWKS_URL) : chaque satellite les déclare dans son.env.exampleen montant. Ça te va ?ioredisen dépendance de@eqs/auth(chargé seulement siREDIS_URL) : OK ?oidEntra seulement — choix délibéré, jamais par e-mail (risque de prise de compte). D'accord ?next-fullstackne sait pas siHUB_ISSUERest posée : les contrôlesauth.jeton-hub-*y restent « sans objet » tant que le satellite ne passe pas les jetons.EQS_TEST_REDIS_URLest posée ; la CI n'a pas de Redis.🤖 Generated with Claude Code
Plan du hub A-6. Le hub avait mesuré (L-039) que better-auth lit `secondaryStorage` et le stockage de la limitation AVANT l'`init` des greffons : posé par un greffon, seul l'adaptateur interne passait sur Redis. La parade est au niveau de `creerAuth`. - `creerAuth({ secondaryStorage })` : passé avec le reste, entraîne `session.storeSessionInDatabase` et la limitation sur le stockage ; - `@eqs/auth/redis` : `clientRedis`, `stockageRedis` (`getAndDelete` et `increment` atomiques par script Lua, clés préfixées) ; `ioredis` chargé seulement par qui pose `REDIS_URL` ; - `createEqsApp` : client sur `REDIS_URL` (schéma de base), donné à better-auth et au limiteur de Nest (`LimiteurRedis`), sonde `redis` non vitale, fermé à l'arrêt ; `redis` injecte un client. Sans `REDIS_URL`, rien ne change. Prouvé sur un vrai Redis jetable (`EQS_TEST_REDIS_URL`, sauté sinon) : deux répliques à bases distinctes partagent le seau du limiteur et la session ; et, sans Redis, la mécanique better-auth sur un stockage de même forme. En montant : `REDIS_URL=` dans `.env.example`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Le hub a renoncé au TOTP sur son compte de secours (L-035) : `twoFactor` décide sur `user.twoFactorEnabled`, et Prisma n'étend pas `User` depuis un autre fichier — or `auth.prisma` est généré, refusé à l'édition. Même trou pour les `champsUtilisateur` de D-072. - un fragment déclare ses colonnes de `User` par `// @eqs:user <champ>` ; `schemaAuth({ fragments, colonnesUtilisateur })` (`@eqs/auth/prisma`) compose et les injecte, en signant chaque ligne de sa source ; - `schemas/two-factor.prisma` : `TwoFactor` et `User.twoFactorEnabled` ; - `eqs auth prisma [--out] [--check]` lit `package.json#eqs.schemasAuth` (défaut `["auth"]`, la copie de toujours) et `eqs.colonnesUtilisateur` : le script que D-069 laissait au gabarit devient une commande du socle. Prouvé : fragment composé confronté au schéma de better-auth avec `twoFactor` ; trajet TOTP complet sur `creerAuth` (activation, `twoFactorRedirect` sans session, code faux refusé, session au bon code) ; hors dépôt, PostgreSQL 16 jetable + Prisma 6.19 : `validate`, `db push` du schéma composé, même trajet par `prismaAdapter`, colonne et table écrites. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Seul `moi.lire` échappait à la permission, en dur. Le hub, pour ses routes « pour soi » (sessions, audit, consentement), n'avait que `@Public()` plus un contrôle à la main, ou `@Permission()` vers un droit donné à tous — écart de `auth.route-sans-decision` (L-028, L-033). - `@Authentifie()` (Nest) et `withApi({ authentifie: true })` / `garder` (Next) : identité exigée (401 sans), aucune permission ; `@SessionSeule()` se combine ; - décision énumérée `authentifiee`, acceptée par `auth.route-sans-decision` ; `@Permission()` croisé est un écart qui dit de la retirer ; - contrat Next : `x-eqs-authentifie` ; les deux harnais écartent ces routes de `auth.403-permission`, comme `moi.lire` ; - `/moi` de la plateforme porte `@Authentifie()` ; `LIBRES` reste. Déjà sur `main` : les dispenses s'appliquent dans les harnais (#149) ; le message de l'écart la nomme désormais. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Le hub réécrivait le corps de `sign-in/social` par un `fetchImpl` pour y glisser `oauth_query`, recopiait l'extraction des paramètres signés, et sa page `/connexion?echec=…` n'était alimentée par rien : `errorCallbackURL` n'était jamais posé. - `urlDeConnexion` et `SignIn` : `oauthQuery` (→ `oauth_query`, sur `sign-in/social` et `sign-in/email` ; par mot de passe, la connexion suit l'adresse que better-auth rend) et `retourEchec` (→ `errorCallbackURL`, filtré par `retourSur`) ; - `repriseOAuth(recherche)` : les seuls paramètres signés ; - `PageConnexion({ reprendreOAuth: true })`. Absents, le corps de toujours (testé). Un `retourEchec` hors du site n'est pas transmis, tabulation comprise. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>Plan du hub A-7. Jeton du hub : `at+jwt` EdDSA, `iss` = le hub, `aud` = le service, `scope` = permissions natives, `oid`, `act: { sub, typ: 'agent' }`. - `DepsPrincipal.emetteurs` : `resoudrePrincipal` lit l'`iss` sans vérifier, pour choisir qui vérifiera, et confie le jeton à cet émetteur : signature contre SES clés, `typ`, EdDSA, expiration (401) ; audience (403) ; la personne retrouvée ICI (`utilisateurParOid`, compte Microsoft ; 403 si inconnue ou suspendue) ; permissions = rôles d'ici ∩ portée ; `Principal.agent = { clientId, scopes }`. Un `iss` inconnu : Entra, sinon 401 ; - `HUB_ISSUER`, `HUB_AUDIENCE` (défaut `PUBLIC_URL`), `HUB_JWKS_URL` (défaut : découverte RFC 8414, un `issuer` différent annoncé est refusé), deux règles bloquantes ; `auth.hub` (jwks, utilisateur, now) dans `createEqsApp` et `creerApi` ; - conformité : `auth.jeton-hub-emetteur` (401), `-audience` (403), `-portee` (403), sans objet sans `HUB_ISSUER` ; `hubDeTest()` dans `@eqs/testing`. Sans `HUB_ISSUER`, rien ne change (testé). Prouvé : 18 cas sur `resoudrePrincipal` dont la découverte sur un serveur local ; de bout en bout sur `createEqsApp` et `creerApi` ; les trois contrôles verts contre un service qui accepte le hub, l'audience rouge contre un qui ne l'accepte pas. En montant : `HUB_ISSUER=`, `HUB_AUDIENCE=`, `HUB_JWKS_URL=` dans `.env.example`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>