@nodefony/security 10.0.0-beta.2 → 10.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +44 -10
- package/dist/nodefony/command/security-oauth-doctor.js +66 -0
- package/dist/nodefony/command/security-secrets.js +42 -25
- package/dist/nodefony/command/security-token.js +46 -15
- package/dist/nodefony/command/security-user-delete.js +5 -4
- package/dist/nodefony/command/security-user-password.js +4 -11
- package/dist/nodefony/config/config.js +80 -5
- package/dist/nodefony/contracts/index.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +24 -0
- package/dist/nodefony/errors/index.js +2 -1
- package/dist/nodefony/keycloak/keycloakAdmin.js +255 -0
- package/dist/nodefony/keycloak/keycloakRealm.js +260 -0
- package/dist/nodefony/keycloak/keycloakRealmInput.js +124 -0
- package/dist/nodefony/keycloak/scaffold.js +231 -0
- package/dist/nodefony/keycloak/security-keycloak-realm.js +128 -0
- package/dist/nodefony/scaffold/contribute.js +20 -0
- package/dist/nodefony/service/authFlow.js +111 -11
- package/dist/nodefony/service/firewall.js +111 -21
- package/dist/nodefony/service/keycloak.js +209 -0
- package/dist/nodefony/service/oauth2.js +379 -26
- package/dist/nodefony/service/tokenService.js +57 -6
- package/dist/nodefony/service/totp.js +51 -6
- package/dist/nodefony/service/webAuthn.js +20 -2
- package/dist/nodefony/src/SecuredArea.js +14 -3
- package/dist/nodefony/src/admin/SecurityAdminApi.js +70 -40
- package/dist/nodefony/src/admin/adminAudit.js +0 -1
- package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +66 -6
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +38 -15
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +1 -0
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +13 -3
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +9 -2
- package/dist/nodefony/src/authenticator/bearer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +27 -0
- package/dist/nodefony/src/entryPoint.js +39 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +40 -0
- package/dist/nodefony/src/oauth/metadata.js +36 -9
- package/dist/nodefony/src/oauth/oauth2Client.js +60 -33
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +39 -6
- package/dist/nodefony/src/oauth/providerDiagnosis.js +283 -0
- package/dist/nodefony/src/oauth/providerRoles.js +79 -0
- package/dist/nodefony/src/oauth/providers/github.js +32 -6
- package/dist/nodefony/src/oauth/providers/oidc.js +82 -3
- package/dist/nodefony/src/openToSignup.js +47 -0
- package/dist/nodefony/src/sessionAuthentication.js +26 -0
- package/dist/nodefony/src/token/JwtKeystore.js +78 -46
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +40 -13
- package/dist/nodefony/src/token/secretFile.js +59 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +51 -0
- package/dist/types/index.d.ts +26 -6
- package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
- package/dist/types/nodefony/command/security-token.d.ts +9 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
- package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
- package/dist/types/nodefony/config/config.d.ts +63 -1
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
- package/dist/types/nodefony/errors/index.d.ts +1 -0
- package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
- package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
- package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
- package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
- package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
- package/dist/types/nodefony/service/authFlow.d.ts +35 -3
- package/dist/types/nodefony/service/firewall.d.ts +17 -3
- package/dist/types/nodefony/service/keycloak.d.ts +115 -0
- package/dist/types/nodefony/service/oauth2.d.ts +125 -9
- package/dist/types/nodefony/service/tokenService.d.ts +7 -1
- package/dist/types/nodefony/service/totp.d.ts +28 -4
- package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
- package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +54 -12
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +18 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +13 -0
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
- package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
- package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
- package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
- package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
- package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
- package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
- package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
- package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
- package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
- package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
- package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
- package/docs/api-keys.md +17 -17
- package/docs/audit.md +16 -16
- package/docs/authenticators.md +60 -27
- package/docs/authorization.md +16 -16
- package/docs/cors.md +29 -29
- package/docs/csrf.md +35 -33
- package/docs/external-jwt.md +66 -4
- package/docs/firewall.md +57 -24
- package/docs/headers.md +30 -30
- package/docs/index.md +12 -4
- package/docs/keycloak.md +818 -0
- package/docs/login-page.md +361 -0
- package/docs/oauth2.md +236 -122
- package/docs/obtenir-un-jeton.md +18 -11
- package/docs/tokens.md +136 -85
- package/docs/totp.md +105 -64
- package/docs/webauthn.md +90 -87
- package/docs/webhooks.md +3 -3
- package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/account/theme.properties +14 -0
- package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/admin/theme.properties +12 -0
- package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
- package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
- package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
- package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/email/theme.properties +8 -0
- package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
- package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
- package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
- package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
- package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/login/template.ftl +423 -0
- package/keycloak/themes/nodefony/login/theme.properties +17 -0
- package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
- package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
- package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
- package/package.json +17 -15
package/docs/obtenir-un-jeton.md
CHANGED
|
@@ -55,7 +55,7 @@ aucune écoute réseau (`security-token.ts:36`). C'est ce qui la rend utilisable
|
|
|
55
55
|
ne tourne pas, en intégration continue, ou dans un conteneur d'amorçage.
|
|
56
56
|
|
|
57
57
|
**Le jeton vise une porte, et une seule.** L'audience est celle de la porte visée — la porte MCP par
|
|
58
|
-
défaut, `/nodefony/mcp` (`src/nodefony/src/mcp/protocol.ts:
|
|
58
|
+
défaut, `/nodefony/mcp` (`src/nodefony/src/mcp/protocol.ts:97`). Un jeton d'une autre audience est
|
|
59
59
|
refusé, et c'est toute la raison d'être de la liaison d'audience
|
|
60
60
|
([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html)).
|
|
61
61
|
|
|
@@ -125,19 +125,26 @@ Puis `npm run build` — le runtime lit le `dist`, pas la source.
|
|
|
125
125
|
|
|
126
126
|
### Les options, et ce que chacune décide
|
|
127
127
|
|
|
128
|
-
| Option | Défaut
|
|
129
|
-
| ---------------------- |
|
|
130
|
-
| `[identifier]` | `admin`
|
|
131
|
-
| `-s, --scope <liste>` | `admin:read`
|
|
132
|
-
| `-r, --resource <uri>` | la porte MCP de l'application
|
|
133
|
-
| `-a, --agent <noms>` | ceux détectés
|
|
134
|
-
| `-t, --ttl <minutes>` |
|
|
135
|
-
| `-w, --write` | proposé en terminal
|
|
136
|
-
| `-j, --json` | —
|
|
128
|
+
| Option | Défaut | Ce qu'elle change |
|
|
129
|
+
| ---------------------- | -------------------------------------------- | -------------------------------------------------------- |
|
|
130
|
+
| `[identifier]` | `admin` | le compte porteur du jeton |
|
|
131
|
+
| `-s, --scope <liste>` | `admin:read` | les pouvoirs demandés — ajouter `admin:write` pour muter |
|
|
132
|
+
| `-r, --resource <uri>` | la porte MCP de l'application | viser une **autre** audience |
|
|
133
|
+
| `-a, --agent <noms>` | ceux détectés | quels agents servir — `none` pour aucun |
|
|
134
|
+
| `-t, --ttl <minutes>` | 8 h avec `--write`, sinon la config (15 min) | la durée de validité, **bornée à 30 jours** |
|
|
135
|
+
| `-w, --write` | proposé en terminal | poser `NF_MCP_TOKEN` chez les agents présents |
|
|
136
|
+
| `-j, --json` | — | sortie machine, sans invite |
|
|
137
137
|
|
|
138
138
|
Le plafond de `--ttl` n'est pas décoratif : un jeton posé dans un fichier **est une clé**, et une
|
|
139
139
|
clé se remplace (`security-token.ts:67-84`).
|
|
140
140
|
|
|
141
|
+
**Posé chez un agent, le jeton vit 8 h par défaut** (`security-token.ts:106-110`). L'en-tête d'un
|
|
142
|
+
agent est figé : rien ne le rafraîchit, et le défaut de la configuration (15 min, taillé pour un
|
|
143
|
+
client d'API qui renouvelle son jeton) produisait un 401 au bout d'un quart d'heure. Huit heures
|
|
144
|
+
couvrent une journée de travail et bornent une fuite à quelques heures ; `--ttl` explicite
|
|
145
|
+
l'emporte toujours, et le défaut global `jwt.accessTtlS` ne bouge pas. La commande affiche l'heure
|
|
146
|
+
d'**expiration** (`expires_at` en `--json`) : c'est elle qui explique le 401 du lendemain.
|
|
147
|
+
|
|
141
148
|
### Où `--write` pose la valeur
|
|
142
149
|
|
|
143
150
|
La variable est `NF_MCP_TOKEN` (`src/nodefony/src/cli/aiMcpReport.ts:32`). La commande ne sert que
|
|
@@ -191,7 +198,7 @@ est décrit dans [tokens](tokens.md), pas ici.
|
|
|
191
198
|
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
192
199
|
| `impossible d'émettre un jeton pour cette porte ici` | **en développement** : l'audience n'est pas déclarée à l'émetteur — c'est une liste blanche, pas un défaut ouvert | `use("@nodefony/security", { jwt: { audiences: ["<la porte>"] } })`, puis `npm run build` |
|
|
193
200
|
| Le même message, mais l'environnement affiché n'est pas `development` | la porte MCP est servie par un module `policy: "dev"` : **elle n'existe pas** dans cet environnement | `NODE_ENV=development npx nodefony security:token`, ou viser une autre porte avec `--resource` |
|
|
194
|
-
| Le jeton est émis, mais avec **moins** de scopes que demandé | l'émetteur retire ceux que ce porteur ne peut pas obtenir, et il le dit ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) §3.3) | lire le champ `scopes` de la sortie — c'est celui qui fait foi (`security-token.ts:
|
|
201
|
+
| Le jeton est émis, mais avec **moins** de scopes que demandé | l'émetteur retire ceux que ce porteur ne peut pas obtenir, et il le dit ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) §3.3) | lire le champ `scopes` de la sortie — c'est celui qui fait foi (`security-token.ts:460`) |
|
|
195
202
|
| L'agent reçoit `401` alors que le jeton vient d'être posé | la valeur a été écrite dans un dossier que **cet** agent ne lit pas | `--agent <nom>` pour viser explicitement, ou l'`export` dans le shell qui lance l'agent |
|
|
196
203
|
| `--write` ne fait rien et n'écrit aucun fichier | aucun agent n'est **constaté** dans ce projet | la commande liste alors les emplacements connus et donne la ligne d'`export` |
|
|
197
204
|
|
package/docs/tokens.md
CHANGED
|
@@ -25,7 +25,7 @@ tags:
|
|
|
25
25
|
]
|
|
26
26
|
version: "doc"
|
|
27
27
|
status: stable
|
|
28
|
-
updated: 2026-10-
|
|
28
|
+
updated: 2026-10-05
|
|
29
29
|
source: "src/packages/@nodefony/security/docs/tokens.md"
|
|
30
30
|
---
|
|
31
31
|
|
|
@@ -87,11 +87,11 @@ défaut » en prod.
|
|
|
87
87
|
## La vision Nodefony — un service propriétaire, des endpoints minces
|
|
88
88
|
|
|
89
89
|
`TokenService` est **propriétaire** du store et du keystore : à `TokenService.#build()`
|
|
90
|
-
(`tokenService.ts:
|
|
91
|
-
`tokenStore` au container (`tokenService.ts:
|
|
92
|
-
(`tokenService.ts:
|
|
90
|
+
(`tokenService.ts:158`), si `jwt.enabled` ou `apiKeys.enabled`, il résout le store pluggable, pose
|
|
91
|
+
`tokenStore` au container (`tokenService.ts:219`) puis crée le keystore et pose `jwtKeystore`
|
|
92
|
+
(`tokenService.ts:225-231`) — consommés par le `JwtAuthenticator` et les endpoints. Il arme un
|
|
93
93
|
**gc** via `GcScheduler` (timer `unref` + **jitter** de phase pour étaler les balayages entre pods,
|
|
94
|
-
`tokenService.ts:
|
|
94
|
+
`tokenService.ts:233-239`).
|
|
95
95
|
|
|
96
96
|
Les endpoints HTTP sont des **adaptateurs minces** portés par `@nodefony/framework`, couplés **par
|
|
97
97
|
nom de service** via le contrat structurel `ITokenIssuer` — framework n'importe jamais security
|
|
@@ -144,7 +144,7 @@ le framework monte deux routes (`mountTokenAuthRoutes()`, `TokenAuthController.t
|
|
|
144
144
|
> [!IMPORTANT]
|
|
145
145
|
> Ces routes n'existent **que si** `@nodefony/security` est chargé — sinon 404, zéro surface
|
|
146
146
|
> (`framework/index.ts:415`). Module chargé mais JWT désactivé : elles existent et répondent
|
|
147
|
-
> `503 Token issuance unavailable` (`TokenAuthController.ts:69`). Elles sont `bypassFirewall: true` (`TokenAuthController.ts:
|
|
147
|
+
> `503 Token issuance unavailable` (`TokenAuthController.ts:69`). Elles sont `bypassFirewall: true` (`TokenAuthController.ts:188`) :
|
|
148
148
|
> elles SONT le mécanisme d'émission — protégées, obtenir un token exigerait d'être déjà
|
|
149
149
|
> authentifié (deadlock). Le JWT part en **réponse JSON** (Bearer), jamais en cookie ni en URL.
|
|
150
150
|
|
|
@@ -154,10 +154,12 @@ le framework monte deux routes (`mountTokenAuthRoutes()`, `TokenAuthController.t
|
|
|
154
154
|
// nodefony.config.ts (extrait) — la zone API machine + la source de clé
|
|
155
155
|
use("@nodefony/security", {
|
|
156
156
|
jwt: {
|
|
157
|
-
// dev
|
|
158
|
-
// prod
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
// dev : persiste la clé Ed25519 sous var/ (sinon clé ÉPHÉMÈRE + warning).
|
|
158
|
+
// prod : NF_JWT_KEYSET, la même clé sur tous les pods — voir la section keystore.
|
|
159
|
+
keystore: {
|
|
160
|
+
keySetJson: process.env.NF_JWT_KEYSET,
|
|
161
|
+
dir: "var/keys",
|
|
162
|
+
},
|
|
161
163
|
},
|
|
162
164
|
areas: {
|
|
163
165
|
// Le firewall vérifie le Bearer JWT sur CHAQUE requête de la zone.
|
|
@@ -229,36 +231,36 @@ Erreurs mappées par duck-typing dans `#renderAuthError()` (`TokenAuthController
|
|
|
229
231
|
|
|
230
232
|
### Émission (grant M2M/CLI)
|
|
231
233
|
|
|
232
|
-
`issueForCredentials()` (`tokenService.ts:
|
|
234
|
+
`issueForCredentials()` (`tokenService.ts:387`) vérifie l'identifiant/mot de passe via le
|
|
233
235
|
service `users`, avec le **throttling NIST partagé** — `ThrottledError` avant tout hachage
|
|
234
|
-
(`tokenService.ts:
|
|
235
|
-
par `#auditGrant()` (`tokenService.ts:
|
|
236
|
+
(`tokenService.ts:398`). Chaque tentative échouée est auditée `login.failure`/`login.throttled`
|
|
237
|
+
par `#auditGrant()` (`tokenService.ts:414-426`). Puis `issueTokens()` (`tokenService.ts:582`)
|
|
236
238
|
produit :
|
|
237
239
|
|
|
238
240
|
- un **access token** : JWT signé EdDSA, en-tête `typ:"at+jwt"` + `kid`, claims
|
|
239
|
-
`iss`/`sub`/`aud`/`exp` (15 min) + `jti` — `#signAccess()` (`tokenService.ts:
|
|
241
|
+
`iss`/`sub`/`aud`/`exp` (15 min) + `jti` — `#signAccess()` (`tokenService.ts:694-707`) ;
|
|
240
242
|
- un **refresh token** : secret opaque haute entropie `nfr_<32 octets base64url>`, **stocké haché**
|
|
241
243
|
`sha256` (le clair n'existe qu'en réponse, jamais au repos) — `#buildRefresh()`
|
|
242
|
-
(`tokenService.ts:
|
|
244
|
+
(`tokenService.ts:726-761`).
|
|
243
245
|
|
|
244
|
-
La réponse suit RFC 6749 §5.1 — `ITokenResponse` (`tokenService.ts:
|
|
245
|
-
`token.issued` via `recordAudit` avec le `tokenId` corrélable (`tokenService.ts:
|
|
246
|
+
La réponse suit RFC 6749 §5.1 — `ITokenResponse` (`tokenService.ts:50-58`). Tout succès est audité
|
|
247
|
+
`token.issued` via `recordAudit` avec le `tokenId` corrélable (`tokenService.ts:357-363`).
|
|
246
248
|
|
|
247
249
|
### Rotation & détection de rejeu (RFC 9700 §4.14)
|
|
248
250
|
|
|
249
|
-
`refresh()` (`tokenService.ts:
|
|
251
|
+
`refresh()` (`tokenService.ts:647`) est le cœur défensif, dans l'ordre :
|
|
250
252
|
|
|
251
|
-
1. Lookup par hash — `findByHash`, refus uniforme si inconnu/mauvais type (`tokenService.ts:
|
|
253
|
+
1. Lookup par hash — `findByHash`, refus uniforme si inconnu/mauvais type (`tokenService.ts:653`).
|
|
252
254
|
2. **Détection de rejeu** : refresh **déjà révoqué** re-présenté → `revokeFamily` coupe toute la
|
|
253
|
-
famille + audit `token.reuse_detected`, signal d'attaque fort (`tokenService.ts:
|
|
254
|
-
3. Expiration `expiresAt` vérifiée (`tokenService.ts:
|
|
255
|
+
famille + audit `token.reuse_detected`, signal d'attaque fort (`tokenService.ts:622-636`).
|
|
256
|
+
3. Expiration `expiresAt` vérifiée (`tokenService.ts:793`).
|
|
255
257
|
4. **Sujet revérifié** — compte disparu/inactif/verrouillé rejeté sans attendre l'exp,
|
|
256
|
-
`#resolveUserForRefresh()` (`tokenService.ts:
|
|
258
|
+
`#resolveUserForRefresh()` (`tokenService.ts:852`).
|
|
257
259
|
5. **Downscoping** : les `scopes` du nouveau couple sont ceux de l'ancien, jamais plus
|
|
258
|
-
(`tokenService.ts:
|
|
260
|
+
(`tokenService.ts:684`).
|
|
259
261
|
6. **Rotation** : nouveau refresh (même famille), l'ancien chaîné `replacedBy` + révoqué
|
|
260
|
-
`"rotated"` (`tokenService.ts:
|
|
261
|
-
et le refresh courant reste valide (`tokenService.ts:
|
|
262
|
+
`"rotated"` (`tokenService.ts:716-718`). Si `rotateRefresh` est désactivé, l'access est réémis
|
|
263
|
+
et le refresh courant reste valide (`tokenService.ts:702`).
|
|
262
264
|
|
|
263
265
|
### Mise en situation — ton refresh token a été volé
|
|
264
266
|
|
|
@@ -278,26 +280,73 @@ la victime est déconnectée (signal visible) au lieu d'un vol silencieux indéf
|
|
|
278
280
|
|
|
279
281
|
## 🔐 Le keystore Ed25519 — la clé ne fuit pas, pas de secret « par défaut » en prod
|
|
280
282
|
|
|
281
|
-
`JwtKeystore.#load()` résout la source de clé par **priorité** (`JwtKeystore.ts:
|
|
283
|
+
`JwtKeystore.#load()` résout la source de clé par **priorité** (`JwtKeystore.ts:219`), pensée
|
|
282
284
|
pour ne jamais auto-générer une clé en clair silencieusement en prod :
|
|
283
285
|
|
|
284
|
-
1. **env** — `keySetJson` (
|
|
285
|
-
|
|
286
|
-
2. **fichier** — `dir/keyset.json`, généré si absent,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
286
|
+
1. **env** — `keySetJson` (`NF_JWT_KEYSET` dans une application générée) : la MÊME clé pour tous
|
|
287
|
+
les process qui servent l'application (`JwtKeystore.ts:222`).
|
|
288
|
+
2. **fichier** — `dir/keyset.json`, généré si absent, en mode 600, par une création **exclusive**
|
|
289
|
+
(`createSecretExclusive`, `secretFile.ts:188`) : quand plusieurs workers de `nodefony cluster`
|
|
290
|
+
démarrent ensemble, un seul crée la clé et les autres relisent la sienne (`JwtKeystore.ts:242`).
|
|
291
|
+
Source de développement, ou d'un serveur unique à disque persistant.
|
|
292
|
+
3. **mémoire** — aucune source → clé **éphémère**, propre au process (`JwtKeystore.ts:263`). En
|
|
293
|
+
développement : un WARNING au premier jeton. En production, un process qui sert **refuse de
|
|
294
|
+
démarrer** (§ suivant).
|
|
295
|
+
|
|
296
|
+
Le JWKS servi par `getPublicJWKS()` est **public** — `JwtKeystore.ts:209`.
|
|
292
297
|
La composante privée `d` en est retirée à l'import, par liste BLANCHE de paramètres
|
|
293
|
-
(`#importKeyset()`, `JwtKeystore.ts:
|
|
298
|
+
(`#importKeyset()`, `JwtKeystore.ts:275`, RFC 8037/7517).
|
|
294
299
|
C'est ce JWKS qu'utilise le vérificateur local (`createLocalJWKSet`, `JwtAuthenticator.ts:174`),
|
|
295
|
-
jamais une clé venue du jeton. Le chargement est mémoïsé — `#ensureLoaded()` (`JwtKeystore.ts:
|
|
300
|
+
jamais une clé venue du jeton. Le chargement est mémoïsé — `#ensureLoaded()` (`JwtKeystore.ts:215`).
|
|
301
|
+
|
|
302
|
+
### Plusieurs pods ou workers — UNE clé pour tous
|
|
303
|
+
|
|
304
|
+
Chaque jeton porte le `kid` de la clé qui l'a signé. Deux process qui ont chacun la leur refusent
|
|
305
|
+
les jetons l'un de l'autre : derrière un répartiteur, l'utilisateur est déconnecté au hasard (401),
|
|
306
|
+
et tout redémarrage invalide les jetons en vol.
|
|
307
|
+
|
|
308
|
+
**En production, un process qui SERT sans source partagée refuse de démarrer**
|
|
309
|
+
(`#requireSharedSigningKey`, `tokenService.ts:123`), avec le motif et les trois issues dans le
|
|
310
|
+
journal. Une commande de console (`orm:migrate`, `security:user:add`) n'est pas concernée : elle
|
|
311
|
+
ne signe aucun jeton. Sous `nodefony cluster`, un worker refusé ne se relance pas — le master
|
|
312
|
+
arrête le cluster et sort en **78** (`ClusterManager.ts:212`), le code d'une faute de configuration.
|
|
313
|
+
|
|
314
|
+
Le déploiement, de bout en bout :
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# 1. Générer la valeur UNE fois par environnement (staging ≠ production)
|
|
318
|
+
npx nodefony security:secrets --jwt-keyset > keyset.json
|
|
319
|
+
|
|
320
|
+
# 2. La ranger dans le gestionnaire de secrets — ici un Secret Kubernetes
|
|
321
|
+
kubectl create secret generic app-jwt --from-file=NF_JWT_KEYSET=keyset.json
|
|
322
|
+
rm keyset.json # elle porte la clé PRIVÉE : aucun exemplaire ne traîne
|
|
323
|
+
|
|
324
|
+
# 3. L'injecter dans CHAQUE pod (extrait du Deployment)
|
|
325
|
+
# env:
|
|
326
|
+
# - name: NF_JWT_KEYSET
|
|
327
|
+
# valueFrom: { secretKeyRef: { name: app-jwt, key: NF_JWT_KEYSET } }
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Le câblage, que le gabarit d'application écrit déjà :
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
jwt: {
|
|
334
|
+
keystore: {
|
|
335
|
+
keySetJson: ctx.env.NF_JWT_KEYSET, // présente : l'emporte partout
|
|
336
|
+
dir: ctx.isProd ? undefined : "var/keys", // développement
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
```
|
|
296
340
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
341
|
+
- **Jamais dans un fichier du poste, jamais dans git** : en développement la clé vit dans `var/keys/` ;
|
|
342
|
+
une clé privée de production sur un poste n'apporte que le risque de fuir.
|
|
343
|
+
- **Dans un fichier `.env`** (Docker Compose `env_file`), l'entourer de quotes simples :
|
|
344
|
+
`NF_JWT_KEYSET='{"active":"…","keys":[…]}'`.
|
|
345
|
+
- **Valeur illisible** : la configuration security est refusée au démarrage, le chemin
|
|
346
|
+
`jwt.keystore.keySetJson` nommé, la valeur **jamais** recopiée dans le message.
|
|
347
|
+
- **Rotation** : la valeur est STABLE — la remplacer refuse les jetons en vol. Ajouter la nouvelle
|
|
348
|
+
clé au tableau `keys`, la désigner dans `active`, garder l'ancienne le temps que ses jetons
|
|
349
|
+
expirent (`accessTtlS`, `refreshTtlS`), puis la retirer.
|
|
301
350
|
|
|
302
351
|
## 🧩 Le store pluggable — durable par défaut, jamais de faux durable silencieux
|
|
303
352
|
|
|
@@ -308,18 +357,18 @@ seuil `invalidBefore` par porteur (`ITokenStore.ts:11-15`). Les backends s'enreg
|
|
|
308
357
|
fabrique — `registerTokenStore()` (`tokenStoreRegistry.ts:41-46`) : les adapters lourds importent
|
|
309
358
|
`import type { ITokenStore }` (effacé à la compilation), zéro couplage runtime.
|
|
310
359
|
|
|
311
|
-
Sa résolution au boot (`tokenService.ts:
|
|
360
|
+
Sa résolution au boot (`tokenService.ts:156-207`) suit la doctrine `store:"auto"` du framework :
|
|
312
361
|
|
|
313
362
|
- `auto` (défaut) → suit l'infra database déclarée via `resolveAutoStore` — **borné aux backends
|
|
314
|
-
réellement enregistrés**, repli memory **annoncé** (`tokenService.ts:
|
|
363
|
+
réellement enregistrés**, repli memory **annoncé** (`tokenService.ts:160-168`) ;
|
|
315
364
|
- store explicite **inconnu** → en prod, **boot avorté** (fail-loud) ; en dev, brique désactivée et
|
|
316
|
-
annoncée avec la liste `listTokenStores()` (`tokenService.ts:
|
|
365
|
+
annoncée avec la liste `listTokenStores()` (`tokenService.ts:171-182`) — jamais de fallback
|
|
317
366
|
memory silencieux pour du durable ;
|
|
318
367
|
- store `memory` **en prod** → `WARNING` nommant l'impact : denylist/refresh/clés API per-pod et
|
|
319
|
-
volatils, révocation non partagée (`tokenService.ts:
|
|
368
|
+
volatils, révocation non partagée (`tokenService.ts:186-193`).
|
|
320
369
|
|
|
321
370
|
La décision (configuré → résolu, raison) est publiée au kernel par `registerStoreResolution()`
|
|
322
|
-
(`tokenService.ts:
|
|
371
|
+
(`tokenService.ts:195-204`) — visible dans Studio.
|
|
323
372
|
|
|
324
373
|
### Mise en situation — quel store pour quelle app ?
|
|
325
374
|
|
|
@@ -348,7 +397,7 @@ La décision (configuré → résolu, raison) est publiée au kernel par `regist
|
|
|
348
397
|
|
|
349
398
|
- Enregistré par le module drizzle (`drizzle/nodefony/registerStores.ts:261`).
|
|
350
399
|
- Élu par `store:"auto"` dès qu'une infra database SQL est déclarée ; sinon sqlite local si drizzle
|
|
351
|
-
est chargé (`config.ts:
|
|
400
|
+
est chargé (`config.ts:452-457`).
|
|
352
401
|
- Pagination **offset + total** (helper `paginate()` d'orm-core) ; e2e sur PostgreSQL et MySQL réels.
|
|
353
402
|
|
|
354
403
|
### `mongoose` — MongoDB
|
|
@@ -360,12 +409,12 @@ La décision (configuré → résolu, raison) est publiée au kernel par `regist
|
|
|
360
409
|
### `redis` — cluster, TTL natif
|
|
361
410
|
|
|
362
411
|
- Enregistré par le module redis (`redis/nodefony/registerStores.ts:75`).
|
|
363
|
-
- TTL natif : `expire()` posé à l'écriture du record (`RedisTokenStore.ts:
|
|
412
|
+
- TTL natif : `expire()` posé à l'écriture du record (`RedisTokenStore.ts:94`) — l'expiration ne
|
|
364
413
|
dépend pas du gc.
|
|
365
414
|
- Listing par `SCAN` : curseur opaque `skip:scanCursor`, `decodeCursor()`
|
|
366
415
|
(`RedisTokenStore.ts:428`) — sans ordre global ni total, capacité réduite **assumée**.
|
|
367
416
|
- `countTokens()` renvoie `-1` : un comptage exact exigerait un SCAN complet O(N), refusé
|
|
368
|
-
(`RedisTokenStore.ts:
|
|
417
|
+
(`RedisTokenStore.ts:529`).
|
|
369
418
|
|
|
370
419
|
### Le record — une seule table pour PAT et refresh
|
|
371
420
|
|
|
@@ -406,7 +455,7 @@ Les colonnes par dialecte vivent dans la doc de chaque adapter (règle anti-trip
|
|
|
406
455
|
|
|
407
456
|
Le `gc()` du store purge la `denylist` expirée, les records à terme, les PAT révoqués au-delà de
|
|
408
457
|
la rétention (`ITokenStore.ts:274-280`). Orchestré par le `GcScheduler` du service ; `runGc()` reste public pour
|
|
409
|
-
un futur worker cron — poser alors `gcIntervalS: 0` (`tokenService.ts:
|
|
458
|
+
un futur worker cron — poser alors `gcIntervalS: 0` (`tokenService.ts:338`).
|
|
410
459
|
|
|
411
460
|
> [!TIP]
|
|
412
461
|
> Un refresh révoqué **par rotation** n'est PAS purgé tout de suite : il est conservé jusqu'à son
|
|
@@ -416,80 +465,82 @@ un futur worker cron — poser alors `gcIntervalS: 0` (`tokenService.ts:293`).
|
|
|
416
465
|
|
|
417
466
|
## ⚙️ Configuration
|
|
418
467
|
|
|
419
|
-
Tables dérivées du schéma Zod — `jwtSchema` (`config.ts:
|
|
420
|
-
(`config.ts:
|
|
468
|
+
Tables dérivées du schéma Zod — `jwtSchema` (`config.ts:373-449`) et `tokenStoreSchema`
|
|
469
|
+
(`config.ts:482-514`), défauts inclus.
|
|
421
470
|
|
|
422
471
|
### `jwt.*`
|
|
423
472
|
|
|
424
473
|
<!-- prettier-ignore -->
|
|
425
474
|
| Option | Type | Défaut | Effet |
|
|
426
475
|
| --- | --- | --- | --- |
|
|
427
|
-
| `enabled` | boolean | `true` | Active signature + refresh (`config.ts:
|
|
476
|
+
| `enabled` | boolean | `true` | Active signature + refresh (`config.ts:375`) |
|
|
428
477
|
| `alg` | `EdDSA` \| `RS256` | `EdDSA` | `RS256` = slot non câblé (`jwtRuntime.ts:21`) |
|
|
429
|
-
| `accessTtlS` | number (s) | `900` | TTL de l'access token — 15 min (`config.ts:
|
|
430
|
-
| `refreshTtlS` | number (s) | `604800` | TTL du refresh — 7 jours (`config.ts:
|
|
431
|
-
| `rotateRefresh` | boolean | `true` | Rotation du refresh à chaque usage, OWASP (`config.ts:
|
|
478
|
+
| `accessTtlS` | number (s) | `900` | TTL de l'access token — 15 min (`config.ts:420`) |
|
|
479
|
+
| `refreshTtlS` | number (s) | `604800` | TTL du refresh — 7 jours (`config.ts:425`) |
|
|
480
|
+
| `rotateRefresh` | boolean | `true` | Rotation du refresh à chaque usage, OWASP (`config.ts:430`) |
|
|
432
481
|
| `jwks` | boolean | `true` | Publie `/.well-known/jwks.json` + les métadonnées RFC 8414 — sans `issuer` en URL https, rien n'est publié |
|
|
433
|
-
| `audiences` | string[] | `[]` | `aud` acceptées (RFC 8707) ; vide = `[issuer]` (`config.ts:
|
|
482
|
+
| `audiences` | string[] | `[]` | `aud` acceptées (RFC 8707) ; vide = `[issuer]` (`config.ts:446`) |
|
|
434
483
|
| `issuer` | string? | — | Claim `iss`, **STABLE** après émission ; omis → repli `"nodefony"`, qui n'est PAS publiable (RFC 8414 §2 exige une URL https) |
|
|
435
|
-
| `keystore.keySetJson` | string? | — | JWK Set privé
|
|
436
|
-
| `keystore.dir` | string? | — | Dossier `keyset.json` chmod 600 —
|
|
484
|
+
| `keystore.keySetJson` | string? | — | JWK Set privé partagé par tous les process (`NF_JWT_KEYSET`) — SECRET, forme vérifiée au démarrage (`security/nodefony/config/config.ts:452`) |
|
|
485
|
+
| `keystore.dir` | string? | — | Dossier `keyset.json` chmod 600, création exclusive — dev, ou serveur unique à disque persistant (`config.ts:434`) |
|
|
437
486
|
|
|
438
487
|
### `tokenStore.*`
|
|
439
488
|
|
|
440
489
|
| Option | Type | Défaut | Effet |
|
|
441
490
|
| ---------------------- | ---------- | -------- | ------------------------------------------------------------------------------------- |
|
|
442
|
-
| `store` | string | `"auto"` | `auto`\|`memory`\|`drizzle`\|`mongoose`\|`redis` — pluggable (`config.ts:
|
|
443
|
-
| `gcIntervalS` | number (s) | `600` | Purge périodique ; `0` = désactivé — chaque process purge SON store (`config.ts:
|
|
444
|
-
| `gcJitter` | boolean | `true` | Étale le gc d'un délai aléatoire par process — cluster (`config.ts:
|
|
445
|
-
| `retentionRevokedDays` | number (j) | `30` | Rétention d'un PAT révoqué SANS expiration avant purge (`config.ts:
|
|
491
|
+
| `store` | string | `"auto"` | `auto`\|`memory`\|`drizzle`\|`mongoose`\|`redis` — pluggable (`config.ts:452-457`) |
|
|
492
|
+
| `gcIntervalS` | number (s) | `600` | Purge périodique ; `0` = désactivé — chaque process purge SON store (`config.ts:501`) |
|
|
493
|
+
| `gcJitter` | boolean | `true` | Étale le gc d'un délai aléatoire par process — cluster (`config.ts:509`) |
|
|
494
|
+
| `retentionRevokedDays` | number (j) | `30` | Rétention d'un PAT révoqué SANS expiration avant purge (`config.ts:515`) |
|
|
446
495
|
|
|
447
496
|
## 📜 Normes appliquées
|
|
448
497
|
|
|
449
498
|
| Domaine | Norme | Ancrage |
|
|
450
499
|
| -------------------------------- | --------------- | ------------------------------------------------------- |
|
|
451
|
-
| Réponse d'émission | RFC 6749 §5.1 | `ITokenResponse` (`tokenService.ts:
|
|
452
|
-
| Rotation + détection de rejeu | RFC 9700 §4.14 | `refresh()` (`tokenService.ts:
|
|
453
|
-
| Profil access token `typ:at+jwt` | RFC 9068 | `#signAccess()` (`tokenService.ts:
|
|
454
|
-
| Claims JWT (`iss/sub/aud/exp`) | RFC 7519 | `#signAccess()` (`tokenService.ts:
|
|
455
|
-
| Ed25519 / JWK / JWKS public | RFC 8037 · 7517 | `#importKeyset()` (`JwtKeystore.ts:
|
|
500
|
+
| Réponse d'émission | RFC 6749 §5.1 | `ITokenResponse` (`tokenService.ts:50-58`) |
|
|
501
|
+
| Rotation + détection de rejeu | RFC 9700 §4.14 | `refresh()` (`tokenService.ts:647`) |
|
|
502
|
+
| Profil access token `typ:at+jwt` | RFC 9068 | `#signAccess()` (`tokenService.ts:694-707`) |
|
|
503
|
+
| Claims JWT (`iss/sub/aud/exp`) | RFC 7519 | `#signAccess()` (`tokenService.ts:731-739`) |
|
|
504
|
+
| Ed25519 / JWK / JWKS public | RFC 8037 · 7517 | `#importKeyset()` (`JwtKeystore.ts:275`) |
|
|
456
505
|
| Audiences liées à la ressource | RFC 8707 | `audience` du record (`ITokenStore.ts:104-105`) |
|
|
457
|
-
| 429 + `Retry-After` | RFC 6585 | `#renderAuthError()` (`TokenAuthController.ts:
|
|
458
|
-
| Backoff de login | NIST SP 800-63B | `ThrottledError` avant hachage (`tokenService.ts:
|
|
506
|
+
| 429 + `Retry-After` | RFC 6585 | `#renderAuthError()` (`TokenAuthController.ts:111-118`) |
|
|
507
|
+
| Backoff de login | NIST SP 800-63B | `ThrottledError` avant hachage (`tokenService.ts:433`) |
|
|
459
508
|
|
|
460
509
|
## ⚡ Performance & mémoire
|
|
461
510
|
|
|
462
|
-
- **`jose` importé lazy** (dep lourde) : `#ensureJose()` au premier usage (`tokenService.ts:
|
|
511
|
+
- **`jose` importé lazy** (dep lourde) : `#ensureJose()` au premier usage (`tokenService.ts:826`)
|
|
463
512
|
— le boot ne paie rien si le JWT n'est jamais sollicité ; keystore mémoïsé pareil.
|
|
464
513
|
- **Rien sur le hot path requête** : émission et rotation sont des endpoints cold-path ; la
|
|
465
514
|
vérification (hot path) vit chez le `JwtAuthenticator`.
|
|
466
515
|
- **Timers civilisés** : `GcScheduler` `unref` (n'empêche pas l'arrêt) + jitter anti-balayages
|
|
467
|
-
simultanés (`tokenService.ts:
|
|
516
|
+
simultanés (`tokenService.ts:233-239`) ; denylist mémoire bornée par purge amortie
|
|
468
517
|
(`MemoryTokenStore.ts:331-341`).
|
|
469
518
|
- **Jamais N en RAM** : `listPage()` borne toute lecture admin à une page (`ITokenStore.ts:233`).
|
|
470
519
|
|
|
471
520
|
## 📡 Observabilité — Studio
|
|
472
521
|
|
|
473
522
|
- **Écran Stores** (`/nodefony/stores`) : la résolution du store de jetons (configuré → résolu +
|
|
474
|
-
raison) publiée par `registerStoreResolution()` (`tokenService.ts:
|
|
523
|
+
raison) publiée par `registerStoreResolution()` (`tokenService.ts:195-204`).
|
|
475
524
|
- **Écran Audit** (`/nodefony/audit`) : événements `token.issued`, `token.reuse_detected` (signal d'attaque),
|
|
476
525
|
`login.failure`/`login.throttled` du grant — corrélables par `tokenId`.
|
|
477
526
|
- **Écran ApiKeys** : le même store côté PAT, listing paginé serveur.
|
|
478
527
|
|
|
479
528
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
480
529
|
|
|
481
|
-
| Symptôme
|
|
482
|
-
|
|
|
483
|
-
| 404 sur `/nodefony/security/api/token`
|
|
484
|
-
| 503 « Token issuance unavailable »
|
|
485
|
-
| Refresh tokens invalidés à chaque redémarrage
|
|
486
|
-
|
|
|
487
|
-
|
|
|
488
|
-
|
|
|
489
|
-
|
|
|
490
|
-
|
|
|
491
|
-
|
|
|
492
|
-
|
|
|
530
|
+
| Symptôme | Cause (dans le code) | Correction |
|
|
531
|
+
| ---------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- |
|
|
532
|
+
| 404 sur `/nodefony/security/api/token` | Module `@nodefony/security` non chargé | Charger `@nodefony/security` + `jwt.enabled: true` |
|
|
533
|
+
| 503 « Token issuance unavailable » | Service non initialisé (JWT désactivé, store indisponible) | Vérifier config `jwt`/`tokenStore` + logs de boot |
|
|
534
|
+
| Refresh tokens invalidés à chaque redémarrage | Keystore en mémoire (aucune source configurée) | `NF_JWT_KEYSET` (prod) ou `jwt.keystore.dir` (dev) |
|
|
535
|
+
| Boot refusé « DÉMARRAGE REFUSÉ — aucune clé de signature » | Production, process qui sert, ni `keySetJson` ni `dir` | `security:secrets --jwt-keyset` → secret → `NF_JWT_KEYSET` |
|
|
536
|
+
| Cluster arrêté « cluster master down (exit 78) » | Un worker a refusé de démarrer (faute de configuration) | Lire le motif du worker juste au-dessus, le corriger |
|
|
537
|
+
| JWT rejeté après un déploiement multi-pod | Clés différentes par pod (pas de clé partagée) | La même `NF_JWT_KEYSET` injectée dans chaque pod |
|
|
538
|
+
| Tout rejeté après changement de config | `issuer`/`audiences` divergents entre émission et vérification | `issuer` STABLE — ne pas le changer après émission |
|
|
539
|
+
| Révocation sans effet entre pods | `tokenStore:"memory"` en prod (per-pod) | Store durable (`NF_DATABASE_URL` → drizzle, ou redis) |
|
|
540
|
+
| Reconnexion forcée inattendue | Détection de rejeu : un vieux refresh révoqué a été rejoué | Attendu (anti-vol) — la famille est coupée |
|
|
541
|
+
| Boot avorté « token store inconnu » | `tokenStore.store` explicite introuvable (fail-loud prod) | Corriger le nom / enregistrer le store |
|
|
542
|
+
| Scopes qui n'augmentent pas au refresh | Downscoping volontaire | Réémettre via un nouveau grant pour élargir |
|
|
543
|
+
| Listing admin sans `total` sur Redis | `countTokens()` = `-1` (comptage O(N) refusé), curseur SCAN | Attendu — capacité réduite annoncée, paginer par curseur |
|
|
493
544
|
|
|
494
545
|
## 🧪 Tests & couverture
|
|
495
546
|
|
|
@@ -508,7 +559,7 @@ Cinq familles couvrent la brique — les **chiffres exacts vivent dans la carte
|
|
|
508
559
|
test de charge sur le grant.
|
|
509
560
|
|
|
510
561
|
Les bancs sur serveur réel se **skippent sans leurs variables d'infra** — un skip compte comme
|
|
511
|
-
vert : lire le bloc gates (`vitest
|
|
562
|
+
vert : lire le bloc gates (`scripts/test/vitest/gates.ts`, affiché en fin de run) avant de conclure.
|
|
512
563
|
Couverture : `npm run coverage` dans `@nodefony/security`.
|
|
513
564
|
|
|
514
565
|
## 🔗 Pour aller plus loin
|