@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.5
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.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +6 -8
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +18 -11
- package/dist/nodefony/command/security-oauth-doctor.js +5 -3
- package/dist/nodefony/command/security-secrets.js +10 -4
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +5 -3
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +5 -3
- package/dist/nodefony/config/config.js +20 -4
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/errors/AccessDeniedError.js +1 -2
- package/dist/nodefony/errors/ApiKeyError.js +3 -2
- package/dist/nodefony/errors/AuthenticationError.js +1 -2
- package/dist/nodefony/errors/CsrfError.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +1 -2
- package/dist/nodefony/errors/InvalidTargetError.js +1 -2
- package/dist/nodefony/errors/SsrfError.js +1 -2
- package/dist/nodefony/errors/ThrottledError.js +1 -2
- package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
- package/dist/nodefony/errors/WebAuthnError.js +1 -2
- package/dist/nodefony/keycloak/keycloakAdmin.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealm.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealmInput.js +1 -2
- package/dist/nodefony/keycloak/scaffold.js +3 -3
- package/dist/nodefony/keycloak/security-keycloak-realm.js +5 -3
- package/dist/nodefony/scaffold/contribute.js +1 -2
- package/dist/nodefony/service/accessTokenVerifier.js +101 -34
- package/dist/nodefony/service/apiKeys.js +45 -5
- package/dist/nodefony/service/auditService.js +4 -5
- package/dist/nodefony/service/authFlow.js +10 -8
- package/dist/nodefony/service/authorization.js +1 -2
- package/dist/nodefony/service/cors.js +1 -2
- package/dist/nodefony/service/csrf.js +1 -2
- package/dist/nodefony/service/firewall.js +5 -4
- package/dist/nodefony/service/keycloak.js +1 -2
- package/dist/nodefony/service/oauth2.js +29 -7
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +38 -22
- package/dist/nodefony/service/totp.js +4 -5
- package/dist/nodefony/service/webAuthn.js +4 -5
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +1 -2
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +1 -2
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -2
- package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
- package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
- package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
- package/dist/nodefony/src/audit/auditBridge.js +1 -2
- package/dist/nodefony/src/audit/auditFilters.js +1 -2
- package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
- package/dist/nodefony/src/audit/readAuditContext.js +1 -2
- package/dist/nodefony/src/audit/recordAudit.js +1 -2
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +8 -113
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +42 -4
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +91 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +3 -17
- package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
- package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +1 -2
- package/dist/nodefony/src/crypto/secretCipher.js +1 -2
- package/dist/nodefony/src/csp.js +1 -2
- package/dist/nodefony/src/csrfToken.js +1 -2
- package/dist/nodefony/src/entryPoint.js +1 -2
- package/dist/nodefony/src/net/ssrfGuard.js +1 -2
- package/dist/nodefony/src/net/transportFailure.js +57 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +1 -2
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +5 -16
- package/dist/nodefony/src/oauth/oauth2Client.js +1 -2
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +1 -2
- package/dist/nodefony/src/oauth/providerDiagnosis.js +5 -6
- package/dist/nodefony/src/oauth/providerRoles.js +1 -2
- package/dist/nodefony/src/oauth/providers/github.js +16 -6
- package/dist/nodefony/src/oauth/providers/oidc.js +9 -6
- package/dist/nodefony/src/openToSignup.js +1 -2
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
- package/dist/nodefony/src/realtime/frameAuthorizer.js +2 -3
- package/dist/nodefony/src/rolesWithin.js +57 -0
- package/dist/nodefony/src/sessionAuthentication.js +1 -2
- package/dist/nodefony/src/sessionIdentity.js +29 -3
- package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
- package/dist/nodefony/src/token/AnonymousToken.js +1 -2
- package/dist/nodefony/src/token/JwtKeystore.js +14 -5
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +6 -3
- package/dist/nodefony/src/token/UserToken.js +1 -2
- package/dist/nodefony/src/token/jwtRuntime.js +1 -2
- package/dist/nodefony/src/token/scopeGrants.js +109 -0
- package/dist/nodefony/src/token/secretFile.js +1 -2
- package/dist/nodefony/src/token/tokenCriteria.js +1 -2
- package/dist/nodefony/src/token/tokenFilters.js +1 -2
- package/dist/nodefony/src/token/tokenSort.js +1 -2
- package/dist/nodefony/src/token/tokenStatus.js +1 -2
- package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
- package/dist/nodefony/src/totp/totpCipher.js +1 -2
- package/dist/nodefony/src/totp/totpCrypto.js +1 -2
- package/dist/nodefony/src/totp/totpOperations.js +1 -2
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +1 -2
- package/dist/nodefony/src/voter/RoleVoter.js +1 -2
- package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
- package/dist/nodefony/src/voter/voterRegistry.js +1 -2
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
- package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
- package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
- package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
- package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
- package/dist/nodefony/src/webhook/webhookSort.js +1 -2
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
- package/dist/nodefony/testing/index.js +108 -0
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
- package/dist/types/nodefony/config/config.d.ts +1 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- package/dist/types/nodefony/service/oauth2.d.ts +20 -0
- package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +2 -2
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
- package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
- package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
- package/dist/types/nodefony/testing/index.d.ts +51 -0
- package/docs/api-keys.md +181 -174
- package/docs/audit.md +140 -140
- package/docs/authenticators.md +101 -101
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +74 -71
- package/docs/external-jwt.md +22 -12
- package/docs/firewall.md +85 -82
- package/docs/headers.md +81 -81
- package/docs/index.md +4 -4
- package/docs/keycloak.md +95 -78
- package/docs/login-page.md +23 -23
- package/docs/oauth2.md +157 -146
- package/docs/obtenir-un-jeton.md +116 -26
- package/docs/tokens.md +128 -122
- package/docs/totp.md +136 -136
- package/docs/webauthn.md +153 -153
- package/docs/webhooks.md +136 -124
- package/package.json +19 -13
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/tokens.md
CHANGED
|
@@ -87,20 +87,20 @@ 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#TokenService.#build`), si `jwt.enabled` ou `apiKeys.enabled`, il résout le store pluggable, pose
|
|
91
|
+
`tokenStore` au container (`tokenService.ts#TokenService.#build`) puis crée le keystore et pose `jwtKeystore`
|
|
92
|
+
(`tokenService.ts#TokenService.#build`) — 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#TokenService.#build`).
|
|
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
|
|
98
|
-
(`TokenAuthController.ts
|
|
98
|
+
(`TokenAuthController.ts#ITokenIssuer`).
|
|
99
99
|
|
|
100
100
|
Constat clé de cohérence : `iss`/`aud`/`ttl` sont dérivés **une seule fois** par
|
|
101
|
-
`resolveJwtRuntime()` (`jwtRuntime.ts
|
|
101
|
+
`resolveJwtRuntime()` (`jwtRuntime.ts#resolveJwtRuntime`) et **partagés** entre l'émetteur et le vérificateur —
|
|
102
102
|
une divergence ferait tout rejeter. Fonction pure : les deux côtés obtiennent la même valeur sans la
|
|
103
|
-
partager par référence. `issuer` omis → `"nodefony"` (`jwtRuntime.ts
|
|
103
|
+
partager par référence. `issuer` omis → `"nodefony"` (`jwtRuntime.ts#resolveJwtRuntime`), à surcharger en prod.
|
|
104
104
|
|
|
105
105
|
### Être DÉCOUVRABLE — publier ses clés (RFC 8414)
|
|
106
106
|
|
|
@@ -136,36 +136,41 @@ seulement ses signatures vérifiables. Omettre `grant_types_supported` annoncera
|
|
|
136
136
|
### Les endpoints d'émission sont FOURNIS
|
|
137
137
|
|
|
138
138
|
Dans une app `nodefony create app`, dès que le module security est chargé avec `jwt.enabled` (défaut),
|
|
139
|
-
le framework monte deux routes (`mountTokenAuthRoutes()`, `TokenAuthController.ts
|
|
139
|
+
le framework monte deux routes (`mountTokenAuthRoutes()`, `TokenAuthController.ts#mountTokenAuthRoutes`) :
|
|
140
140
|
|
|
141
141
|
- `POST /nodefony/security/api/token` — body `{username, password, scope?}` → couple access/refresh ;
|
|
142
142
|
- `POST /nodefony/security/api/token/refresh` — body `{refresh_token}` → rotation.
|
|
143
143
|
|
|
144
144
|
> [!IMPORTANT]
|
|
145
145
|
> Ces routes n'existent **que si** `@nodefony/security` est chargé — sinon 404, zéro surface
|
|
146
|
-
> (`framework/index.ts
|
|
147
|
-
> `503 Token issuance unavailable` (`TokenAuthController.ts
|
|
146
|
+
> (`framework/index.ts#Framework.onKernelReady`). Module chargé mais JWT désactivé : elles existent et répondent
|
|
147
|
+
> `503 Token issuance unavailable` (`TokenAuthController.ts#TokenAuthController.token`). Elles sont `bypassFirewall: true` (`TokenAuthController.ts#mountTokenAuthRoutes`) :
|
|
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
|
|
|
151
151
|
### La config : une zone protégée par `jwt` + une clé qui survit au redémarrage
|
|
152
152
|
|
|
153
|
-
```typescript
|
|
154
|
-
// nodefony
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
153
|
+
```typescript ignore
|
|
154
|
+
// nodefony/config/security.ts — la zone API machine + la source de clé
|
|
155
|
+
import type { ConfigContext } from "nodefony";
|
|
156
|
+
import type { ISecurityConfigInput } from "@nodefony/security";
|
|
157
|
+
import type { env } from "../../env";
|
|
158
|
+
|
|
159
|
+
export const securityConfig = (ctx: ConfigContext<typeof env>) =>
|
|
160
|
+
({
|
|
161
|
+
jwt: {
|
|
162
|
+
// dev : persiste la clé Ed25519 sous var/ (sinon clé ÉPHÉMÈRE + warning).
|
|
163
|
+
// prod : NF_JWT_KEYSET, la même clé sur tous les pods — voir la section keystore.
|
|
164
|
+
keystore: {
|
|
165
|
+
keySetJson: ctx.env.NF_JWT_KEYSET,
|
|
166
|
+
dir: "var/keys",
|
|
167
|
+
},
|
|
162
168
|
},
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
+
areas: {
|
|
170
|
+
// Le firewall vérifie le Bearer JWT sur CHAQUE requête de la zone.
|
|
171
|
+
api: { pattern: "^/api/v1", authenticators: ["jwt"] },
|
|
172
|
+
},
|
|
173
|
+
}) satisfies ISecurityConfigInput;
|
|
169
174
|
```
|
|
170
175
|
|
|
171
176
|
### Ce que TU écris : le controller scopé
|
|
@@ -222,45 +227,46 @@ curl -s -H 'Content-Type: application/json' \
|
|
|
222
227
|
# 4) Rejouer l'ANCIEN refresh → 401 {"error":"invalid_grant"} + famille coupée
|
|
223
228
|
```
|
|
224
229
|
|
|
225
|
-
Erreurs mappées par duck-typing dans `#renderAuthError()` (`TokenAuthController.ts
|
|
230
|
+
Erreurs mappées par duck-typing dans `#renderAuthError()` (`TokenAuthController.ts#TokenAuthController.#renderAuthError`) :
|
|
226
231
|
401 message uniforme `invalid_grant` (anti-énumération), 429 avec `Retry-After` du throttler NIST.
|
|
227
232
|
Émission indisponible (JWT désactivé, store absent) → 503 `isEnabled()`
|
|
228
|
-
(`TokenAuthController.ts
|
|
233
|
+
(`TokenAuthController.ts#ITokenIssuer.isEnabled`).
|
|
229
234
|
|
|
230
235
|
## 🏗️ Architecture interne — la vie d'un couple access/refresh
|
|
231
236
|
|
|
232
237
|
### Émission (grant M2M/CLI)
|
|
233
238
|
|
|
234
|
-
`issueForCredentials()` (`tokenService.ts
|
|
239
|
+
`issueForCredentials()` (`tokenService.ts#TokenService.issueForCredentials`) vérifie l'identifiant/mot de passe via le
|
|
235
240
|
service `users`, avec le **throttling NIST partagé** — `ThrottledError` avant tout hachage
|
|
236
|
-
(`tokenService.ts
|
|
237
|
-
par `#auditGrant()` (`tokenService.ts
|
|
241
|
+
(`tokenService.ts#TokenService.issueForCredentials`). Chaque tentative échouée est auditée `login.failure`/`login.throttled`
|
|
242
|
+
par `#auditGrant()` (`tokenService.ts#TokenService.#auditGrant`). Puis `issueTokens()` (`tokenService.ts#TokenService.issueTokens`)
|
|
238
243
|
produit :
|
|
239
244
|
|
|
240
245
|
- un **access token** : JWT signé EdDSA, en-tête `typ:"at+jwt"` + `kid`, claims
|
|
241
|
-
`iss`/`sub`/`aud`/`exp` (15 min) + `jti` — `#signAccess()` (`tokenService.ts
|
|
246
|
+
`iss`/`sub`/`aud`/`exp` (15 min) + `jti` — `#signAccess()` (`tokenService.ts#TokenService.#signAccess`) ;
|
|
242
247
|
- un **refresh token** : secret opaque haute entropie `nfr_<32 octets base64url>`, **stocké haché**
|
|
243
248
|
`sha256` (le clair n'existe qu'en réponse, jamais au repos) — `#buildRefresh()`
|
|
244
|
-
(`tokenService.ts
|
|
249
|
+
(`tokenService.ts#TokenService.#buildRefresh`).
|
|
245
250
|
|
|
246
|
-
La réponse suit RFC 6749 §5.1 — `ITokenResponse` (`tokenService.ts
|
|
247
|
-
`token.issued` via `recordAudit` avec le `tokenId` corrélable (`tokenService.ts
|
|
251
|
+
La réponse suit RFC 6749 §5.1 — `ITokenResponse` (`tokenService.ts#ITokenResponse`). Tout succès est audité
|
|
252
|
+
`token.issued` via `recordAudit` avec le `tokenId` corrélable (`tokenService.ts#TokenService.runGc`).
|
|
248
253
|
|
|
249
254
|
### Rotation & détection de rejeu (RFC 9700 §4.14)
|
|
250
255
|
|
|
251
|
-
`refresh()` (`tokenService.ts
|
|
256
|
+
`refresh()` (`tokenService.ts#TokenService.refresh`) est le cœur défensif, dans l'ordre :
|
|
252
257
|
|
|
253
|
-
1. Lookup par hash — `findByHash`, refus uniforme si inconnu/mauvais type (`tokenService.ts
|
|
258
|
+
1. Lookup par hash — `findByHash`, refus uniforme si inconnu/mauvais type (`tokenService.ts#TokenService.refresh`).
|
|
254
259
|
2. **Détection de rejeu** : refresh **déjà révoqué** re-présenté → `revokeFamily` coupe toute la
|
|
255
|
-
famille + audit `token.reuse_detected`, signal d'attaque fort (`tokenService.ts
|
|
256
|
-
3. Expiration `expiresAt` vérifiée (`tokenService.ts
|
|
260
|
+
famille + audit `token.reuse_detected`, signal d'attaque fort (`tokenService.ts#TokenService.refresh`).
|
|
261
|
+
3. Expiration `expiresAt` vérifiée (`tokenService.ts#TokenService.refresh`).
|
|
257
262
|
4. **Sujet revérifié** — compte disparu/inactif/verrouillé rejeté sans attendre l'exp,
|
|
258
|
-
`#resolveUserForRefresh()` (`tokenService.ts
|
|
259
|
-
5. **Downscoping** : les `scopes` du nouveau couple sont ceux de l'ancien, jamais plus
|
|
260
|
-
(`
|
|
263
|
+
`#resolveUserForRefresh()` (`tokenService.ts#TokenService.#resolveUserForRefresh`).
|
|
264
|
+
5. **Downscoping** : les `scopes` du nouveau couple sont ceux de l'ancien, jamais plus — et ils
|
|
265
|
+
sont REJUGÉS sur les rôles du compte relu (`grantableScopes`, règle d'administration) : un rôle
|
|
266
|
+
retiré retire son scope au prochain renouvellement (`tokenService.ts#TokenService.#grantableScopes`).
|
|
261
267
|
6. **Rotation** : nouveau refresh (même famille), l'ancien chaîné `replacedBy` + révoqué
|
|
262
|
-
`"rotated"` (`tokenService.ts
|
|
263
|
-
et le refresh courant reste valide (`tokenService.ts
|
|
268
|
+
`"rotated"` (`tokenService.ts#TokenService.refresh`). Si `rotateRefresh` est désactivé, l'access est réémis
|
|
269
|
+
et le refresh courant reste valide (`tokenService.ts#TokenService.refresh`).
|
|
264
270
|
|
|
265
271
|
### Mise en situation — ton refresh token a été volé
|
|
266
272
|
|
|
@@ -280,24 +286,24 @@ la victime est déconnectée (signal visible) au lieu d'un vol silencieux indéf
|
|
|
280
286
|
|
|
281
287
|
## 🔐 Le keystore Ed25519 — la clé ne fuit pas, pas de secret « par défaut » en prod
|
|
282
288
|
|
|
283
|
-
`JwtKeystore.#load()` résout la source de clé par **priorité** (`JwtKeystore.ts
|
|
289
|
+
`JwtKeystore.#load()` résout la source de clé par **priorité** (`JwtKeystore.ts#JwtKeystore.#load`), pensée
|
|
284
290
|
pour ne jamais auto-générer une clé en clair silencieusement en prod :
|
|
285
291
|
|
|
286
292
|
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
|
|
293
|
+
les process qui servent l'application (`JwtKeystore.ts#JwtKeystore.#load`).
|
|
288
294
|
2. **fichier** — `dir/keyset.json`, généré si absent, en mode 600, par une création **exclusive**
|
|
289
|
-
(`createSecretExclusive`, `secretFile.ts
|
|
290
|
-
démarrent ensemble, un seul crée la clé et les autres relisent la sienne (`JwtKeystore.ts
|
|
295
|
+
(`createSecretExclusive`, `secretFile.ts#createSecretExclusive`) : quand plusieurs workers de `nodefony cluster`
|
|
296
|
+
démarrent ensemble, un seul crée la clé et les autres relisent la sienne (`JwtKeystore.ts#JwtKeystore.#load`).
|
|
291
297
|
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
|
|
298
|
+
3. **mémoire** — aucune source → clé **éphémère**, propre au process (`JwtKeystore.ts#JwtKeystore.#load`). En
|
|
293
299
|
développement : un WARNING au premier jeton. En production, un process qui sert **refuse de
|
|
294
300
|
démarrer** (§ suivant).
|
|
295
301
|
|
|
296
|
-
Le JWKS servi par `getPublicJWKS()` est **public** — `JwtKeystore.ts
|
|
302
|
+
Le JWKS servi par `getPublicJWKS()` est **public** — `JwtKeystore.ts#JwtKeystore.getPublicJWKS`.
|
|
297
303
|
La composante privée `d` en est retirée à l'import, par liste BLANCHE de paramètres
|
|
298
|
-
(`#importKeyset()`, `JwtKeystore.ts
|
|
299
|
-
C'est ce JWKS qu'utilise le vérificateur local (`createLocalJWKSet`, `JwtAuthenticator.ts
|
|
300
|
-
jamais une clé venue du jeton. Le chargement est mémoïsé — `#ensureLoaded()` (`JwtKeystore.ts
|
|
304
|
+
(`#importKeyset()`, `JwtKeystore.ts#JwtKeystore.#importKeyset`, RFC 8037/7517).
|
|
305
|
+
C'est ce JWKS qu'utilise le vérificateur local (`createLocalJWKSet`, `JwtAuthenticator.ts#JwtAuthenticator.#ensureGetKey`),
|
|
306
|
+
jamais une clé venue du jeton. Le chargement est mémoïsé — `#ensureLoaded()` (`JwtKeystore.ts#JwtKeystore.#ensureLoaded`).
|
|
301
307
|
|
|
302
308
|
### Plusieurs pods ou workers — UNE clé pour tous
|
|
303
309
|
|
|
@@ -306,10 +312,10 @@ les jetons l'un de l'autre : derrière un répartiteur, l'utilisateur est décon
|
|
|
306
312
|
et tout redémarrage invalide les jetons en vol.
|
|
307
313
|
|
|
308
314
|
**En production, un process qui SERT sans source partagée refuse de démarrer**
|
|
309
|
-
(`#requireSharedSigningKey`, `tokenService.ts
|
|
315
|
+
(`#requireSharedSigningKey`, `tokenService.ts#TokenService.#requireSharedSigningKey`), avec le motif et les trois issues dans le
|
|
310
316
|
journal. Une commande de console (`orm:migrate`, `security:user:add`) n'est pas concernée : elle
|
|
311
317
|
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
|
|
318
|
+
arrête le cluster et sort en **78** (`ClusterManager.ts#ClusterManager.#onExit`), le code d'une faute de configuration.
|
|
313
319
|
|
|
314
320
|
Le déploiement, de bout en bout :
|
|
315
321
|
|
|
@@ -353,22 +359,22 @@ jwt: {
|
|
|
353
359
|
### Le contrat et l'enregistrement
|
|
354
360
|
|
|
355
361
|
Le `tokenStore` héberge **trois structures** : les records (refresh + PAT), la denylist `jti` et le
|
|
356
|
-
seuil `invalidBefore` par porteur (`ITokenStore.ts
|
|
357
|
-
fabrique — `registerTokenStore()` (`tokenStoreRegistry.ts
|
|
362
|
+
seuil `invalidBefore` par porteur (`ITokenStore.ts`). Les backends s'enregistrent par
|
|
363
|
+
fabrique — `registerTokenStore()` (`tokenStoreRegistry.ts#registerTokenStore`) : les adapters lourds importent
|
|
358
364
|
`import type { ITokenStore }` (effacé à la compilation), zéro couplage runtime.
|
|
359
365
|
|
|
360
|
-
Sa résolution au boot (`tokenService.ts
|
|
366
|
+
Sa résolution au boot (`tokenService.ts#TokenService.#build`) suit la doctrine `store:"auto"` du framework :
|
|
361
367
|
|
|
362
368
|
- `auto` (défaut) → suit l'infra database déclarée via `resolveAutoStore` — **borné aux backends
|
|
363
|
-
réellement enregistrés**, repli memory **annoncé** (`tokenService.ts
|
|
369
|
+
réellement enregistrés**, repli memory **annoncé** (`tokenService.ts#TokenService.#build`) ;
|
|
364
370
|
- store explicite **inconnu** → en prod, **boot avorté** (fail-loud) ; en dev, brique désactivée et
|
|
365
|
-
annoncée avec la liste `listTokenStores()` (`tokenService.ts
|
|
371
|
+
annoncée avec la liste `listTokenStores()` (`tokenService.ts#TokenService.#build`) — jamais de fallback
|
|
366
372
|
memory silencieux pour du durable ;
|
|
367
373
|
- store `memory` **en prod** → `WARNING` nommant l'impact : denylist/refresh/clés API per-pod et
|
|
368
|
-
volatils, révocation non partagée (`tokenService.ts
|
|
374
|
+
volatils, révocation non partagée (`tokenService.ts#TokenService.#build`).
|
|
369
375
|
|
|
370
376
|
La décision (configuré → résolu, raison) est publiée au kernel par `registerStoreResolution()`
|
|
371
|
-
(`tokenService.ts
|
|
377
|
+
(`tokenService.ts#TokenService.#build`) — visible dans Studio.
|
|
372
378
|
|
|
373
379
|
### Mise en situation — quel store pour quelle app ?
|
|
374
380
|
|
|
@@ -384,143 +390,143 @@ La décision (configuré → résolu, raison) est publiée au kernel par `regist
|
|
|
384
390
|
|
|
385
391
|
### `memory` — la référence 0 dépendance
|
|
386
392
|
|
|
387
|
-
- Builtin, enregistré à l'import du module via `registerTokenStore` (`tokenStoreRegistry.ts
|
|
393
|
+
- Builtin, enregistré à l'import du module via `registerTokenStore` (`tokenStoreRegistry.ts#registerTokenStore`).
|
|
388
394
|
- `listPage` : tri `createdAt` DESC + tiebreaker `id`, déterministe pour l'offset — parité SQL
|
|
389
|
-
(`MemoryTokenStore.ts
|
|
395
|
+
(`MemoryTokenStore.ts#MemoryTokenStore.listPage`).
|
|
390
396
|
- Denylist bornée : purge **amortie** tous les 256 ajouts — `#maybeSweep()`
|
|
391
|
-
(`MemoryTokenStore.ts
|
|
397
|
+
(`MemoryTokenStore.ts#MemoryTokenStore.#maybeSweep`) + expiration paresseuse à la lecture. Pas de minuterie, pas de fuite.
|
|
392
398
|
- `snapshot()`/`restore()` sérialisables — base d'une persistance fichier, index reconstruits
|
|
393
|
-
(`MemoryTokenStore.ts
|
|
399
|
+
(`MemoryTokenStore.ts#MemoryTokenStore.restore`).
|
|
394
400
|
- Volatil, par-process : dev/tests. Pilote le banc de contrat commun.
|
|
395
401
|
|
|
396
402
|
### `drizzle` — SQL, le défaut durable
|
|
397
403
|
|
|
398
|
-
- Enregistré par le module drizzle (`drizzle/nodefony/registerStores.ts
|
|
404
|
+
- Enregistré par le module drizzle (`drizzle/nodefony/registerStores.ts#registerDrizzleFrameworkStores`).
|
|
399
405
|
- Élu par `store:"auto"` dès qu'une infra database SQL est déclarée ; sinon sqlite local si drizzle
|
|
400
|
-
est chargé (`config.ts
|
|
406
|
+
est chargé (`config.ts#jwtSchema`).
|
|
401
407
|
- Pagination **offset + total** (helper `paginate()` d'orm-core) ; e2e sur PostgreSQL et MySQL réels.
|
|
402
408
|
|
|
403
409
|
### `mongoose` — MongoDB
|
|
404
410
|
|
|
405
|
-
- Enregistré par le module mongoose (`mongoose/nodefony/registerStores.ts
|
|
406
|
-
- Pagination **offset + total** via `listPage` (`MongooseTokenStore.ts
|
|
407
|
-
- Purge par `gc()` explicite sur `expiresAt` (`MongooseTokenStore.ts
|
|
411
|
+
- Enregistré par le module mongoose (`mongoose/nodefony/registerStores.ts#registerMongooseFrameworkStores`).
|
|
412
|
+
- Pagination **offset + total** via `listPage` (`MongooseTokenStore.ts#MongooseTokenStore.listPage`).
|
|
413
|
+
- Purge par `gc()` explicite sur `expiresAt` (`MongooseTokenStore.ts#MongooseTokenStore.#toRecord`).
|
|
408
414
|
|
|
409
415
|
### `redis` — cluster, TTL natif
|
|
410
416
|
|
|
411
|
-
- Enregistré par le module redis (`redis/nodefony/registerStores.ts
|
|
412
|
-
- TTL natif : `expire()` posé à l'écriture du record (`RedisTokenStore.ts
|
|
417
|
+
- Enregistré par le module redis (`redis/nodefony/registerStores.ts#registerRedisFrameworkStores`).
|
|
418
|
+
- TTL natif : `expire()` posé à l'écriture du record (`RedisTokenStore.ts#RedisClientLike.expire`) — l'expiration ne
|
|
413
419
|
dépend pas du gc.
|
|
414
420
|
- Listing par `SCAN` : curseur opaque `skip:scanCursor`, `decodeCursor()`
|
|
415
|
-
(`RedisTokenStore.ts
|
|
421
|
+
(`RedisTokenStore.ts#RedisTokenStore.listAll`) — sans ordre global ni total, capacité réduite **assumée**.
|
|
416
422
|
- `countTokens()` renvoie `-1` : un comptage exact exigerait un SCAN complet O(N), refusé
|
|
417
|
-
(`RedisTokenStore.ts
|
|
423
|
+
(`RedisTokenStore.ts#RedisTokenStore.countTokens`).
|
|
418
424
|
|
|
419
425
|
### Le record — une seule table pour PAT et refresh
|
|
420
426
|
|
|
421
|
-
`IAccessTokenRecord` (`ITokenStore.ts
|
|
422
|
-
refresh, champs non pertinents à `null`, horodatages epoch ms (`ITokenStore.ts
|
|
427
|
+
`IAccessTokenRecord` (`ITokenStore.ts#IAccessTokenRecord`) est **single-table** : un même schéma porte PAT et
|
|
428
|
+
refresh, champs non pertinents à `null`, horodatages epoch ms (`ITokenStore.ts#IAccessTokenRecord`). Deux
|
|
423
429
|
constats de design :
|
|
424
430
|
|
|
425
431
|
- `subjectId` est une **référence logique souple, PAS une FK SQL** : porteur polymorphe
|
|
426
432
|
(user/service), store multi-backend, découplage des modules — intégrité assurée à l'usage
|
|
427
|
-
(`ITokenStore.ts
|
|
433
|
+
(`ITokenStore.ts#IAccessTokenRecord.subjectId`) ;
|
|
428
434
|
- slots prêts sans migration : `resources` (permissions fine-grained façon GitHub) et `cnf`
|
|
429
|
-
(sender-constrained DPoP/mTLS) (`ITokenStore.ts
|
|
435
|
+
(sender-constrained DPoP/mTLS) (`ITokenStore.ts#IAccessTokenRecord.cnf`).
|
|
430
436
|
|
|
431
437
|
Les colonnes par dialecte vivent dans la doc de chaque adapter (règle anti-triple-vérité).
|
|
432
438
|
|
|
433
439
|
### Lister pour l'admin — pagination native
|
|
434
440
|
|
|
435
|
-
- `listPage()` ne matérialise **jamais** plus d'une page (`ITokenStore.ts
|
|
441
|
+
- `listPage()` ne matérialise **jamais** plus d'une page (`ITokenStore.ts#ITokenStore.listPage`) — capacité par
|
|
436
442
|
backend : **offset + total** (SQL/Mongo/mémoire) ou **curseur** (`nextCursor`, Redis).
|
|
437
|
-
- `countTokens()` donne le `total` ; Redis répond `-1` (`ITokenStore.ts
|
|
438
|
-
- Filtres portables `ITokenListQuery` : `subjectId`, `kind`, `revoked` (`ITokenStore.ts
|
|
439
|
-
prédicat partagé `matchesTokenQuery()` (`MemoryTokenStore.ts
|
|
443
|
+
- `countTokens()` donne le `total` ; Redis répond `-1` (`ITokenStore.ts#ITokenStore.countTokens`).
|
|
444
|
+
- Filtres portables `ITokenListQuery` : `subjectId`, `kind`, `revoked` (`ITokenStore.ts#ITokenListQuery`) —
|
|
445
|
+
prédicat partagé `matchesTokenQuery()` (`MemoryTokenStore.ts#matchesTokenQuery`).
|
|
440
446
|
- `listAll()` reste réservé au **dump d'incident** cross-porteur, cold-path admin
|
|
441
|
-
(`ITokenStore.ts
|
|
447
|
+
(`ITokenStore.ts#ITokenStore.listAll`).
|
|
442
448
|
- Consommateur type : le data plane des clés API — `ApiKeyService.listPagePat()`
|
|
443
|
-
(`apiKeys.ts
|
|
449
|
+
(`apiKeys.ts#ApiKeyService.listPagePat`), jamais un listAll matérialisé.
|
|
444
450
|
|
|
445
451
|
### Révoquer — trois portées
|
|
446
452
|
|
|
447
|
-
- **Un access** avant son `exp` : denylist `denyJti()`/`isJtiDenied()` (`ITokenStore.ts
|
|
453
|
+
- **Un access** avant son `exp` : denylist `denyJti()`/`isJtiDenied()` (`ITokenStore.ts#ITokenStore.isJtiDenied`).
|
|
448
454
|
- **Un refresh/PAT** : `revoke()` idempotent, `revokeFamily()` pour la chaîne de rotation
|
|
449
|
-
(`ITokenStore.ts
|
|
455
|
+
(`ITokenStore.ts#ITokenStore.revokeFamily`).
|
|
450
456
|
- **Tout un porteur** (logout global, ban) : seuil `revokeAllForSubject()` — tout access dont
|
|
451
|
-
`iat < invalidBefore` est rejeté (`ITokenStore.ts
|
|
452
|
-
logouts successifs ne le reculent pas (`MemoryTokenStore.ts
|
|
457
|
+
`iat < invalidBefore` est rejeté (`ITokenStore.ts#ITokenStore`) ; le seuil est **monotone**, deux
|
|
458
|
+
logouts successifs ne le reculent pas (`MemoryTokenStore.ts#MemoryTokenStore.revokeAllForSubject`).
|
|
453
459
|
|
|
454
460
|
### La maintenance (gc)
|
|
455
461
|
|
|
456
462
|
Le `gc()` du store purge la `denylist` expirée, les records à terme, les PAT révoqués au-delà de
|
|
457
|
-
la rétention (`ITokenStore.ts
|
|
458
|
-
un futur worker cron — poser alors `gcIntervalS: 0` (`tokenService.ts
|
|
463
|
+
la rétention (`ITokenStore.ts#ITokenStore.gc`). Orchestré par le `GcScheduler` du service ; `runGc()` reste public pour
|
|
464
|
+
un futur worker cron — poser alors `gcIntervalS: 0` (`tokenService.ts#TokenService.getPublicJWKS`).
|
|
459
465
|
|
|
460
466
|
> [!TIP]
|
|
461
467
|
> Un refresh révoqué **par rotation** n'est PAS purgé tout de suite : il est conservé jusqu'à son
|
|
462
468
|
> `expiresAt` — c'est la **fenêtre de détection de rejeu** — puis tombe au gc, `#isPurgeable()`
|
|
463
|
-
> (`MemoryTokenStore.ts
|
|
469
|
+
> (`MemoryTokenStore.ts#MemoryTokenStore.#isPurgeable`). Un store **local** (memory) est par-process : seul SON process
|
|
464
470
|
> peut le purger → ne déléguez le gc au cron QUE pour un store partagé.
|
|
465
471
|
|
|
466
472
|
## ⚙️ Configuration
|
|
467
473
|
|
|
468
|
-
Tables dérivées du schéma Zod — `jwtSchema` (`config.ts
|
|
469
|
-
(`config.ts
|
|
474
|
+
Tables dérivées du schéma Zod — `jwtSchema` (`config.ts#jwtSchema`) et `tokenStoreSchema`
|
|
475
|
+
(`config.ts#tokenStoreSchema`), défauts inclus.
|
|
470
476
|
|
|
471
477
|
### `jwt.*`
|
|
472
478
|
|
|
473
479
|
<!-- prettier-ignore -->
|
|
474
480
|
| Option | Type | Défaut | Effet |
|
|
475
481
|
| --- | --- | --- | --- |
|
|
476
|
-
| `enabled` | boolean | `true` | Active signature + refresh (`config.ts
|
|
477
|
-
| `alg` | `EdDSA` \| `RS256` | `EdDSA` | `RS256` = slot non câblé (`jwtRuntime.ts
|
|
478
|
-
| `accessTtlS` | number (s) | `900` | TTL de l'access token — 15 min (`config.ts
|
|
479
|
-
| `refreshTtlS` | number (s) | `604800` | TTL du refresh — 7 jours (`config.ts
|
|
480
|
-
| `rotateRefresh` | boolean | `true` | Rotation du refresh à chaque usage, OWASP (`config.ts
|
|
482
|
+
| `enabled` | boolean | `true` | Active signature + refresh (`config.ts#rateLimitSchema`) |
|
|
483
|
+
| `alg` | `EdDSA` \| `RS256` | `EdDSA` | `RS256` = slot non câblé (`jwtRuntime.ts#IJwtRuntime.alg`) |
|
|
484
|
+
| `accessTtlS` | number (s) | `900` | TTL de l'access token — 15 min (`config.ts#jwtSchema`) |
|
|
485
|
+
| `refreshTtlS` | number (s) | `604800` | TTL du refresh — 7 jours (`config.ts#jwtSchema`) |
|
|
486
|
+
| `rotateRefresh` | boolean | `true` | Rotation du refresh à chaque usage, OWASP (`config.ts#jwtSchema`) |
|
|
481
487
|
| `jwks` | boolean | `true` | Publie `/.well-known/jwks.json` + les métadonnées RFC 8414 — sans `issuer` en URL https, rien n'est publié |
|
|
482
|
-
| `audiences` | string[] | `[]` | `aud` acceptées (RFC 8707) ; vide = `[issuer]` (`config.ts
|
|
488
|
+
| `audiences` | string[] | `[]` | `aud` acceptées (RFC 8707) ; vide = `[issuer]` (`config.ts#jwtSchema`) |
|
|
483
489
|
| `issuer` | string? | — | Claim `iss`, **STABLE** après émission ; omis → repli `"nodefony"`, qui n'est PAS publiable (RFC 8414 §2 exige une URL https) |
|
|
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
|
|
485
|
-
| `keystore.dir` | string? | — | Dossier `keyset.json` chmod 600, création exclusive — dev, ou serveur unique à disque persistant (`config.ts
|
|
490
|
+
| `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#jwtSchema`) |
|
|
491
|
+
| `keystore.dir` | string? | — | Dossier `keyset.json` chmod 600, création exclusive — dev, ou serveur unique à disque persistant (`config.ts#jwtSchema`) |
|
|
486
492
|
|
|
487
493
|
### `tokenStore.*`
|
|
488
494
|
|
|
489
|
-
| Option | Type | Défaut | Effet
|
|
490
|
-
| ---------------------- | ---------- | -------- |
|
|
491
|
-
| `store` | string | `"auto"` | `auto`\|`memory`\|`drizzle`\|`mongoose`\|`redis` — pluggable (`config.ts
|
|
492
|
-
| `gcIntervalS` | number (s) | `600` | Purge périodique ; `0` = désactivé — chaque process purge SON store (`config.ts
|
|
493
|
-
| `gcJitter` | boolean | `true` | Étale le gc d'un délai aléatoire par process — cluster (`config.ts
|
|
494
|
-
| `retentionRevokedDays` | number (j) | `30` | Rétention d'un PAT révoqué SANS expiration avant purge (`config.ts
|
|
495
|
+
| Option | Type | Défaut | Effet |
|
|
496
|
+
| ---------------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------- |
|
|
497
|
+
| `store` | string | `"auto"` | `auto`\|`memory`\|`drizzle`\|`mongoose`\|`redis` — pluggable (`config.ts#jwtSchema`) |
|
|
498
|
+
| `gcIntervalS` | number (s) | `600` | Purge périodique ; `0` = désactivé — chaque process purge SON store (`config.ts#tokenStoreSchema`) |
|
|
499
|
+
| `gcJitter` | boolean | `true` | Étale le gc d'un délai aléatoire par process — cluster (`config.ts#tokenStoreSchema`) |
|
|
500
|
+
| `retentionRevokedDays` | number (j) | `30` | Rétention d'un PAT révoqué SANS expiration avant purge (`config.ts#tokenStoreSchema`) |
|
|
495
501
|
|
|
496
502
|
## 📜 Normes appliquées
|
|
497
503
|
|
|
498
|
-
| Domaine | Norme | Ancrage
|
|
499
|
-
| -------------------------------- | --------------- |
|
|
500
|
-
| Réponse d'émission | RFC 6749 §5.1 | `ITokenResponse` (`tokenService.ts
|
|
501
|
-
| Rotation + détection de rejeu | RFC 9700 §4.14 | `refresh()` (`tokenService.ts
|
|
502
|
-
| Profil access token `typ:at+jwt` | RFC 9068 | `#signAccess()` (`tokenService.ts
|
|
503
|
-
| Claims JWT (`iss/sub/aud/exp`) | RFC 7519 | `#signAccess()` (`tokenService.ts
|
|
504
|
-
| Ed25519 / JWK / JWKS public | RFC 8037 · 7517 | `#importKeyset()` (`JwtKeystore.ts
|
|
505
|
-
| Audiences liées à la ressource | RFC 8707 | `audience` du record (`ITokenStore.ts
|
|
506
|
-
| 429 + `Retry-After` | RFC 6585 | `#renderAuthError()` (`TokenAuthController.ts
|
|
507
|
-
| Backoff de login | NIST SP 800-63B | `ThrottledError` avant hachage (`tokenService.ts
|
|
504
|
+
| Domaine | Norme | Ancrage |
|
|
505
|
+
| -------------------------------- | --------------- | ------------------------------------------------------------------------------------ |
|
|
506
|
+
| Réponse d'émission | RFC 6749 §5.1 | `ITokenResponse` (`tokenService.ts#ITokenResponse`) |
|
|
507
|
+
| Rotation + détection de rejeu | RFC 9700 §4.14 | `refresh()` (`tokenService.ts#TokenService.refresh`) |
|
|
508
|
+
| Profil access token `typ:at+jwt` | RFC 9068 | `#signAccess()` (`tokenService.ts#TokenService.#signAccess`) |
|
|
509
|
+
| Claims JWT (`iss/sub/aud/exp`) | RFC 7519 | `#signAccess()` (`tokenService.ts#TokenService.#signAccess`) |
|
|
510
|
+
| Ed25519 / JWK / JWKS public | RFC 8037 · 7517 | `#importKeyset()` (`JwtKeystore.ts#JwtKeystore.#importKeyset`) |
|
|
511
|
+
| Audiences liées à la ressource | RFC 8707 | `audience` du record (`ITokenStore.ts#IAccessTokenRecord.audience`) |
|
|
512
|
+
| 429 + `Retry-After` | RFC 6585 | `#renderAuthError()` (`TokenAuthController.ts#TokenAuthController.#renderAuthError`) |
|
|
513
|
+
| Backoff de login | NIST SP 800-63B | `ThrottledError` avant hachage (`tokenService.ts#TokenService.issueForCredentials`) |
|
|
508
514
|
|
|
509
515
|
## ⚡ Performance & mémoire
|
|
510
516
|
|
|
511
|
-
- **`jose` importé lazy** (dep lourde) : `#ensureJose()` au premier usage (`tokenService.ts
|
|
517
|
+
- **`jose` importé lazy** (dep lourde) : `#ensureJose()` au premier usage (`tokenService.ts#TokenService.#ensureJose`)
|
|
512
518
|
— le boot ne paie rien si le JWT n'est jamais sollicité ; keystore mémoïsé pareil.
|
|
513
519
|
- **Rien sur le hot path requête** : émission et rotation sont des endpoints cold-path ; la
|
|
514
520
|
vérification (hot path) vit chez le `JwtAuthenticator`.
|
|
515
521
|
- **Timers civilisés** : `GcScheduler` `unref` (n'empêche pas l'arrêt) + jitter anti-balayages
|
|
516
|
-
simultanés (`tokenService.ts
|
|
517
|
-
(`MemoryTokenStore.ts
|
|
518
|
-
- **Jamais N en RAM** : `listPage()` borne toute lecture admin à une page (`ITokenStore.ts
|
|
522
|
+
simultanés (`tokenService.ts#TokenService.#build`) ; denylist mémoire bornée par purge amortie
|
|
523
|
+
(`MemoryTokenStore.ts#MemoryTokenStore.restore`).
|
|
524
|
+
- **Jamais N en RAM** : `listPage()` borne toute lecture admin à une page (`ITokenStore.ts#ITokenStore.listPage`).
|
|
519
525
|
|
|
520
526
|
## 📡 Observabilité — Studio
|
|
521
527
|
|
|
522
528
|
- **Écran Stores** (`/nodefony/stores`) : la résolution du store de jetons (configuré → résolu +
|
|
523
|
-
raison) publiée par `registerStoreResolution()` (`tokenService.ts
|
|
529
|
+
raison) publiée par `registerStoreResolution()` (`tokenService.ts#TokenService.#build`).
|
|
524
530
|
- **Écran Audit** (`/nodefony/audit`) : événements `token.issued`, `token.reuse_detected` (signal d'attaque),
|
|
525
531
|
`login.failure`/`login.throttled` du grant — corrélables par `tokenId`.
|
|
526
532
|
- **Écran ApiKeys** : le même store côté PAT, listing paginé serveur.
|