@nodefony/security 10.0.0-alpha.1
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/LICENSE +544 -0
- package/README.md +182 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +151 -0
- package/dist/nodefony/command/security-secrets.js +158 -0
- package/dist/nodefony/command/security-token.js +335 -0
- package/dist/nodefony/command/security-user-add.js +131 -0
- package/dist/nodefony/command/security-user-delete.js +102 -0
- package/dist/nodefony/command/security-user-list.js +77 -0
- package/dist/nodefony/config/config.js +366 -0
- package/dist/nodefony/config/defineModuleConfig.js +35 -0
- package/dist/nodefony/contracts/IAccessVoter.js +13 -0
- package/dist/nodefony/contracts/IApiKey.js +1 -0
- package/dist/nodefony/contracts/IAuditEvent.js +1 -0
- package/dist/nodefony/contracts/IAuditStore.js +1 -0
- package/dist/nodefony/contracts/IAuthenticator.js +1 -0
- package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
- package/dist/nodefony/contracts/IFirewall.js +1 -0
- package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
- package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
- package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
- package/dist/nodefony/contracts/ISecuredArea.js +1 -0
- package/dist/nodefony/contracts/IToken.js +1 -0
- package/dist/nodefony/contracts/ITokenStore.js +1 -0
- package/dist/nodefony/contracts/ITotpSecret.js +1 -0
- package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
- package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
- package/dist/nodefony/contracts/IWebhookStore.js +1 -0
- package/dist/nodefony/contracts/index.js +2 -0
- package/dist/nodefony/errors/AccessDeniedError.js +14 -0
- package/dist/nodefony/errors/ApiKeyError.js +21 -0
- package/dist/nodefony/errors/AuthenticationError.js +14 -0
- package/dist/nodefony/errors/CsrfError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +39 -0
- package/dist/nodefony/errors/SsrfError.js +17 -0
- package/dist/nodefony/errors/ThrottledError.js +21 -0
- package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
- package/dist/nodefony/errors/WebAuthnError.js +21 -0
- package/dist/nodefony/errors/index.js +9 -0
- package/dist/nodefony/service/accessTokenVerifier.js +77 -0
- package/dist/nodefony/service/apiKeys.js +310 -0
- package/dist/nodefony/service/auditService.js +145 -0
- package/dist/nodefony/service/authFlow.js +332 -0
- package/dist/nodefony/service/authorization.js +95 -0
- package/dist/nodefony/service/cors.js +81 -0
- package/dist/nodefony/service/csrf.js +97 -0
- package/dist/nodefony/service/firewall.js +699 -0
- package/dist/nodefony/service/oauth2.js +153 -0
- package/dist/nodefony/service/securityHeaders.js +80 -0
- package/dist/nodefony/service/tokenService.js +486 -0
- package/dist/nodefony/service/totp.js +209 -0
- package/dist/nodefony/service/webAuthn.js +343 -0
- package/dist/nodefony/service/webhooks.js +539 -0
- package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
- package/dist/nodefony/src/SecuredArea.js +51 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
- package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
- package/dist/nodefony/src/admin/adminAudit.js +37 -0
- package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
- package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
- package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
- package/dist/nodefony/src/audit/auditBridge.js +82 -0
- package/dist/nodefony/src/audit/auditFilters.js +60 -0
- package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
- package/dist/nodefony/src/audit/readAuditContext.js +24 -0
- package/dist/nodefony/src/audit/recordAudit.js +16 -0
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
- package/dist/nodefony/src/authenticator/bearer.js +2 -0
- package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
- package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
- package/dist/nodefony/src/crypto/secretCipher.js +79 -0
- package/dist/nodefony/src/csp.js +54 -0
- package/dist/nodefony/src/csrfToken.js +65 -0
- package/dist/nodefony/src/net/ssrfGuard.js +130 -0
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
- package/dist/nodefony/src/oauth/providers/github.js +65 -0
- package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
- package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
- package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
- package/dist/nodefony/src/sessionIdentity.js +35 -0
- package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
- package/dist/nodefony/src/token/AnonymousToken.js +40 -0
- package/dist/nodefony/src/token/JwtKeystore.js +160 -0
- package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
- package/dist/nodefony/src/token/UserToken.js +67 -0
- package/dist/nodefony/src/token/jwtRuntime.js +19 -0
- package/dist/nodefony/src/token/secretFile.js +134 -0
- package/dist/nodefony/src/token/tokenCriteria.js +35 -0
- package/dist/nodefony/src/token/tokenFilters.js +72 -0
- package/dist/nodefony/src/token/tokenSort.js +40 -0
- package/dist/nodefony/src/token/tokenStatus.js +35 -0
- package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
- package/dist/nodefony/src/totp/totpCipher.js +30 -0
- package/dist/nodefony/src/totp/totpCrypto.js +226 -0
- package/dist/nodefony/src/totp/totpOperations.js +129 -0
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
- package/dist/nodefony/src/voter/RoleVoter.js +32 -0
- package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
- package/dist/nodefony/src/voter/voterRegistry.js +20 -0
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
- package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
- package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
- package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
- package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
- package/dist/nodefony/src/webhook/webhookSort.js +48 -0
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
- package/dist/types/index.d.ts +157 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
- package/dist/types/nodefony/command/security-token.d.ts +44 -0
- package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
- package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
- package/dist/types/nodefony/config/config.d.ts +295 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
- package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
- package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
- package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
- package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
- package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
- package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
- package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
- package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
- package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
- package/dist/types/nodefony/contracts/index.d.ts +9 -0
- package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
- package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
- package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
- package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
- package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
- package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
- package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
- package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
- package/dist/types/nodefony/errors/index.d.ts +8 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
- package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
- package/dist/types/nodefony/service/auditService.d.ts +30 -0
- package/dist/types/nodefony/service/authFlow.d.ts +123 -0
- package/dist/types/nodefony/service/authorization.d.ts +33 -0
- package/dist/types/nodefony/service/cors.d.ts +48 -0
- package/dist/types/nodefony/service/csrf.d.ts +57 -0
- package/dist/types/nodefony/service/firewall.d.ts +148 -0
- package/dist/types/nodefony/service/oauth2.d.ts +66 -0
- package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
- package/dist/types/nodefony/service/tokenService.d.ts +103 -0
- package/dist/types/nodefony/service/totp.d.ts +58 -0
- package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
- package/dist/types/nodefony/service/webhooks.d.ts +160 -0
- package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
- package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
- package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
- package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
- package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
- package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
- package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
- package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
- package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
- package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
- package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
- package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
- package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
- package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
- package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
- package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
- package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
- package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
- package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
- package/dist/types/nodefony/src/csp.d.ts +39 -0
- package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
- package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
- package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
- package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
- package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
- package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
- package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
- package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
- package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
- package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
- package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
- package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
- package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
- package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
- package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
- package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
- package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
- package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
- package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
- package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
- package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
- package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
- package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
- package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
- package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
- package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
- package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
- package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
- package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
- package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
- package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
- package/docs/api-keys.md +691 -0
- package/docs/audit.md +751 -0
- package/docs/authenticators.md +487 -0
- package/docs/authorization.md +497 -0
- package/docs/cors.md +497 -0
- package/docs/csrf.md +392 -0
- package/docs/external-jwt.md +181 -0
- package/docs/firewall.md +546 -0
- package/docs/headers.md +616 -0
- package/docs/index.md +207 -0
- package/docs/lexique.md +190 -0
- package/docs/oauth2.md +575 -0
- package/docs/obtenir-un-jeton.md +225 -0
- package/docs/tokens.md +520 -0
- package/docs/totp.md +804 -0
- package/docs/webauthn.md +733 -0
- package/docs/webhooks.md +1016 -0
- package/package.json +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# @nodefony/security
|
|
2
|
+
|
|
3
|
+
Couche de sécurité de Nodefony.
|
|
4
|
+
|
|
5
|
+
Firewall par zones, authentication (pattern `IAuthenticator`), autorisation (rôles + voters),
|
|
6
|
+
CORS, CSRF, en-têtes de sécurité, JWT, clés API, WebAuthn/passkeys, OAuth2 social, 2FA TOTP,
|
|
7
|
+
webhooks signés, audit persistant. **Modèle d'identité hybride**, **Zero Trust** par défaut.
|
|
8
|
+
Consomme [`@nodefony/user`](https://www.npmjs.com/package/@nodefony/user).
|
|
9
|
+
|
|
10
|
+
> **Statut** : cœur livré — firewall + zones, session serveur (NIST), JWT (`jose`), WebAuthn,
|
|
11
|
+
> OAuth2 social, CSRF/CORS/en-têtes natifs, clés API/PAT, 2FA TOTP, webhooks, audit persistant,
|
|
12
|
+
> rate-limit, **voters d'autorisation** (`RoleVoter` + `ScopeVoter`, jury affirmatif à véto
|
|
13
|
+
> `DENY`, découverts au boot par le `voterRegistry`) et les décorateurs **`@IsGranted`** et
|
|
14
|
+
> **`@CsrfProtect`**, appliqués par le `Resolver` avant l'action.
|
|
15
|
+
>
|
|
16
|
+
> Reste : ACL fine par ressource (niveau B de `authorization.ts`), journal d'authentification
|
|
17
|
+
> dédié, `rpId` WebAuthn dérivé du `Host` (multi-vhost), `MTlsAuthenticator` (niche). Le serveur
|
|
18
|
+
> d'autorisation OAuth 2.1 est tranché **après** la 10.0.0.
|
|
19
|
+
|
|
20
|
+
## Principes
|
|
21
|
+
|
|
22
|
+
- **Pattern `IAuthenticator`** (supports / authenticate / onSuccess) — pas de Bridge/Factory.
|
|
23
|
+
- **Zero Trust** : une zone protégée sans utilisateur authentifié → `401`.
|
|
24
|
+
- **Identité hybride** (révisé 2026-06-06) : le **web/Studio** ouvre une **session serveur**
|
|
25
|
+
(cookie opaque BFF, révocable, `HttpOnly; Secure; SameSite`) ; les **API/agents** portent leur
|
|
26
|
+
preuve à chaque requête (**JWT** signé / clé API). Jamais « full stateless » : la session reste
|
|
27
|
+
la fondation web (révocation immédiate), le JWT est réservé au sans-état machine-à-machine.
|
|
28
|
+
- **Config type-safe** : schéma **Zod** (18 sections, tout `enabled`), **introspectable** (Studio
|
|
29
|
+
génère son formulaire d'édition). L'app configure via `use("@nodefony/security", { … })`.
|
|
30
|
+
- **En-têtes natifs** (sans la lib `helmet`) — 0 dépendance, nonce CSP par requête.
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
La sécurité se configure dans le **`nodefony.config.ts`** de l'app via `use()` — colocalisée
|
|
35
|
+
avec le chargement du module, jamais dans un fichier séparé :
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// nodefony.config.ts
|
|
39
|
+
import { defineConfig, use } from "nodefony";
|
|
40
|
+
|
|
41
|
+
export default defineConfig((ctx) => ({
|
|
42
|
+
modules: [
|
|
43
|
+
"@nodefony/http",
|
|
44
|
+
"@nodefony/framework",
|
|
45
|
+
use(
|
|
46
|
+
"@nodefony/security",
|
|
47
|
+
{
|
|
48
|
+
// Hiérarchie de rôles (RBAC) — ROLE_X hérite des rôles listés (DFS au boot).
|
|
49
|
+
roleHierarchy: { ROLE_ADMIN: ["ROLE_USER"] },
|
|
50
|
+
// Zones firewall — clé = nom de zone, valeur = pattern + authenticators.
|
|
51
|
+
areas: {
|
|
52
|
+
// Web/Studio : session serveur (cookie opaque BFF).
|
|
53
|
+
admin: { pattern: "^/admin", authenticators: ["session"] },
|
|
54
|
+
// API machine-à-machine : preuve à chaque requête, aucune session.
|
|
55
|
+
"api-m2m": {
|
|
56
|
+
pattern: "^/api",
|
|
57
|
+
authenticators: ["jwt", "apikey"],
|
|
58
|
+
stateless: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{ policy: "mandatory" }, // sécurité = requise dès qu'on sert du trafic
|
|
63
|
+
),
|
|
64
|
+
],
|
|
65
|
+
}));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> Une zone se déclare idéalement **par module** (override `module-security`, dans la config du
|
|
69
|
+
> module) pour vivre au plus près de ses routes. Authenticators fournis : `anonymous`, `session`,
|
|
70
|
+
> `userpassword`, `jwt`, `apikey`, `webauthn`, plus les providers OAuth2. La chaîne est validée
|
|
71
|
+
> au boot (`mode: "first"` par défaut = le premier qui reconnaît authentifie ; `"all"` = tous
|
|
72
|
+
> requis, ex. mTLS + JWT). Config invalide → firewall **fail-closed** (tout rejeté).
|
|
73
|
+
|
|
74
|
+
Toutes les sections (`cors`, `csrf`, `headers`, `rateLimit`, `jwt`, `apiKeys`, `webhooks`,
|
|
75
|
+
`audit`, `studio`…) ont des **défauts sûrs** et sont **désactivables** via `enabled`.
|
|
76
|
+
Voir [`nodefony/config/config.ts`](https://github.com/nodefony/nodefony-core/blob/claude-ts/src/packages/@nodefony/security/nodefony/config/config.ts) — chaque option y est
|
|
77
|
+
documentée (explication + défaut + reco).
|
|
78
|
+
|
|
79
|
+
### Sections
|
|
80
|
+
|
|
81
|
+
<!-- prettier-ignore -->
|
|
82
|
+
| Section | Rôle | Défaut |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| `encoders` | hash mot de passe | Argon2id (OWASP) ; bcrypt legacy |
|
|
85
|
+
| `roleHierarchy` | héritage de rôles | `{}` (plats) |
|
|
86
|
+
| `areas` | zones firewall (pattern + host + authenticators) | `{}` (aucune route protégée) |
|
|
87
|
+
| `cors` | Cross-Origin | strict (jamais `*`+credentials) |
|
|
88
|
+
| `csrf` | Fetch Metadata (`Sec-Fetch-Site`) + repli Origin (OWASP 2025) | activé ; `strictSameSite:false`, `trustedOrigins:[]` |
|
|
89
|
+
| `headers` | HSTS/CSP+nonces/frameguard/noSniff… (natif) | activé ; avancés (COOP/COEP/CORP…) en option |
|
|
90
|
+
| `rateLimit` | anti brute-force + lockout | activé |
|
|
91
|
+
| `jwt` | jetons API/agents (sans-état) | EdDSA, access 15 min / refresh 7 j, rotation |
|
|
92
|
+
| `apiKeys` | clés API (PAT) hashées | préfixe `nf`, expiry 90 j |
|
|
93
|
+
| `webhooks` | sortants signés HMAC | anti-replay + anti-SSRF |
|
|
94
|
+
| `audit` | journal sécurité (append-only) | activé, stream Studio |
|
|
95
|
+
| `studio` | durcissement console admin | **OFF**, `localhost` (durcissement réservé) |
|
|
96
|
+
|
|
97
|
+
### CSRF — Fetch Metadata d'abord
|
|
98
|
+
|
|
99
|
+
La défense CSRF est **globale** (toute requête qui modifie l'état : `POST`/`PUT`/`PATCH`/`DELETE` ;
|
|
100
|
+
les méthodes sûres `GET`/`HEAD`/`OPTIONS` ne sont jamais bloquées). Trois couches, dans l'ordre :
|
|
101
|
+
|
|
102
|
+
1. **`Sec-Fetch-Site`** (défense primaire) — le navigateur tamponne lui-même la provenance de la
|
|
103
|
+
requête, un script attaquant ne peut pas la falsifier. `same-origin` et `none` (navigation directe
|
|
104
|
+
ou client non-navigateur) passent ; `cross-site` est **rejeté en 403**.
|
|
105
|
+
2. **Repli `Origin`/`Referer`** — pour les vieux navigateurs sans `Sec-Fetch-*` : l'origine doit
|
|
106
|
+
correspondre à l'hôte de l'app. Ni l'un ni l'autre (client non-navigateur) → autorisé (hors vecteur).
|
|
107
|
+
3. **Cookie `SameSite=Lax`** — défense en profondeur sur le cookie de session.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
csrf: {
|
|
111
|
+
strictSameSite: false, // true → bloque aussi les sous-domaines (same-site) : multi-tenant
|
|
112
|
+
trustedOrigins: ["https://app.example.org"], // alias multi-domaine légitimes (autorisés même cross-site)
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> `trustedOrigins` est **distinct** de `cors.origins` : déclarer un simple alias de domaine ne doit pas
|
|
117
|
+
> ouvrir la lecture CORS des réponses au JavaScript tiers. Une origine listée dans `cors.origins` est
|
|
118
|
+
> toutefois aussi acceptée (ce que CORS autorise explicitement n'est pas du CSRF).
|
|
119
|
+
|
|
120
|
+
Le token synchronizer renforcé (`@CsrfProtect` / `@CsrfExempt`) arrive à l'étape suivante.
|
|
121
|
+
|
|
122
|
+
### CORS — l'inverse du CSRF
|
|
123
|
+
|
|
124
|
+
CORS **assouplit** la Same-Origin Policy : il autorise un site tiers à _lire_ la réponse de l'app en
|
|
125
|
+
JavaScript (le CSRF, lui, _empêche_ un tiers de déclencher une mutation). La politique est globale :
|
|
126
|
+
|
|
127
|
+
- **Preflight** `OPTIONS` (Fetch Standard) court-circuité **avant le routing** → `204` + en-têtes
|
|
128
|
+
`Access-Control-Allow-*`. Il ne s'authentifie jamais (il ne porte pas de credentials).
|
|
129
|
+
- **Origine autorisée** → l'origine est **reflétée** (`Access-Control-Allow-Origin: <origine>` + `Vary: Origin`).
|
|
130
|
+
`*` n'est émis que sans `credentials`. Origine non autorisée → aucun en-tête (le navigateur bloque).
|
|
131
|
+
- **`origins:["*"]` + `credentials:true` est refusé au démarrage** (refine Zod) : le navigateur l'interdit,
|
|
132
|
+
et c'est une faille classique. Pour les credentials, lister les origines explicitement.
|
|
133
|
+
|
|
134
|
+
### Introspection (Studio)
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { securityConfigJsonSchema } from "@nodefony/security";
|
|
138
|
+
const schema = securityConfigJsonSchema(); // JSON Schema → formulaire d'édition Studio
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Journal d'audit (événements de sécurité)
|
|
142
|
+
|
|
143
|
+
Le journal trace les **transitions d'état** de sécurité (login, refus, jeton émis/révoqué, verrou
|
|
144
|
+
WS) — distinct du log de trafic (1 PDU/requête). Émission **explicite** par point sensible ; le
|
|
145
|
+
**chemin de succès reste muet** (le volume n'est pas un signal), seul l'échec/refus émet (cold-path)
|
|
146
|
+
→ aucun coût ajouté au hot-path nominal. Activé par défaut (`audit.enabled`, OWASP A09), coût nul si
|
|
147
|
+
désactivé. Lecture : `GET /nodefony/security/api/audit/events` (RBAC `ROLE_NODEFONY_ADMIN`).
|
|
148
|
+
|
|
149
|
+
**Flux temps réel** (lot 4) : canal WS **`nodefony:audit`**, réservé `ROLE_NODEFONY_ADMIN`
|
|
150
|
+
(plancher système, un cran au-dessus de l'observabilité générique `ROLE_ADMIN`). Enregistré comme
|
|
151
|
+
**canal système** sur le hub realtime (servable par tout endpoint, sans couplage à Studio) ;
|
|
152
|
+
**lazy** : le pont ne s'abonne au journal qu'au 1ᵉʳ auditeur connecté et s'en détache au dernier.
|
|
153
|
+
Coalescé (1 frame `{ events, dropped }` toutes les ~250 ms, ring borné) → un pic d'échecs sous
|
|
154
|
+
attaque ne noie pas la console. Un user non habilité qui tente de s'y abonner est refusé **et audité**
|
|
155
|
+
(`frame.denied`).
|
|
156
|
+
|
|
157
|
+
Un **secret n'entre jamais** dans un événement — seule sa _présence_ est tracée (`flags`).
|
|
158
|
+
|
|
159
|
+
| `action` | `category` | `outcome` | Émis par |
|
|
160
|
+
| ---------------------- | ---------- | --------- | -------------------------------- |
|
|
161
|
+
| `login.success` | `auth` | success | `AuthFlow` (BFF) / fédéré |
|
|
162
|
+
| `login.failure` | `auth` | failure | `AuthFlow`, `TokenService` grant |
|
|
163
|
+
| `login.throttled` | `auth` | failure | `AuthFlow`, `TokenService` grant |
|
|
164
|
+
| `logout` | `session` | success | `AuthFlow` |
|
|
165
|
+
| `auth.failure` | `auth` | failure | `Firewall` (credential invalide) |
|
|
166
|
+
| `auth.throttled` | `auth` | failure | `Firewall` (backoff NIST) |
|
|
167
|
+
| `auth.denied` | `auth` | denied | `Firewall` (Zero Trust) |
|
|
168
|
+
| `access.denied` | `authz` | denied | `Authorization` (voters/RBAC) |
|
|
169
|
+
| `frame.denied` | `ws` | denied | verrou de frame WS |
|
|
170
|
+
| `token.issued` | `token` | success | `TokenService` |
|
|
171
|
+
| `token.reuse_detected` | `token` | denied | `TokenService` (RFC 9700) |
|
|
172
|
+
| `apikey.created` | `token` | success | `ApiKeyService` |
|
|
173
|
+
| `apikey.revoked` | `token` | success | `ApiKeyService` |
|
|
174
|
+
|
|
175
|
+
## Erreurs
|
|
176
|
+
|
|
177
|
+
- `AuthenticationError` → `401` (non authentifié).
|
|
178
|
+
- `AccessDeniedError` → `403` (authentifié mais non autorisé).
|
|
179
|
+
|
|
180
|
+
## Licence
|
|
181
|
+
|
|
182
|
+
CeCILL-B — Christophe CAMENSULI.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
|
|
2
|
+
function __decorate(decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { __decorate as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorateMetadata.js
|
|
2
|
+
function __decorateMetadata(k, v) {
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
export { __decorateMetadata as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import config from "./nodefony/config/config.js";
|
|
2
|
+
import { SecuredArea } from "./nodefony/src/SecuredArea.js";
|
|
3
|
+
import { LoginThrottler } from "./nodefony/src/throttle/LoginThrottler.js";
|
|
4
|
+
import { RoleHierarchyWalker } from "./nodefony/src/RoleHierarchyWalker.js";
|
|
5
|
+
import { CsrfTokenManager } from "./nodefony/src/csrfToken.js";
|
|
6
|
+
import { CsrfError } from "./nodefony/errors/CsrfError.js";
|
|
7
|
+
import { Csrf } from "./nodefony/service/csrf.js";
|
|
8
|
+
import { Cors } from "./nodefony/service/cors.js";
|
|
9
|
+
import { SecurityHeaders } from "./nodefony/service/securityHeaders.js";
|
|
10
|
+
import { AuthenticationError } from "./nodefony/errors/AuthenticationError.js";
|
|
11
|
+
import { ThrottledError } from "./nodefony/errors/ThrottledError.js";
|
|
12
|
+
import { UnverifiableTokenError } from "./nodefony/errors/UnverifiableTokenError.js";
|
|
13
|
+
import { defineSecurityConfig, securityConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
|
|
14
|
+
import { AnonymousToken } from "./nodefony/src/token/AnonymousToken.js";
|
|
15
|
+
import { AnonymousAuthenticator } from "./nodefony/src/authenticator/AnonymousAuthenticator.js";
|
|
16
|
+
import { UserToken } from "./nodefony/src/token/UserToken.js";
|
|
17
|
+
import { SessionAuthenticator } from "./nodefony/src/authenticator/SessionAuthenticator.js";
|
|
18
|
+
import { UserPasswordAuthenticator } from "./nodefony/src/authenticator/UserPasswordAuthenticator.js";
|
|
19
|
+
import peekIssuer from "./nodefony/src/authenticator/peekIssuer.js";
|
|
20
|
+
import { JwtAuthenticator } from "./nodefony/src/authenticator/JwtAuthenticator.js";
|
|
21
|
+
import { generateApiKey, hashApiKey, looksLikeApiKey, parseApiKey } from "./nodefony/src/apikey/apiKeyFormat.js";
|
|
22
|
+
import { ApiKeyAuthenticator } from "./nodefony/src/authenticator/ApiKeyAuthenticator.js";
|
|
23
|
+
import { ExternalJwtAuthenticator } from "./nodefony/src/authenticator/ExternalJwtAuthenticator.js";
|
|
24
|
+
import { resolveJwtRuntime } from "./nodefony/src/token/jwtRuntime.js";
|
|
25
|
+
import { getAuthenticatorFactory, listAuthenticatorFactories, registerAuthenticatorFactory } from "./nodefony/src/authenticator/authenticatorRegistry.js";
|
|
26
|
+
import { recordAudit } from "./nodefony/src/audit/recordAudit.js";
|
|
27
|
+
import { readAuditContext } from "./nodefony/src/audit/readAuditContext.js";
|
|
28
|
+
import { Firewall } from "./nodefony/service/firewall.js";
|
|
29
|
+
import { AuthFlow } from "./nodefony/service/authFlow.js";
|
|
30
|
+
import { InvalidTargetError } from "./nodefony/errors/InvalidTargetError.js";
|
|
31
|
+
import { TOKEN_DEFAULT_ORDER, TOKEN_SORTABLE_FIELDS } from "./nodefony/src/token/tokenSort.js";
|
|
32
|
+
import { matchesTokenStatus, tokenStatusOf } from "./nodefony/src/token/tokenStatus.js";
|
|
33
|
+
import { MemoryTokenStore } from "./nodefony/src/token/MemoryTokenStore.js";
|
|
34
|
+
import { getTokenStoreFactory, listTokenStores, registerTokenStore } from "./nodefony/src/token/tokenStoreRegistry.js";
|
|
35
|
+
import { JwtKeystore } from "./nodefony/src/token/JwtKeystore.js";
|
|
36
|
+
import { TokenService } from "./nodefony/service/tokenService.js";
|
|
37
|
+
import { RemoteJwtVerifier } from "./nodefony/src/token/RemoteJwtVerifier.js";
|
|
38
|
+
import { AccessTokenVerifierService } from "./nodefony/service/accessTokenVerifier.js";
|
|
39
|
+
import { VoterVote } from "./nodefony/contracts/IAccessVoter.js";
|
|
40
|
+
import { RoleVoter } from "./nodefony/src/voter/RoleVoter.js";
|
|
41
|
+
import { ScopeVoter } from "./nodefony/src/voter/ScopeVoter.js";
|
|
42
|
+
import { listVoterFactories, registerVoterFactory } from "./nodefony/src/voter/voterRegistry.js";
|
|
43
|
+
import { Authorization } from "./nodefony/service/authorization.js";
|
|
44
|
+
import { MemoryWebAuthnCredentialStore } from "./nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js";
|
|
45
|
+
import { getWebAuthnStoreFactory, listWebAuthnStores, registerWebAuthnStore } from "./nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js";
|
|
46
|
+
import { WebAuthnService } from "./nodefony/service/webAuthn.js";
|
|
47
|
+
import { getOAuthProviderFactory, listOAuthProviders, registerOAuthProvider } from "./nodefony/src/oauth/oauthProviderRegistry.js";
|
|
48
|
+
import { OAuth2Service } from "./nodefony/service/oauth2.js";
|
|
49
|
+
import { TOKEN_FACETS } from "./nodefony/src/token/tokenFilters.js";
|
|
50
|
+
import { ApiKeyError } from "./nodefony/errors/ApiKeyError.js";
|
|
51
|
+
import { ApiKeyService } from "./nodefony/service/apiKeys.js";
|
|
52
|
+
import { MemoryAuditStore } from "./nodefony/src/audit/MemoryAuditStore.js";
|
|
53
|
+
import { getAuditStoreFactory, listAuditStores, registerAuditStore } from "./nodefony/src/audit/auditStoreRegistry.js";
|
|
54
|
+
import AuditService from "./nodefony/service/auditService.js";
|
|
55
|
+
import { MemoryTotpSecretStore } from "./nodefony/src/totp/MemoryTotpSecretStore.js";
|
|
56
|
+
import { getTotpStoreFactory, listTotpStores, registerTotpStore } from "./nodefony/src/totp/totpSecretStoreRegistry.js";
|
|
57
|
+
import { decryptSecret, deriveKey, encryptSecret, generateEphemeralKey } from "./nodefony/src/crypto/secretCipher.js";
|
|
58
|
+
import { deriveTotpKey } from "./nodefony/src/totp/totpCipher.js";
|
|
59
|
+
import { base32Decode, totpCode } from "./nodefony/src/totp/totpCrypto.js";
|
|
60
|
+
import { beginTotpEnrollment, confirmTotpEnrollment, disableTotp, totpStatus, verifyTotpLogin } from "./nodefony/src/totp/totpOperations.js";
|
|
61
|
+
import { TotpService } from "./nodefony/service/totp.js";
|
|
62
|
+
import { WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS } from "./nodefony/src/webhook/webhookSort.js";
|
|
63
|
+
import { MemoryWebhookStore } from "./nodefony/src/webhook/MemoryWebhookStore.js";
|
|
64
|
+
import { getWebhookStoreFactory, listWebhookStores, registerWebhookStore } from "./nodefony/src/webhook/webhookStoreRegistry.js";
|
|
65
|
+
import { SsrfError } from "./nodefony/errors/SsrfError.js";
|
|
66
|
+
import { assertPublicUrl, isBlockedAddress } from "./nodefony/src/net/ssrfGuard.js";
|
|
67
|
+
import { WebhookService } from "./nodefony/service/webhooks.js";
|
|
68
|
+
import SecuritySecrets from "./nodefony/command/security-secrets.js";
|
|
69
|
+
import SecurityUserAdd from "./nodefony/command/security-user-add.js";
|
|
70
|
+
import SecurityUserList from "./nodefony/command/security-user-list.js";
|
|
71
|
+
import SecurityUserDelete from "./nodefony/command/security-user-delete.js";
|
|
72
|
+
import SecurityToken from "./nodefony/command/security-token.js";
|
|
73
|
+
import { createSecurityAdminApi, parseAuditQuery, registerSecurityAdminApi } from "./nodefony/src/admin/SecurityAdminApi.js";
|
|
74
|
+
import { registerUserRevocationCascade } from "./nodefony/src/admin/userRevocationCascade.js";
|
|
75
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
|
|
76
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
|
|
77
|
+
import { tokenStatusCriteria } from "./nodefony/src/token/tokenCriteria.js";
|
|
78
|
+
import { AccessDeniedError } from "./nodefony/errors/AccessDeniedError.js";
|
|
79
|
+
import "./nodefony/errors/index.js";
|
|
80
|
+
import "./nodefony/contracts/index.js";
|
|
81
|
+
import { Kernel, Module, services } from "nodefony";
|
|
82
|
+
import { fileURLToPath } from "node:url";
|
|
83
|
+
import { registerUserAdminApi } from "@nodefony/user";
|
|
84
|
+
//#region index.ts
|
|
85
|
+
/**
|
|
86
|
+
* `@nodefony/security` — couche de sécurité de Nodefony (refonte 2026, P6).
|
|
87
|
+
*
|
|
88
|
+
* Identité **hybride** : session serveur cookie opaque (BFF) par défaut web/Studio,
|
|
89
|
+
* JWT réservé API/M2M/agents. Pattern **`IAuthenticator`** + registre de fabriques
|
|
90
|
+
* (pluggable), **Zero Trust** par défaut, config type-safe `defineSecurityConfig()`
|
|
91
|
+
* + Zod validée au boot (fail-closed si invalide). Consomme `@nodefony/user`
|
|
92
|
+
* (IUser/IUserProvider/IPasswordVerifier) — jamais l'inverse.
|
|
93
|
+
*
|
|
94
|
+
* Livré : firewall (zones, mode first|all, challenge RFC 7235), authenticators
|
|
95
|
+
* `anonymous`/`userpassword` (Basic RFC 7617)/`session` (BFF, J3), flux
|
|
96
|
+
* login/logout/me (`AuthFlow`, anti-fixation + throttling partagé).
|
|
97
|
+
* Jwt/oauth2/mtls/apikey, CORS, CSRF, autorisation par décorateurs et data
|
|
98
|
+
* plane Studio arrivent aux sessions suivantes (plan J0→J10).
|
|
99
|
+
*/
|
|
100
|
+
let Security = class Security extends Module {
|
|
101
|
+
constructor(kernel) {
|
|
102
|
+
super("security", kernel, fileURLToPath(import.meta.url), config);
|
|
103
|
+
this.addCommand(SecuritySecrets);
|
|
104
|
+
this.addCommand(SecurityUserAdd);
|
|
105
|
+
this.addCommand(SecurityUserList);
|
|
106
|
+
this.addCommand(SecurityUserDelete);
|
|
107
|
+
this.addCommand(SecurityToken);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* JSON Schema de la config security → data plane admin (config riche Studio).
|
|
111
|
+
* ⚠️ Les VALEURS effectives des secrets (csrf/jwt/oauth) sont redactées côté
|
|
112
|
+
* serveur (`safeConfig`) AVANT envoi — le schéma ne décrit que la structure.
|
|
113
|
+
*/
|
|
114
|
+
configSchema() {
|
|
115
|
+
return securityConfigJsonSchema();
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Enregistre les data planes admin **sécurité** (`/nodefony/security/api/*`,
|
|
119
|
+
* P6.14) ET **utilisateur** (`/nodefony/user/api/*`, P6.15) auprès du broker
|
|
120
|
+
* AVANT que framework ne monte les routes (`onReady`). Le data plane user est
|
|
121
|
+
* DÉFINI dans `@nodefony/user` (propriétaire du domaine) mais monté ici, car
|
|
122
|
+
* `@nodefony/user` est une lib pure non-bootable et `security` en dépend déjà.
|
|
123
|
+
* No-op si le broker est absent (Studio non chargé) ou déjà enregistré.
|
|
124
|
+
*/
|
|
125
|
+
async onKernelBoot() {
|
|
126
|
+
const container = this.kernel?.container;
|
|
127
|
+
const registry = container?.get("adminBroker");
|
|
128
|
+
if (registry && container) {
|
|
129
|
+
registerSecurityAdminApi(registry, container);
|
|
130
|
+
registerUserAdminApi(registry, container);
|
|
131
|
+
}
|
|
132
|
+
if (this.kernel && container) registerUserRevocationCascade(this.kernel, container);
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
Security = __decorate([services([
|
|
137
|
+
Firewall,
|
|
138
|
+
AuthFlow,
|
|
139
|
+
TokenService,
|
|
140
|
+
AccessTokenVerifierService,
|
|
141
|
+
ApiKeyService,
|
|
142
|
+
Authorization,
|
|
143
|
+
WebAuthnService,
|
|
144
|
+
OAuth2Service,
|
|
145
|
+
AuditService,
|
|
146
|
+
TotpService,
|
|
147
|
+
WebhookService
|
|
148
|
+
]), __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])], Security);
|
|
149
|
+
var security_default = Security;
|
|
150
|
+
//#endregion
|
|
151
|
+
export { AccessDeniedError, AccessTokenVerifierService, AnonymousAuthenticator, AnonymousToken, ApiKeyAuthenticator, ApiKeyError, ApiKeyService, AuditService, AuthFlow, AuthenticationError, Authorization, Cors, Csrf, CsrfError, CsrfTokenManager, ExternalJwtAuthenticator, Firewall, InvalidTargetError, JwtAuthenticator, JwtKeystore, LoginThrottler, MemoryAuditStore, MemoryTokenStore, MemoryTotpSecretStore, MemoryWebAuthnCredentialStore, MemoryWebhookStore, OAuth2Service, RemoteJwtVerifier, RoleHierarchyWalker, RoleVoter, ScopeVoter, SecuredArea, SecurityHeaders, SessionAuthenticator, SsrfError, TOKEN_DEFAULT_ORDER, TOKEN_FACETS, TOKEN_SORTABLE_FIELDS, ThrottledError, TokenService, TotpService, UnverifiableTokenError, UserPasswordAuthenticator, UserToken, VoterVote, WEBHOOK_DEFAULT_ORDER, WEBHOOK_SORTABLE_FIELDS, WebAuthnService, WebhookService, assertPublicUrl, base32Decode, beginTotpEnrollment, confirmTotpEnrollment, createSecurityAdminApi, decryptSecret, security_default as default, defineSecurityConfig, deriveKey, deriveTotpKey, disableTotp, encryptSecret, generateApiKey, generateEphemeralKey, getAuditStoreFactory, getAuthenticatorFactory, getOAuthProviderFactory, getTokenStoreFactory, getTotpStoreFactory, getWebAuthnStoreFactory, getWebhookStoreFactory, hashApiKey, isBlockedAddress, listAuditStores, listAuthenticatorFactories, listOAuthProviders, listTokenStores, listTotpStores, listVoterFactories, listWebAuthnStores, listWebhookStores, looksLikeApiKey, matchesTokenStatus, parseApiKey, parseAuditQuery, peekIssuer, readAuditContext, recordAudit, registerAuditStore, registerAuthenticatorFactory, registerOAuthProvider, registerSecurityAdminApi, registerTokenStore, registerTotpStore, registerVoterFactory, registerWebAuthnStore, registerWebhookStore, resolveJwtRuntime, securityConfigJsonSchema, tokenStatusCriteria, tokenStatusOf, totpCode, totpStatus, verifyTotpLogin };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { readIfPresentSync } from "../src/token/secretFile.js";
|
|
2
|
+
import { Command } from "nodefony";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { appendFileSync } from "node:fs";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
//#region nodefony/command/security-secrets.ts
|
|
8
|
+
const options = {
|
|
9
|
+
helpGroup: "COMPTES ET SECRETS",
|
|
10
|
+
showBanner: false,
|
|
11
|
+
kernelEvent: "onReady"
|
|
12
|
+
};
|
|
13
|
+
const CYAN = "\x1B[36m";
|
|
14
|
+
const GREEN = "\x1B[32m";
|
|
15
|
+
const YELLOW = "\x1B[33m";
|
|
16
|
+
const DIM = "\x1B[2m";
|
|
17
|
+
const BOLD = "\x1B[1m";
|
|
18
|
+
const RESET = "\x1B[0m";
|
|
19
|
+
/** Les 3 clés générées (nom d'env var → rôle affiché). */
|
|
20
|
+
const KEYS = [
|
|
21
|
+
"NF_TOTP_KEY",
|
|
22
|
+
"NF_WEBHOOK_KEY",
|
|
23
|
+
"NF_CSRF_SECRET"
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Ce que chaque secret PROTÈGE, et ce qui casse sans lui.
|
|
27
|
+
*
|
|
28
|
+
* 🔴 Sans ce catalogue, la commande était muette sur l'essentiel : quand les
|
|
29
|
+
* trois clés étaient en place, elle affichait trois « ✓ » et RIEN d'autre — ni
|
|
30
|
+
* les noms, ni les rôles. On ne savait donc ni ce qui avait été généré, ni
|
|
31
|
+
* pourquoi. Un secret qu'on ne comprend pas est un secret qu'on ne fait jamais
|
|
32
|
+
* tourner, et qu'on recopie d'un environnement à l'autre.
|
|
33
|
+
*
|
|
34
|
+
* La conséquence est écrite au présent et pour la PRODUCTION : c'est là qu'une
|
|
35
|
+
* clé absente cesse d'être un avertissement de développement.
|
|
36
|
+
*/
|
|
37
|
+
const ROLES = {
|
|
38
|
+
NF_TOTP_KEY: {
|
|
39
|
+
protected: "chiffre le secret 2FA de chaque compte au repos (AES-256-GCM)",
|
|
40
|
+
without: "2FA désactivé en production — un secret chiffré par une clé éphémère serait illisible au redémarrage"
|
|
41
|
+
},
|
|
42
|
+
NF_WEBHOOK_KEY: {
|
|
43
|
+
protected: "chiffre les secrets de signature des webhooks au repos",
|
|
44
|
+
without: "webhooks désactivés en production (fail-safe, jamais de signature muette)"
|
|
45
|
+
},
|
|
46
|
+
NF_CSRF_SECRET: {
|
|
47
|
+
protected: "signe les jetons anti-rejeu des mutations (`@CsrfProtect`)",
|
|
48
|
+
without: "en cluster, le jeton émis par un pod est rejeté par les autres"
|
|
49
|
+
},
|
|
50
|
+
"jwt.keystore": {
|
|
51
|
+
protected: "signe les JWT (clé Ed25519, rotation gérée par le keystore)",
|
|
52
|
+
without: "chaque process signe avec la sienne : un jeton émis par la CLI est refusé par le serveur"
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* `nodefony security:secrets` — génère les clés de chiffrement attendues par le
|
|
57
|
+
* module security (TOTP, webhooks, CSRF) au bon format (32 octets aléatoires,
|
|
58
|
+
* base64) et guide le câblage en 3 FICHIERS (.env → env.ts → nodefony.config.ts).
|
|
59
|
+
* Réponse directe aux warnings « clé ÉPHÉMÈRE générée » du boot.
|
|
60
|
+
*
|
|
61
|
+
* DX anti-confusion (vécu : blocs collés dans le shell → parse error zsh) :
|
|
62
|
+
* - chaque étape nomme son FICHIER et rappelle que rien ne se tape au terminal ;
|
|
63
|
+
* - les étapes déjà faites sont DÉTECTÉES (grep des fichiers du projet) et
|
|
64
|
+
* affichées `✓` au lieu de redemander un collage ;
|
|
65
|
+
* - `--write` écrit le `.env` (fichier local gitignoré) : ajoute uniquement les
|
|
66
|
+
* clés ABSENTES, ne remplace jamais une valeur existante (rotation = manuelle).
|
|
67
|
+
* `env.ts` et `nodefony.config.ts` ne sont JAMAIS modifiés (code de l'app).
|
|
68
|
+
*/
|
|
69
|
+
var SecuritySecrets = class extends Command {
|
|
70
|
+
constructor(cli) {
|
|
71
|
+
super("security:secrets", "engendre les clés de chiffrement du module security", cli, options);
|
|
72
|
+
this.addOption("-j, --json", "sortie JSON (scripts/CI)");
|
|
73
|
+
this.addOption("-w, --write", "écrit les clés manquantes dans le .env du projet (jamais de remplacement)");
|
|
74
|
+
}
|
|
75
|
+
/** Racine du projet (kernel booté) — repli cwd. */
|
|
76
|
+
#root() {
|
|
77
|
+
return this.kernel?.path ?? process.cwd();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `true` si `.env.local` est SUIVI par git — y écrire des secrets les mènerait
|
|
81
|
+
* au commit (convention B : `*.local` doit être gitignoré). Best-effort : git
|
|
82
|
+
* absent / hors repo → `false` (on écrit).
|
|
83
|
+
*/
|
|
84
|
+
#dotenvTracked() {
|
|
85
|
+
try {
|
|
86
|
+
return spawnSync("git", [
|
|
87
|
+
"ls-files",
|
|
88
|
+
"--error-unmatch",
|
|
89
|
+
".env.local"
|
|
90
|
+
], {
|
|
91
|
+
cwd: this.#root(),
|
|
92
|
+
stdio: "ignore"
|
|
93
|
+
}).status === 0;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Contenu d'un fichier du projet, "" si absent/illisible (détection best-effort). */
|
|
99
|
+
#read(file) {
|
|
100
|
+
try {
|
|
101
|
+
return readIfPresentSync(path.resolve(this.#root(), file)) ?? "";
|
|
102
|
+
} catch {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async generate(opts) {
|
|
107
|
+
const gen = () => randomBytes(32).toString("base64");
|
|
108
|
+
const secrets = {};
|
|
109
|
+
for (const k of KEYS) secrets[k] = gen();
|
|
110
|
+
if (opts.json) {
|
|
111
|
+
process.stdout.write(JSON.stringify(secrets, null, 2) + "\n");
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
const dotenvLocal = this.#read(".env.local");
|
|
115
|
+
const dotenv = this.#read(".env") + "\n" + dotenvLocal;
|
|
116
|
+
const envTs = this.#read("env.ts");
|
|
117
|
+
const cfgTs = this.#read("nodefony.config.ts");
|
|
118
|
+
const missingInDotenv = KEYS.filter((k) => !new RegExp(`^\\s*${k}\\s*=`, "m").test(dotenv));
|
|
119
|
+
const missingInEnvTs = KEYS.filter((k) => !envTs.includes(k));
|
|
120
|
+
const WIRING = {
|
|
121
|
+
NF_TOTP_KEY: ` totp: { encryptionKey: ctx.env.NF_TOTP_KEY },`,
|
|
122
|
+
NF_WEBHOOK_KEY: ` webhooks: { encryptionKey: ctx.env.NF_WEBHOOK_KEY },`,
|
|
123
|
+
NF_CSRF_SECRET: ` csrf: { secret: ctx.env.NF_CSRF_SECRET },`
|
|
124
|
+
};
|
|
125
|
+
const missingInCfg = KEYS.filter((k) => !cfgTs.includes(k));
|
|
126
|
+
const w = (s) => {
|
|
127
|
+
process.stdout.write(s);
|
|
128
|
+
};
|
|
129
|
+
w(`\n${BOLD}🔐 Secrets du module security${RESET} ${DIM}— 4 secrets, 3 fichiers${RESET}\n\n`);
|
|
130
|
+
for (const [name, role] of Object.entries(ROLES)) w(` ${(name === "jwt.keystore" ? /keystore\s*:/u.test(cfgTs) : new RegExp(`^\\s*${name}\\s*=`, "m").test(dotenv)) ? "\x1B[32m✓" : "\x1B[33m○"}${RESET} ${BOLD}${name.padEnd(16)}${RESET}${DIM}${role.protected}${RESET}\n ${DIM}sans → ${role.without}${RESET}\n`);
|
|
131
|
+
const tokenStillThere = /^\s*NF_MCP_TOKEN\s*=/m.test(dotenvLocal);
|
|
132
|
+
w(`\n${DIM} · NF_MCP_TOKEN n'est PAS un secret de cette application, et n'a rien à\n faire dans cette liste : c'est un jeton qu'elle ÉMET, présenté par un\n agent pour entrer. Aucun code d'ici ne le lit. Il se pose chez l'agent\n qui le porte — nodefony security:token --write.${RESET}\n`);
|
|
133
|
+
if (tokenStillThere) w(`${YELLOW} ⚠ une ligne NF_MCP_TOKEN traîne encore dans .env.local — rien ne la lit,\n tu peux la retirer.${RESET}\n`);
|
|
134
|
+
w(`\n${YELLOW}⚠ rien ne se tape dans le terminal : chaque bloc se colle dans le fichier indiqué.${RESET}\n\n`);
|
|
135
|
+
w(`${BOLD}1. Fichier ${CYAN}.env.local${RESET}${BOLD} — les valeurs${RESET} ${DIM}(gitignoré ; .env commité = défauts NON-secrets)${RESET}\n`);
|
|
136
|
+
if (missingInDotenv.length === 0) w(` ${GREEN}✓ les 3 clés y sont déjà${RESET} ${DIM}(rien à faire — rotation = remplacer la valeur à la main)${RESET}\n\n`);
|
|
137
|
+
else if (opts.write && this.#dotenvTracked()) w(` ${YELLOW}⚠ .env.local est suivi par git — je n'y écris PAS de secrets.${RESET}\n ${DIM}Ajoute \`*.local\` au .gitignore (et \`git rm --cached .env.local\`), puis relance --write ;\n ou colle les lignes ci-dessous à la main :${RESET}\n\n` + missingInDotenv.map((k) => ` ${k}=${secrets[k]}`).join("\n") + `\n\n`);
|
|
138
|
+
else if (opts.write) {
|
|
139
|
+
const block = (dotenvLocal && !dotenvLocal.endsWith("\n") ? "\n" : "") + `# clés security — générées par \`nodefony security:secrets\`\n` + missingInDotenv.map((k) => `${k}=${secrets[k]}`).join("\n") + "\n";
|
|
140
|
+
appendFileSync(path.resolve(this.#root(), ".env.local"), block);
|
|
141
|
+
w(` ${GREEN}✓ écrit dans .env.local${RESET} ${DIM}(${missingInDotenv.join(", ")} — les clés déjà présentes n'ont pas été touchées)${RESET}\n\n`);
|
|
142
|
+
} else w(` colle ces lignes ${DIM}(ou relance avec ${RESET}${CYAN}--write${RESET}${DIM} pour que je les écrive)${RESET} :\n\n` + missingInDotenv.map((k) => ` ${k}=${secrets[k]}`).join("\n") + `\n ${DIM}(en prod : Secret k8s / vault — jamais en git)${RESET}\n\n`);
|
|
143
|
+
w(`${BOLD}2. Fichier ${CYAN}env.ts${RESET}${BOLD} — la déclaration typée${RESET} ${DIM}(env.ts est le seul lecteur de process.env)${RESET}\n`);
|
|
144
|
+
if (missingInEnvTs.length === 0) w(` ${GREEN}✓ déjà déclarées${RESET}\n\n`);
|
|
145
|
+
else w(` ajoute dans le defineEnv({ … }) :\n\n` + missingInEnvTs.map((k) => ` ${k}: envString({ optional: true }),`).join("\n") + `\n\n`);
|
|
146
|
+
w(`${BOLD}3. Fichier ${CYAN}nodefony.config.ts${RESET}${BOLD} — le câblage vers le module security${RESET}\n`);
|
|
147
|
+
if (missingInCfg.length === 0) w(` ${GREEN}✓ déjà câblées${RESET}\n\n`);
|
|
148
|
+
else w(" complète l'entrée security du manifeste modules :\n\n use(\"@nodefony/security\", {\n" + missingInCfg.map((k) => WIRING[k]).join("\n") + `\n }),\n\n`);
|
|
149
|
+
const jwtCable = /keystore\s*:/u.test(cfgTs);
|
|
150
|
+
w(`${BOLD}4. Fichier ${CYAN}nodefony.config.ts${RESET}${BOLD} — les clés de SIGNATURE des jetons${RESET} ${DIM}(jwt.keystore)${RESET}\n`);
|
|
151
|
+
if (jwtCable) w(` ${GREEN}✓ déjà câblées${RESET}\n\n`);
|
|
152
|
+
else w(` ${YELLOW}⚠ absentes : chaque process signe avec une clé ÉPHÉMÈRE.${RESET}\n ${DIM}Un jeton émis par la CLI porte alors un \`kid\` que le serveur ne\n connaît pas, et il est refusé — et un redémarrage invalide les jetons\n en vol. Ce n'est pas une valeur à coller : c'est une SOURCE à déclarer.${RESET}\n\n use("@nodefony/security", {\n jwt: { keystore: ctx.isProd ? {} : { dir: "var/keys" } },\n }),\n\n ${DIM}En production, le dossier n'a pas de sens (pods jetables) : la clé vient\n de l'environnement — jwt.keystore.keySetJson, injecté par ton gestionnaire\n de secrets et partagé par tous les pods.${RESET}\n\n`);
|
|
153
|
+
w(`${DIM}Pourquoi 3 fichiers ? .env.local porte la VALEUR (secret machine, gitignoré —\nle .env commité ne porte que des défauts non-secrets) ; env.ts la DÉCLARE\n(catalogue typé, validé au boot) ; nodefony.config.ts la CÂBLE au module.\nLes étapes 2 et 3 ne se font qu'une fois — ensuite seule l'étape 1 vit.\nL'étape 4 sort de ce schéma, et c'est voulu : un keyset n'est pas une valeur\nqu'on colle, mais une source que le keystore gère (rotation, permissions).${RESET}\n\nRelance le serveur : plus aucun warning « clé ÉPHÉMÈRE » au boot.\n\n`);
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
//#endregion
|
|
158
|
+
export { SecuritySecrets as default };
|