@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/firewall.md
CHANGED
|
@@ -52,10 +52,10 @@ flowchart TD
|
|
|
52
52
|
AU -->|succès| OK["user + token dans l'ALS → contrôleur"]
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
`Firewall.isSecure()` (`firewall.ts
|
|
56
|
-
`Firewall.matchPath()` (`firewall.ts
|
|
55
|
+
`Firewall.isSecure()` (`firewall.ts#Firewall.isSecure`) rattache la requête à une **zone** via
|
|
56
|
+
`Firewall.matchPath()` (`firewall.ts#Firewall.matchPath`) ; `Firewall.handleSecurity()` (`firewall.ts#Firewall.handleSecurity`) décide.
|
|
57
57
|
Les zones sont triées par **spécificité** dans `#build()` — `list.sort` par longueur de motif :
|
|
58
|
-
le plus long gagne, pas le premier déclaré (`firewall.ts
|
|
58
|
+
le plus long gagne, pas le premier déclaré (`firewall.ts#Firewall.#build`).
|
|
59
59
|
|
|
60
60
|
## 📖 Lexique
|
|
61
61
|
|
|
@@ -74,31 +74,34 @@ le plus long gagne, pas le premier déclaré (`firewall.ts:258`).
|
|
|
74
74
|
### Dans une app `nodefony create app`, le firewall est DÉJÀ actif
|
|
75
75
|
|
|
76
76
|
Le scaffold déclare trois zones (`main`, `secure`, `machine`) dans `nodefony/config/security.ts`, importé par `nodefony.config.ts` ; en voici deux — c'est la forme canonique (un **objet par
|
|
77
|
-
nom**, validé Zod au boot : `areas: z.record(...)`, `config.ts
|
|
77
|
+
nom**, validé Zod au boot : `areas: z.record(...)`, `config.ts#securityConfigSchema`) :
|
|
78
78
|
|
|
79
79
|
```typescript
|
|
80
|
-
// nodefony/config/security.ts
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
// nodefony/config/security.ts
|
|
81
|
+
import type { ISecurityConfigInput } from "@nodefony/security";
|
|
82
|
+
|
|
83
|
+
export const securityConfig = () =>
|
|
84
|
+
({
|
|
85
|
+
areas: {
|
|
86
|
+
// Zone de TES routes : `session` PUIS `anonymous` → identifié si cookie,
|
|
87
|
+
// sinon visiteur accepté. Hors zone, l'identité n'est JAMAIS résolue.
|
|
88
|
+
main: {
|
|
89
|
+
pattern: "^/api",
|
|
90
|
+
authenticators: ["session", "anonymous"],
|
|
91
|
+
},
|
|
92
|
+
// Zone PROTÉGÉE — pattern PLUS SPÉCIFIQUE que ^/api : le firewall trie
|
|
93
|
+
// par longueur → /api/secure/* tombe ICI. Pas d'`anonymous` : sans
|
|
94
|
+
// session → 401 AVANT ton controller (Zero Trust).
|
|
95
|
+
secure: {
|
|
96
|
+
pattern: "^/api/secure",
|
|
97
|
+
authenticators: ["session"],
|
|
98
|
+
},
|
|
88
99
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
secure: {
|
|
93
|
-
pattern: "^/api/secure",
|
|
94
|
-
authenticators: ["session"],
|
|
100
|
+
roleHierarchy: {
|
|
101
|
+
ROLE_NODEFONY_ADMIN: ["ROLE_ADMIN", "ROLE_SUPERVISOR", "ROLE_DEV"],
|
|
102
|
+
ROLE_ADMIN: ["ROLE_USER"],
|
|
95
103
|
},
|
|
96
|
-
}
|
|
97
|
-
roleHierarchy: {
|
|
98
|
-
ROLE_NODEFONY_ADMIN: ["ROLE_ADMIN", "ROLE_SUPERVISOR", "ROLE_DEV"],
|
|
99
|
-
ROLE_ADMIN: ["ROLE_USER"],
|
|
100
|
-
},
|
|
101
|
-
});
|
|
104
|
+
}) satisfies ISecurityConfigInput;
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
> [!IMPORTANT]
|
|
@@ -236,9 +239,9 @@ Point commun de sécurité : **message d'échec uniforme** (`"Invalid token"` /
|
|
|
236
239
|
Credential = l'**identifiant** posé dans le blob de session (jamais un secret).
|
|
237
240
|
|
|
238
241
|
- **N'ouvre jamais la session lui-même** : il exige une session reprise portant un user
|
|
239
|
-
(`supports()`, `SessionAuthenticator.ts
|
|
242
|
+
(`supports()`, `SessionAuthenticator.ts#SessionAuthenticator.supports`). C'est `AuthFlow.login()` (BFF) qui ouvre et
|
|
240
243
|
régénère l'ID (anti-fixation).
|
|
241
|
-
- **L'identité est re-résolue à CHAQUE requête** (`SessionAuthenticator.ts
|
|
244
|
+
- **L'identité est re-résolue à CHAQUE requête** (`SessionAuthenticator.ts#SessionAuthenticator.authenticate`) → rôles frais,
|
|
242
245
|
révocation et verrouillage effectifs immédiatement.
|
|
243
246
|
- **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login
|
|
244
247
|
(pas de popup Basic).
|
|
@@ -246,11 +249,11 @@ Credential = l'**identifiant** posé dans le blob de session (jamais un secret).
|
|
|
246
249
|
### `userpassword` — HTTP Basic, avec throttle NIST
|
|
247
250
|
|
|
248
251
|
Credential = `Authorization: Basic base64(identifiant:motdepasse)` — RFC 7617, split au **premier**
|
|
249
|
-
`:` (`UserPasswordAuthenticator.ts
|
|
252
|
+
`:` (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.createToken`).
|
|
250
253
|
|
|
251
254
|
- **La vérification est déléguée** au `IPasswordVerifier` (le `UserService`) : hash, comparaison,
|
|
252
255
|
leurre anti-timing, re-hash. L'authenticator ne voit que le verdict.
|
|
253
|
-
- **Le throttle NIST SP 800-63B passe AVANT le verifier** (`UserPasswordAuthenticator.ts
|
|
256
|
+
- **Le throttle NIST SP 800-63B passe AVANT le verifier** (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`) :
|
|
254
257
|
un identifiant bloqué ne coûte **aucun hash argon2** — protège d'un DoS par hachage.
|
|
255
258
|
Échec → backoff ; `ThrottledError` → **429 + `Retry-After`**.
|
|
256
259
|
- Challenge : `Basic realm="nodefony"`.
|
|
@@ -259,23 +262,23 @@ Credential = `Authorization: Basic base64(identifiant:motdepasse)` — RFC 7617,
|
|
|
259
262
|
|
|
260
263
|
### `jwt` — Bearer JWT signé, durci RFC 8725
|
|
261
264
|
|
|
262
|
-
Credential = `Authorization: Bearer <jws>` de structure compacte `a.b.c` (`JwtAuthenticator.ts
|
|
265
|
+
Credential = `Authorization: Bearer <jws>` de structure compacte `a.b.c` (`JwtAuthenticator.ts#COMPACT_JWS`).
|
|
263
266
|
Réservé API service↔service / agents (le web reste sur la session). Access token **EdDSA** signé par
|
|
264
267
|
le keystore du serveur. Défenses **dures**, prouvées en test (RFC 8725 JWT BCP), toutes dans
|
|
265
268
|
`JwtAuthenticator.authenticate()` :
|
|
266
269
|
|
|
267
270
|
- **allowlist d'algorithmes** `["EdDSA"]` — l'algo n'est **jamais** choisi d'après l'en-tête du
|
|
268
|
-
token ; `alg=none` rejeté (`JwtAuthenticator.ts
|
|
271
|
+
token ; `alg=none` rejeté (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
|
|
269
272
|
- **clé par `kid` depuis le JWKS LOCAL** (`createLocalJWKSet`) — jamais `jku`/`jwk` de l'en-tête
|
|
270
|
-
(anti-injection de clé / SSRF, `JwtAuthenticator.ts
|
|
273
|
+
(anti-injection de clé / SSRF, `JwtAuthenticator.ts#JwtAuthenticator.#ensureGetKey`).
|
|
271
274
|
- **`aud` + `iss` obligatoires** + `typ:"at+jwt"` (un refresh présenté comme access est rejeté) +
|
|
272
|
-
exp/nbf (`JwtAuthenticator.ts
|
|
275
|
+
exp/nbf (`JwtAuthenticator.ts#JwtAuthenticator.verifyForResource`).
|
|
273
276
|
- **révocation** malgré l'auto-portage : denylist `jti` + `invalidBefore` par sujet
|
|
274
|
-
(`JwtAuthenticator.ts
|
|
277
|
+
(`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
|
|
275
278
|
- **sujet revérifié** à réception (`loadUserByIdentifier(sub)`) : compte disparu/inactif/verrouillé
|
|
276
|
-
= rejet (`JwtAuthenticator.ts
|
|
279
|
+
= rejet (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
|
|
277
280
|
|
|
278
|
-
Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts
|
|
281
|
+
Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts#JwtAuthenticator.#verify`).
|
|
279
282
|
|
|
280
283
|
> [!WARNING]
|
|
281
284
|
> Un JWT est **auto-porté** : sans état serveur il n'est **pas** révocable. C'est la denylist
|
|
@@ -284,26 +287,26 @@ Le token promu porte `scopes`, `jti`, `claims` (`JwtAuthenticator.ts:178-188`).
|
|
|
284
287
|
|
|
285
288
|
### `apikey` — clé d'API opaque (PAT), révocable
|
|
286
289
|
|
|
287
|
-
Credential = `Authorization: Bearer <prefix>_…` (`ApiKeyAuthenticator.ts
|
|
290
|
+
Credential = `Authorization: Bearer <prefix>_…` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.supports`). Contrairement au JWT,
|
|
288
291
|
c'est un **bearer opaque** : sa vérité vit côté serveur (`ITokenStore`) → **révocable immédiatement**.
|
|
289
292
|
|
|
290
293
|
Défenses de `ApiKeyAuthenticator.authenticate()` :
|
|
291
294
|
|
|
292
295
|
- **forme + CRC validés AVANT tout accès au store** — anti-DoS (`parseApiKey()`,
|
|
293
|
-
`ApiKeyAuthenticator.ts
|
|
294
|
-
- lookup par **hash sha256** : le secret n'existe nulle part au repos (`ApiKeyAuthenticator.ts
|
|
296
|
+
`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
|
|
297
|
+
- lookup par **hash sha256** : le secret n'existe nulle part au repos (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
|
|
295
298
|
- révocation (`revokedAt`), expiration (`expiresAt`), **ban en masse** du porteur
|
|
296
|
-
(`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts
|
|
297
|
-
- **sujet revérifié** à chaque requête — rôles frais (`ApiKeyAuthenticator.ts
|
|
298
|
-
- `lastUsedAt` écrit en **throttlé** — pas une écriture par requête (`ApiKeyAuthenticator.ts
|
|
299
|
+
(`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
|
|
300
|
+
- **sujet revérifié** à chaque requête — rôles frais (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) ;
|
|
301
|
+
- `lastUsedAt` écrit en **throttlé** — pas une écriture par requête (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
|
|
299
302
|
|
|
300
|
-
Le token porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts
|
|
303
|
+
Le token porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`). `jwt` et `apikey` **cohabitent** dans
|
|
301
304
|
une zone : ils se discriminent par la forme (JWT = `a.b.c`, PAT = `prefix_…`).
|
|
302
305
|
|
|
303
306
|
### `anonymous` — accepter l'anonymat, explicitement
|
|
304
307
|
|
|
305
308
|
Le **seul** authenticator qui produit un token non authentifié **sans** déclencher le Zero Trust
|
|
306
|
-
(`AnonymousAuthenticator.ts
|
|
309
|
+
(`AnonymousAuthenticator.ts#AnonymousAuthenticator`). À lister **volontairement** : `["jwt", "anonymous"]` en mode
|
|
307
310
|
`first` = « identifié si preuve présente, sinon **visiteur anonyme accepté** ». En mode `all`, utile
|
|
308
311
|
en dernier : « le canal doit être prouvé (ex. mTLS), l'identité utilisateur est optionnelle ». Coût
|
|
309
312
|
nul : `supports()` accepte tout, le token porte le singleton gelé `anonymousUser` (0 allocation).
|
|
@@ -313,14 +316,14 @@ Sans lui, zone protégée + aucune preuve = 401.
|
|
|
313
316
|
|
|
314
317
|
Il promeut en jeton realtime **toute** identité que le firewall a résolue — session BFF comme jeton
|
|
315
318
|
porteur (JWT, clé d'API). **Enregistré automatiquement** par `Firewall.#wireRealtime()` au
|
|
316
|
-
handshake des zones protégées `realtime` (`firewall.ts
|
|
319
|
+
handshake des zones protégées `realtime` (`firewall.ts#Firewall.#wireRealtime`).
|
|
317
320
|
|
|
318
321
|
- **Perf : il ne relit pas la base.** Handshake et frames tournent dans la même bulle ALS —
|
|
319
322
|
l'identité déjà posée est réutilisée, 2 lectures base économisées par connexion
|
|
320
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
323
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`).
|
|
321
324
|
- **Asymétrie HTTP↔WS assumée** : le jeton est **figé au handshake** (les frames lisent un cache
|
|
322
325
|
O(1)) → une révocation prend effet **en une fenêtre** (tick du hub, et devant chaque
|
|
323
|
-
`api.request`), pas à la frame suivante (`FirewallRealtimeAuthenticator.ts
|
|
326
|
+
`api.request`), pas à la frame suivante (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`). C'est l'état
|
|
324
327
|
de l'art (Socket.IO et Phoenix figent aussi).
|
|
325
328
|
- **Filet** : un revalidator re-lit la session avant chaque action data plane ; fail-closed →
|
|
326
329
|
fermeture 4001.
|
|
@@ -353,7 +356,7 @@ laisser chercher pourquoi `context.session` est nul.
|
|
|
353
356
|
## ⚙️ Ordre et modes (`mode: "first"` vs `"all"`)
|
|
354
357
|
|
|
355
358
|
La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
|
|
356
|
-
(`firewall.ts
|
|
359
|
+
(`firewall.ts#Firewall.#authenticate`). Le `mode` dit comment la parcourir. Trois situations concrètes :
|
|
357
360
|
|
|
358
361
|
### Situation 1 — humains ET machines sur la même API (`first`, le mode courant)
|
|
359
362
|
|
|
@@ -375,7 +378,7 @@ Ce qui se passe, requête par requête :
|
|
|
375
378
|
| --- | --- | --- |
|
|
376
379
|
| le cookie de session | `session` | identifié, `apikey` jamais consulté |
|
|
377
380
|
| `Authorization: Bearer nf_…` | `apikey` | identifié (session ne matche pas, on passe) |
|
|
378
|
-
| une clé **révoquée** `nf_…` | `apikey` | **401 direct** — l'échec d'`authenticate()` remonte, pas de fallback (`firewall.ts
|
|
381
|
+
| une clé **révoquée** `nf_…` | `apikey` | **401 direct** — l'échec d'`authenticate()` remonte, pas de fallback (`firewall.ts#Firewall.#authenticate`) |
|
|
379
382
|
| rien | aucun | **401** (Zero Trust) |
|
|
380
383
|
|
|
381
384
|
### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
|
|
@@ -406,11 +409,11 @@ danger: {
|
|
|
406
409
|
|
|
407
410
|
Le client doit présenter **les deux preuves** dans la même requête (cookie + `Authorization:
|
|
408
411
|
Basic …`). Une seule manque → 401. Le **dernier** token de la chaîne porte l'identité
|
|
409
|
-
(`firewall.ts
|
|
412
|
+
(`firewall.ts#Firewall.#authenticate`) — ici la preuve mot de passe, la plus fraîche.
|
|
410
413
|
|
|
411
414
|
> [!TIP]
|
|
412
415
|
> Un nom d'authenticator inconnu en config **fait échouer le boot** —
|
|
413
|
-
> `Firewall.#instantiateAuthenticators()` est fail-closed (`firewall.ts
|
|
416
|
+
> `Firewall.#instantiateAuthenticators()` est fail-closed (`firewall.ts#Firewall.#instantiateAuthenticators`) : jamais de zone
|
|
414
417
|
> « protégée » silencieusement ouverte à cause d'une faute de frappe.
|
|
415
418
|
|
|
416
419
|
## 🧑⚖️ Autorisation — rôles, scopes, voters (« as-tu le droit ? »)
|
|
@@ -432,30 +435,30 @@ du contrôleur** (seam Resolver) — une action protégée ne s'exécute jamais
|
|
|
432
435
|
|
|
433
436
|
### Le jury et sa stratégie
|
|
434
437
|
|
|
435
|
-
`Authorization.decide(token, attribut, subject?)` (`service/authorization.ts
|
|
438
|
+
`Authorization.decide(token, attribut, subject?)` (`service/authorization.ts#Authorization.decide`) applique une
|
|
436
439
|
stratégie **affirmative + DENY veto**, fermée par défaut (**Zero Trust**) :
|
|
437
440
|
|
|
438
441
|
- un seul **`DENY`** bloque (veto, court-circuit — inutile de finir le jury,
|
|
439
|
-
`authorization.ts
|
|
442
|
+
`authorization.ts#Authorization.decide`) ;
|
|
440
443
|
- sinon un **`GRANT`** suffit ;
|
|
441
444
|
- **silence total** (tous `ABSTAIN`, ou aucun voter compétent) → **`DENY`**
|
|
442
|
-
(`authorization.ts
|
|
445
|
+
(`authorization.ts#Authorization`) ;
|
|
443
446
|
- un voter qui **throw** → **`DENY`** + log ERROR (fail-closed : jamais 500, jamais octroi,
|
|
444
|
-
`authorization.ts
|
|
447
|
+
`authorization.ts#Authorization.decide`).
|
|
445
448
|
|
|
446
|
-
Tout refus est audité (WARNING + `recordAudit`, `authorization.ts
|
|
449
|
+
Tout refus est audité (WARNING + `recordAudit`, `authorization.ts#Authorization`) ; les octrois restent
|
|
447
450
|
muets (volume, pas un signal). Les voters sont instanciés **une fois au boot** via le registre
|
|
448
|
-
(aucun nom en dur, `authorization.ts
|
|
451
|
+
(aucun nom en dur, `authorization.ts#Authorization`).
|
|
449
452
|
|
|
450
453
|
### Les voters intégrés — deux axes
|
|
451
454
|
|
|
452
455
|
- **RoleVoter** (`role`, attributs `ROLE_*`) — `GRANT` si l'utilisateur a le rôle, **hiérarchie
|
|
453
|
-
résolue** ; **`ABSTAIN` sinon**, jamais `DENY` (`RoleVoter.vote()`, `RoleVoter.ts
|
|
456
|
+
résolue** ; **`ABSTAIN` sinon**, jamais `DENY` (`RoleVoter.vote()`, `RoleVoter.ts#RoleVoter.vote`).
|
|
454
457
|
Constat : l'absence d'un rôle ne doit pas opposer son veto aux autres axes — c'est le
|
|
455
458
|
**default-DENY du jury** qui ferme la porte, pas ce voter. C'est ce qui rend une clause OR
|
|
456
459
|
(`@IsGranted(["A","B"])`) possible.
|
|
457
460
|
- **ScopeVoter** (`scope`, attributs `api:action`) — un scope **ne bride jamais un humain**
|
|
458
|
-
(`ScopeVoter.ts
|
|
461
|
+
(`ScopeVoter.ts`) :
|
|
459
462
|
- jeton humain (`session`/`userpassword`/`anonymous`) → `GRANT` no-op : l'autorisation d'un
|
|
460
463
|
humain passe par ses **rôles** ;
|
|
461
464
|
- jeton **machine délégué** (`apikey`/`jwt`/`oauth2`) → `GRANT` si le scope exact est présent,
|
|
@@ -519,8 +522,8 @@ destinataire.
|
|
|
519
522
|
|
|
520
523
|
`RoleHierarchyWalker` (`src/RoleHierarchyWalker.ts`) : `ROLE_ADMIN` hérite `ROLE_USER`, etc.
|
|
521
524
|
**Aplatissement précalculé au boot** → `hasRole()` est O(1) sur le hot path
|
|
522
|
-
(`RoleHierarchyWalker.ts
|
|
523
|
-
complet, pas de fail-silent, `RoleHierarchyWalker.ts
|
|
525
|
+
(`RoleHierarchyWalker.ts#RoleHierarchyWalker.hasRole`), et les **cycles sont détectés au boot** (throw avec le chemin
|
|
526
|
+
complet, pas de fail-silent, `RoleHierarchyWalker.ts#RoleHierarchyWalker.#detectCycles`). La hiérarchie est posée au container
|
|
524
527
|
par le firewall au boot ; le `RoleVoter` la lit en lazy.
|
|
525
528
|
|
|
526
529
|
### Voters métier (le vrai pouvoir applicatif)
|
|
@@ -539,41 +542,41 @@ registerVoterFactory(
|
|
|
539
542
|
```
|
|
540
543
|
|
|
541
544
|
Le voter est **découvert automatiquement** par le service `authorization` — aucun changement dans le
|
|
542
|
-
cœur (`registerVoterFactory()`, `voterRegistry.ts
|
|
545
|
+
cœur (`registerVoterFactory()`, `voterRegistry.ts#registerVoterFactory`). Pourquoi un registre et pas un scan DI des
|
|
543
546
|
`@injectable` : les interfaces TS sont **effacées à la compilation** — rien à scanner ; le registre
|
|
544
|
-
**est** le marqueur explicite (TSDoc du registre, `voterRegistry.ts
|
|
547
|
+
**est** le marqueur explicite (TSDoc du registre, `voterRegistry.ts#IVoterFactoryContext`). Trois axes (rôles,
|
|
545
548
|
scopes, métier), un même jury, combinables.
|
|
546
549
|
|
|
547
550
|
## 🔌 HTTP et WebSocket — le même firewall
|
|
548
551
|
|
|
549
|
-
`Firewall.#wireRealtime()` (`firewall.ts
|
|
550
|
-
(opt-out, `firewall.ts
|
|
551
|
-
**et** un `frameAuthorizer` (RBAC par canal, `firewall.ts
|
|
552
|
+
`Firewall.#wireRealtime()` (`firewall.ts#Firewall.#wireRealtime`) câble, pour toute zone protégée `realtime !== false`
|
|
553
|
+
(opt-out, `firewall.ts#Firewall.#build`), le `FirewallRealtimeAuthenticator` au handshake (`firewall.ts#Firewall.#wireRealtime`)
|
|
554
|
+
**et** un `frameAuthorizer` (RBAC par canal, `firewall.ts#Firewall.#wireRealtime`). Même résolution de zone que HTTP.
|
|
552
555
|
Sur une socket, un refus n'a pas d'en-tête `WWW-Authenticate` (`Firewall.#setChallenge()`,
|
|
553
|
-
`firewall.ts
|
|
556
|
+
`firewall.ts#Firewall.#setChallenge`) : le **code de fermeture** suffit.
|
|
554
557
|
|
|
555
558
|
## 🛡️ En-têtes de sécurité, CSRF, CORS
|
|
556
559
|
|
|
557
|
-
- **`Firewall.applySecurityHeaders()`** (`firewall.ts
|
|
558
|
-
au-dessus du socle transport de `@nodefony/http`. **Nonce CSP paresseux** (`hasNonce`, `firewall.ts
|
|
560
|
+
- **`Firewall.applySecurityHeaders()`** (`firewall.ts#Firewall.applySecurityHeaders`) : CSP, Referrer-Policy, COOP/COEP/CORP
|
|
561
|
+
au-dessus du socle transport de `@nodefony/http`. **Nonce CSP paresseux** (`hasNonce`, `firewall.ts#Firewall.enforceCsrf`) :
|
|
559
562
|
alloué seulement si une directive en a besoin.
|
|
560
|
-
- **`Firewall.enforceCsrf()`** (défense en profondeur, `firewall.ts
|
|
561
|
-
(`Sec-Fetch-Site`) + garde `Origin` (`firewall.ts
|
|
562
|
-
cookie + HMAC (`firewall.ts
|
|
563
|
-
- **`Firewall.handleCors()`** : preflight `OPTIONS` → 204 (`firewall.ts
|
|
563
|
+
- **`Firewall.enforceCsrf()`** (défense en profondeur, `firewall.ts#Firewall.enforceCsrf`) : Fetch Metadata
|
|
564
|
+
(`Sec-Fetch-Site`) + garde `Origin` (`firewall.ts#Firewall.#startTrace`), puis double-submit `x-csrf-token` ≡
|
|
565
|
+
cookie + HMAC (`firewall.ts#Firewall.describe`).
|
|
566
|
+
- **`Firewall.handleCors()`** : preflight `OPTIONS` → 204 (`firewall.ts#Firewall.handleCors`).
|
|
564
567
|
|
|
565
568
|
## 📜 Normes appliquées
|
|
566
569
|
|
|
567
|
-
| Domaine | Norme | Ancrage
|
|
568
|
-
| ---------------------- | --------------- |
|
|
569
|
-
| Challenge d'auth (401) | RFC 9110 §11 | `Firewall.#setChallenge()` (`firewall.ts
|
|
570
|
-
| Bearer | RFC 6750 | `JwtAuthenticator.ts
|
|
571
|
-
| JWT (BCP) | RFC 7519, 8725 | `JwtAuthenticator.ts
|
|
572
|
-
| HTTP Basic | RFC 7617 | `UserPasswordAuthenticator.ts
|
|
573
|
-
| Rate limit (429) | RFC 6585 | 429 + `Retry-After` (`firewall.ts
|
|
574
|
-
| Backoff de login | NIST SP 800-63B | `UserPasswordAuthenticator.ts
|
|
575
|
-
| CSRF | Fetch Metadata | `Firewall.enforceCsrf()` (`firewall.ts
|
|
576
|
-
| Modèle | Zero Trust | `firewall.ts
|
|
570
|
+
| Domaine | Norme | Ancrage |
|
|
571
|
+
| ---------------------- | --------------- | ----------------------------------------------------------------- |
|
|
572
|
+
| Challenge d'auth (401) | RFC 9110 §11 | `Firewall.#setChallenge()` (`firewall.ts#Firewall.#setChallenge`) |
|
|
573
|
+
| Bearer | RFC 6750 | `JwtAuthenticator.ts` · `ApiKeyAuthenticator.ts#INVALID_TOKEN` |
|
|
574
|
+
| JWT (BCP) | RFC 7519, 8725 | `JwtAuthenticator.ts#JwtAuthenticator,104-108` |
|
|
575
|
+
| HTTP Basic | RFC 7617 | `UserPasswordAuthenticator.ts` |
|
|
576
|
+
| Rate limit (429) | RFC 6585 | 429 + `Retry-After` (`firewall.ts#Firewall.handleSecurity`) |
|
|
577
|
+
| Backoff de login | NIST SP 800-63B | `UserPasswordAuthenticator.ts#UserPasswordAuthenticator,101-104` |
|
|
578
|
+
| CSRF | Fetch Metadata | `Firewall.enforceCsrf()` (`firewall.ts#Firewall.enforceCsrf`) |
|
|
579
|
+
| Modèle | Zero Trust | `firewall.ts#Firewall` (aucune preuve → 401) |
|
|
577
580
|
|
|
578
581
|
## ⚡ Performance & mémoire
|
|
579
582
|
|