@nodefony/security 10.0.0-beta.1 → 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/README.md +7 -5
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.153.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.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/securityHeaders.js +12 -1
- 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/securityHeaders.d.ts +2 -1
- 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 +18 -18
- package/docs/audit.md +24 -22
- package/docs/authenticators.md +62 -29
- package/docs/authorization.md +16 -16
- package/docs/cors.md +30 -30
- package/docs/csrf.md +35 -33
- package/docs/external-jwt.md +68 -5
- package/docs/firewall.md +78 -45
- package/docs/headers.md +35 -35
- package/docs/index.md +15 -7
- package/docs/keycloak.md +818 -0
- package/docs/lexique.md +15 -15
- package/docs/login-page.md +361 -0
- package/docs/oauth2.md +242 -125
- package/docs/obtenir-un-jeton.md +18 -11
- package/docs/tokens.md +138 -86
- package/docs/totp.md +113 -71
- package/docs/webauthn.md +115 -115
- package/docs/webhooks.md +21 -21
- 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-
|
|
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
|
|
@@ -142,8 +142,9 @@ le framework monte deux routes (`mountTokenAuthRoutes()`, `TokenAuthController.t
|
|
|
142
142
|
- `POST /nodefony/security/api/token/refresh` — body `{refresh_token}` → rotation.
|
|
143
143
|
|
|
144
144
|
> [!IMPORTANT]
|
|
145
|
-
> Ces routes n'existent **que si**
|
|
146
|
-
> (`
|
|
145
|
+
> Ces routes n'existent **que si** `@nodefony/security` est chargé — sinon 404, zéro surface
|
|
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:188`) :
|
|
147
148
|
> elles SONT le mécanisme d'émission — protégées, obtenir un token exigerait d'être déjà
|
|
148
149
|
> authentifié (deadlock). Le JWT part en **réponse JSON** (Bearer), jamais en cookie ni en URL.
|
|
149
150
|
|
|
@@ -153,10 +154,12 @@ le framework monte deux routes (`mountTokenAuthRoutes()`, `TokenAuthController.t
|
|
|
153
154
|
// nodefony.config.ts (extrait) — la zone API machine + la source de clé
|
|
154
155
|
use("@nodefony/security", {
|
|
155
156
|
jwt: {
|
|
156
|
-
// dev
|
|
157
|
-
// prod
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
},
|
|
160
163
|
},
|
|
161
164
|
areas: {
|
|
162
165
|
// Le firewall vérifie le Bearer JWT sur CHAQUE requête de la zone.
|
|
@@ -228,36 +231,36 @@ Erreurs mappées par duck-typing dans `#renderAuthError()` (`TokenAuthController
|
|
|
228
231
|
|
|
229
232
|
### Émission (grant M2M/CLI)
|
|
230
233
|
|
|
231
|
-
`issueForCredentials()` (`tokenService.ts:
|
|
234
|
+
`issueForCredentials()` (`tokenService.ts:387`) vérifie l'identifiant/mot de passe via le
|
|
232
235
|
service `users`, avec le **throttling NIST partagé** — `ThrottledError` avant tout hachage
|
|
233
|
-
(`tokenService.ts:
|
|
234
|
-
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`)
|
|
235
238
|
produit :
|
|
236
239
|
|
|
237
240
|
- un **access token** : JWT signé EdDSA, en-tête `typ:"at+jwt"` + `kid`, claims
|
|
238
|
-
`iss`/`sub`/`aud`/`exp` (15 min) + `jti` — `#signAccess()` (`tokenService.ts:
|
|
241
|
+
`iss`/`sub`/`aud`/`exp` (15 min) + `jti` — `#signAccess()` (`tokenService.ts:694-707`) ;
|
|
239
242
|
- un **refresh token** : secret opaque haute entropie `nfr_<32 octets base64url>`, **stocké haché**
|
|
240
243
|
`sha256` (le clair n'existe qu'en réponse, jamais au repos) — `#buildRefresh()`
|
|
241
|
-
(`tokenService.ts:
|
|
244
|
+
(`tokenService.ts:726-761`).
|
|
242
245
|
|
|
243
|
-
La réponse suit RFC 6749 §5.1 — `ITokenResponse` (`tokenService.ts:
|
|
244
|
-
`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`).
|
|
245
248
|
|
|
246
249
|
### Rotation & détection de rejeu (RFC 9700 §4.14)
|
|
247
250
|
|
|
248
|
-
`refresh()` (`tokenService.ts:
|
|
251
|
+
`refresh()` (`tokenService.ts:647`) est le cœur défensif, dans l'ordre :
|
|
249
252
|
|
|
250
|
-
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`).
|
|
251
254
|
2. **Détection de rejeu** : refresh **déjà révoqué** re-présenté → `revokeFamily` coupe toute la
|
|
252
|
-
famille + audit `token.reuse_detected`, signal d'attaque fort (`tokenService.ts:
|
|
253
|
-
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`).
|
|
254
257
|
4. **Sujet revérifié** — compte disparu/inactif/verrouillé rejeté sans attendre l'exp,
|
|
255
|
-
`#resolveUserForRefresh()` (`tokenService.ts:
|
|
258
|
+
`#resolveUserForRefresh()` (`tokenService.ts:852`).
|
|
256
259
|
5. **Downscoping** : les `scopes` du nouveau couple sont ceux de l'ancien, jamais plus
|
|
257
|
-
(`tokenService.ts:
|
|
260
|
+
(`tokenService.ts:684`).
|
|
258
261
|
6. **Rotation** : nouveau refresh (même famille), l'ancien chaîné `replacedBy` + révoqué
|
|
259
|
-
`"rotated"` (`tokenService.ts:
|
|
260
|
-
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`).
|
|
261
264
|
|
|
262
265
|
### Mise en situation — ton refresh token a été volé
|
|
263
266
|
|
|
@@ -277,26 +280,73 @@ la victime est déconnectée (signal visible) au lieu d'un vol silencieux indéf
|
|
|
277
280
|
|
|
278
281
|
## 🔐 Le keystore Ed25519 — la clé ne fuit pas, pas de secret « par défaut » en prod
|
|
279
282
|
|
|
280
|
-
`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
|
|
281
284
|
pour ne jamais auto-générer une clé en clair silencieusement en prod :
|
|
282
285
|
|
|
283
|
-
1. **env** — `keySetJson` (
|
|
284
|
-
|
|
285
|
-
2. **fichier** — `dir/keyset.json`, généré si absent,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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`.
|
|
291
297
|
La composante privée `d` en est retirée à l'import, par liste BLANCHE de paramètres
|
|
292
|
-
(`#importKeyset()`, `JwtKeystore.ts:
|
|
298
|
+
(`#importKeyset()`, `JwtKeystore.ts:275`, RFC 8037/7517).
|
|
293
299
|
C'est ce JWKS qu'utilise le vérificateur local (`createLocalJWKSet`, `JwtAuthenticator.ts:174`),
|
|
294
|
-
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
|
+
```
|
|
295
340
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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.
|
|
300
350
|
|
|
301
351
|
## 🧩 Le store pluggable — durable par défaut, jamais de faux durable silencieux
|
|
302
352
|
|
|
@@ -307,18 +357,18 @@ seuil `invalidBefore` par porteur (`ITokenStore.ts:11-15`). Les backends s'enreg
|
|
|
307
357
|
fabrique — `registerTokenStore()` (`tokenStoreRegistry.ts:41-46`) : les adapters lourds importent
|
|
308
358
|
`import type { ITokenStore }` (effacé à la compilation), zéro couplage runtime.
|
|
309
359
|
|
|
310
|
-
Sa résolution au boot (`tokenService.ts:
|
|
360
|
+
Sa résolution au boot (`tokenService.ts:156-207`) suit la doctrine `store:"auto"` du framework :
|
|
311
361
|
|
|
312
362
|
- `auto` (défaut) → suit l'infra database déclarée via `resolveAutoStore` — **borné aux backends
|
|
313
|
-
réellement enregistrés**, repli memory **annoncé** (`tokenService.ts:
|
|
363
|
+
réellement enregistrés**, repli memory **annoncé** (`tokenService.ts:160-168`) ;
|
|
314
364
|
- store explicite **inconnu** → en prod, **boot avorté** (fail-loud) ; en dev, brique désactivée et
|
|
315
|
-
annoncée avec la liste `listTokenStores()` (`tokenService.ts:
|
|
365
|
+
annoncée avec la liste `listTokenStores()` (`tokenService.ts:171-182`) — jamais de fallback
|
|
316
366
|
memory silencieux pour du durable ;
|
|
317
367
|
- store `memory` **en prod** → `WARNING` nommant l'impact : denylist/refresh/clés API per-pod et
|
|
318
|
-
volatils, révocation non partagée (`tokenService.ts:
|
|
368
|
+
volatils, révocation non partagée (`tokenService.ts:186-193`).
|
|
319
369
|
|
|
320
370
|
La décision (configuré → résolu, raison) est publiée au kernel par `registerStoreResolution()`
|
|
321
|
-
(`tokenService.ts:
|
|
371
|
+
(`tokenService.ts:195-204`) — visible dans Studio.
|
|
322
372
|
|
|
323
373
|
### Mise en situation — quel store pour quelle app ?
|
|
324
374
|
|
|
@@ -347,7 +397,7 @@ La décision (configuré → résolu, raison) est publiée au kernel par `regist
|
|
|
347
397
|
|
|
348
398
|
- Enregistré par le module drizzle (`drizzle/nodefony/registerStores.ts:261`).
|
|
349
399
|
- Élu par `store:"auto"` dès qu'une infra database SQL est déclarée ; sinon sqlite local si drizzle
|
|
350
|
-
est chargé (`config.ts:
|
|
400
|
+
est chargé (`config.ts:452-457`).
|
|
351
401
|
- Pagination **offset + total** (helper `paginate()` d'orm-core) ; e2e sur PostgreSQL et MySQL réels.
|
|
352
402
|
|
|
353
403
|
### `mongoose` — MongoDB
|
|
@@ -359,12 +409,12 @@ La décision (configuré → résolu, raison) est publiée au kernel par `regist
|
|
|
359
409
|
### `redis` — cluster, TTL natif
|
|
360
410
|
|
|
361
411
|
- Enregistré par le module redis (`redis/nodefony/registerStores.ts:75`).
|
|
362
|
-
- 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
|
|
363
413
|
dépend pas du gc.
|
|
364
414
|
- Listing par `SCAN` : curseur opaque `skip:scanCursor`, `decodeCursor()`
|
|
365
415
|
(`RedisTokenStore.ts:428`) — sans ordre global ni total, capacité réduite **assumée**.
|
|
366
416
|
- `countTokens()` renvoie `-1` : un comptage exact exigerait un SCAN complet O(N), refusé
|
|
367
|
-
(`RedisTokenStore.ts:
|
|
417
|
+
(`RedisTokenStore.ts:529`).
|
|
368
418
|
|
|
369
419
|
### Le record — une seule table pour PAT et refresh
|
|
370
420
|
|
|
@@ -405,7 +455,7 @@ Les colonnes par dialecte vivent dans la doc de chaque adapter (règle anti-trip
|
|
|
405
455
|
|
|
406
456
|
Le `gc()` du store purge la `denylist` expirée, les records à terme, les PAT révoqués au-delà de
|
|
407
457
|
la rétention (`ITokenStore.ts:274-280`). Orchestré par le `GcScheduler` du service ; `runGc()` reste public pour
|
|
408
|
-
un futur worker cron — poser alors `gcIntervalS: 0` (`tokenService.ts:
|
|
458
|
+
un futur worker cron — poser alors `gcIntervalS: 0` (`tokenService.ts:338`).
|
|
409
459
|
|
|
410
460
|
> [!TIP]
|
|
411
461
|
> Un refresh révoqué **par rotation** n'est PAS purgé tout de suite : il est conservé jusqu'à son
|
|
@@ -415,80 +465,82 @@ un futur worker cron — poser alors `gcIntervalS: 0` (`tokenService.ts:293`).
|
|
|
415
465
|
|
|
416
466
|
## ⚙️ Configuration
|
|
417
467
|
|
|
418
|
-
Tables dérivées du schéma Zod — `jwtSchema` (`config.ts:
|
|
419
|
-
(`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.
|
|
420
470
|
|
|
421
471
|
### `jwt.*`
|
|
422
472
|
|
|
423
473
|
<!-- prettier-ignore -->
|
|
424
474
|
| Option | Type | Défaut | Effet |
|
|
425
475
|
| --- | --- | --- | --- |
|
|
426
|
-
| `enabled` | boolean | `true` | Active signature + refresh (`config.ts:
|
|
476
|
+
| `enabled` | boolean | `true` | Active signature + refresh (`config.ts:375`) |
|
|
427
477
|
| `alg` | `EdDSA` \| `RS256` | `EdDSA` | `RS256` = slot non câblé (`jwtRuntime.ts:21`) |
|
|
428
|
-
| `accessTtlS` | number (s) | `900` | TTL de l'access token — 15 min (`config.ts:
|
|
429
|
-
| `refreshTtlS` | number (s) | `604800` | TTL du refresh — 7 jours (`config.ts:
|
|
430
|
-
| `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`) |
|
|
431
481
|
| `jwks` | boolean | `true` | Publie `/.well-known/jwks.json` + les métadonnées RFC 8414 — sans `issuer` en URL https, rien n'est publié |
|
|
432
|
-
| `audiences` | string[] | `[]` | `aud` acceptées (RFC 8707) ; vide = `[issuer]` (`config.ts:
|
|
482
|
+
| `audiences` | string[] | `[]` | `aud` acceptées (RFC 8707) ; vide = `[issuer]` (`config.ts:446`) |
|
|
433
483
|
| `issuer` | string? | — | Claim `iss`, **STABLE** après émission ; omis → repli `"nodefony"`, qui n'est PAS publiable (RFC 8414 §2 exige une URL https) |
|
|
434
|
-
| `keystore.keySetJson` | string? | — | JWK Set privé
|
|
435
|
-
| `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`) |
|
|
436
486
|
|
|
437
487
|
### `tokenStore.*`
|
|
438
488
|
|
|
439
489
|
| Option | Type | Défaut | Effet |
|
|
440
490
|
| ---------------------- | ---------- | -------- | ------------------------------------------------------------------------------------- |
|
|
441
|
-
| `store` | string | `"auto"` | `auto`\|`memory`\|`drizzle`\|`mongoose`\|`redis` — pluggable (`config.ts:
|
|
442
|
-
| `gcIntervalS` | number (s) | `600` | Purge périodique ; `0` = désactivé — chaque process purge SON store (`config.ts:
|
|
443
|
-
| `gcJitter` | boolean | `true` | Étale le gc d'un délai aléatoire par process — cluster (`config.ts:
|
|
444
|
-
| `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`) |
|
|
445
495
|
|
|
446
496
|
## 📜 Normes appliquées
|
|
447
497
|
|
|
448
498
|
| Domaine | Norme | Ancrage |
|
|
449
499
|
| -------------------------------- | --------------- | ------------------------------------------------------- |
|
|
450
|
-
| Réponse d'émission | RFC 6749 §5.1 | `ITokenResponse` (`tokenService.ts:
|
|
451
|
-
| Rotation + détection de rejeu | RFC 9700 §4.14 | `refresh()` (`tokenService.ts:
|
|
452
|
-
| Profil access token `typ:at+jwt` | RFC 9068 | `#signAccess()` (`tokenService.ts:
|
|
453
|
-
| Claims JWT (`iss/sub/aud/exp`) | RFC 7519 | `#signAccess()` (`tokenService.ts:
|
|
454
|
-
| 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`) |
|
|
455
505
|
| Audiences liées à la ressource | RFC 8707 | `audience` du record (`ITokenStore.ts:104-105`) |
|
|
456
|
-
| 429 + `Retry-After` | RFC 6585 | `#renderAuthError()` (`TokenAuthController.ts:
|
|
457
|
-
| 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`) |
|
|
458
508
|
|
|
459
509
|
## ⚡ Performance & mémoire
|
|
460
510
|
|
|
461
|
-
- **`jose` importé lazy** (dep lourde) : `#ensureJose()` au premier usage (`tokenService.ts:
|
|
511
|
+
- **`jose` importé lazy** (dep lourde) : `#ensureJose()` au premier usage (`tokenService.ts:826`)
|
|
462
512
|
— le boot ne paie rien si le JWT n'est jamais sollicité ; keystore mémoïsé pareil.
|
|
463
513
|
- **Rien sur le hot path requête** : émission et rotation sont des endpoints cold-path ; la
|
|
464
514
|
vérification (hot path) vit chez le `JwtAuthenticator`.
|
|
465
515
|
- **Timers civilisés** : `GcScheduler` `unref` (n'empêche pas l'arrêt) + jitter anti-balayages
|
|
466
|
-
simultanés (`tokenService.ts:
|
|
516
|
+
simultanés (`tokenService.ts:233-239`) ; denylist mémoire bornée par purge amortie
|
|
467
517
|
(`MemoryTokenStore.ts:331-341`).
|
|
468
518
|
- **Jamais N en RAM** : `listPage()` borne toute lecture admin à une page (`ITokenStore.ts:233`).
|
|
469
519
|
|
|
470
520
|
## 📡 Observabilité — Studio
|
|
471
521
|
|
|
472
522
|
- **Écran Stores** (`/nodefony/stores`) : la résolution du store de jetons (configuré → résolu +
|
|
473
|
-
raison) publiée par `registerStoreResolution()` (`tokenService.ts:
|
|
523
|
+
raison) publiée par `registerStoreResolution()` (`tokenService.ts:195-204`).
|
|
474
524
|
- **Écran Audit** (`/nodefony/audit`) : événements `token.issued`, `token.reuse_detected` (signal d'attaque),
|
|
475
525
|
`login.failure`/`login.throttled` du grant — corrélables par `tokenId`.
|
|
476
526
|
- **Écran ApiKeys** : le même store côté PAT, listing paginé serveur.
|
|
477
527
|
|
|
478
528
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
479
529
|
|
|
480
|
-
| Symptôme
|
|
481
|
-
|
|
|
482
|
-
| 404 sur `/nodefony/security/api/token`
|
|
483
|
-
| 503 « Token issuance unavailable »
|
|
484
|
-
| Refresh tokens invalidés à chaque redémarrage
|
|
485
|
-
|
|
|
486
|
-
|
|
|
487
|
-
|
|
|
488
|
-
|
|
|
489
|
-
|
|
|
490
|
-
|
|
|
491
|
-
|
|
|
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 |
|
|
492
544
|
|
|
493
545
|
## 🧪 Tests & couverture
|
|
494
546
|
|
|
@@ -507,7 +559,7 @@ Cinq familles couvrent la brique — les **chiffres exacts vivent dans la carte
|
|
|
507
559
|
test de charge sur le grant.
|
|
508
560
|
|
|
509
561
|
Les bancs sur serveur réel se **skippent sans leurs variables d'infra** — un skip compte comme
|
|
510
|
-
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.
|
|
511
563
|
Couverture : `npm run coverage` dans `@nodefony/security`.
|
|
512
564
|
|
|
513
565
|
## 🔗 Pour aller plus loin
|