@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
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Authenticators — prouver l'identité (session, mot de passe, JWT, clé API)"
|
|
3
|
+
navTitle: Authenticators
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/security"
|
|
6
|
+
topic: authenticators
|
|
7
|
+
coverageModule: security
|
|
8
|
+
section: "Sécurité"
|
|
9
|
+
audience: [developer]
|
|
10
|
+
tags:
|
|
11
|
+
[
|
|
12
|
+
security,
|
|
13
|
+
authentication,
|
|
14
|
+
jwt,
|
|
15
|
+
apikey,
|
|
16
|
+
session,
|
|
17
|
+
basic,
|
|
18
|
+
bearer,
|
|
19
|
+
rfc6750,
|
|
20
|
+
rfc8725,
|
|
21
|
+
nist,
|
|
22
|
+
]
|
|
23
|
+
version: "doc"
|
|
24
|
+
status: stable
|
|
25
|
+
updated: 2026-07-19
|
|
26
|
+
source: "src/packages/@nodefony/security/docs/authenticators.md"
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
# Authenticators — prouver l'identité
|
|
30
|
+
|
|
31
|
+
> Un **authenticator** répond à une seule question : _« qui es-tu, et peux-tu le prouver ? »_. Il ne
|
|
32
|
+
> décide **pas** des droits (ça, c'est l'autorisation / les voters) — il établit une **identité**.
|
|
33
|
+
> Le firewall enchaîne les authenticators déclarés par une zone jusqu'à obtenir une preuve valide,
|
|
34
|
+
> sinon il ferme en 401 (Zero Trust). Nodefony en fournit **six** intégrés, tous ancrés ici sur le
|
|
35
|
+
> code (`src/packages/@nodefony/security/nodefony/src/authenticator/`).
|
|
36
|
+
|
|
37
|
+
📍 [Documentation](../../../../../docs/index.md) › [Sécurité](index.md) › **Authenticators**
|
|
38
|
+
|
|
39
|
+
## 🧠 Le cycle d'un authenticator
|
|
40
|
+
|
|
41
|
+
```mermaid
|
|
42
|
+
flowchart TD
|
|
43
|
+
REQ["Requête (HTTP ou WS)"] --> SUP{"supports(ctx) ?<br/>credential présent ?"}
|
|
44
|
+
SUP -->|non| NEXT["maillon suivant<br/>(ou 401 Zero Trust)"]
|
|
45
|
+
SUP -->|oui| CT["createToken()<br/>credential brut, non vérifié"]
|
|
46
|
+
CT --> AU["authenticate(token)<br/>vérifie · révocation · sujet"]
|
|
47
|
+
AU -->|échec| F["onFailure → 401 + challenge<br/>(message UNIFORME)"]
|
|
48
|
+
AU -->|succès| S["onSuccess → user + token dans l'ALS"]
|
|
49
|
+
S --> CTRL["→ autorisation → contrôleur"]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
C'est `Firewall.#authenticate()` (`firewall.ts:1112`) qui déroule ce cycle pour chaque maillon de la
|
|
53
|
+
zone, dans l'ordre déclaré. Le succès pose l'identité dans l'ALS ; l'échec remonte au firewall qui
|
|
54
|
+
pose le 401 et son challenge — l'authenticator, lui, ne touche jamais à la réponse.
|
|
55
|
+
|
|
56
|
+
## 📖 Lexique
|
|
57
|
+
|
|
58
|
+
| Terme | Sens |
|
|
59
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
60
|
+
| Authentification | Établir **qui** est l'appelant (≠ autorisation, qui établit ce qu'il a le **droit** de faire). |
|
|
61
|
+
| Authenticator | Une stratégie de preuve d'identité (`session`, `jwt`…) implémentant `IAuthenticator`. |
|
|
62
|
+
| BFF | _Backend For Frontend_ : le web s'authentifie par **session serveur** (cookie opaque), pas par jeton exposé au JS. |
|
|
63
|
+
| Bearer | Schéma `Authorization: Bearer <jeton>` (RFC 6750) — porté par les API. |
|
|
64
|
+
| PAT | _Personal Access Token_ : une clé API personnelle, bearer **opaque** révocable. |
|
|
65
|
+
| JWS/JWT | Jeton signé auto-porté (structure compacte `a.b.c`). |
|
|
66
|
+
| JWKS | _JSON Web Key Set_ : le trousseau de clés publiques qui vérifie les signatures JWT. |
|
|
67
|
+
| EdDSA | Algorithme de signature asymétrique (Ed25519) — le seul accepté par le vérificateur JWT. |
|
|
68
|
+
| CRC | Somme de contrôle embarquée dans une clé API — filtre les valeurs malformées avant la base. |
|
|
69
|
+
| ALS | _AsyncLocalStorage_ : le contexte ambiant de la requête où le firewall pose `user` + `token`. |
|
|
70
|
+
| Challenge | En-tête `WWW-Authenticate` renvoyé avec un 401 (RFC 7235) indiquant comment s'authentifier. |
|
|
71
|
+
| Zero Trust | Sur une zone protégée, **aucune preuve valide ⇒ 401** ; l'anonymat n'est accepté que s'il est déclaré. |
|
|
72
|
+
|
|
73
|
+
## Qu'est-ce qu'un authenticator — et quelle faille il ferme
|
|
74
|
+
|
|
75
|
+
Un serveur qui expose des données doit distinguer un appelant légitime d'un inconnu. Le faire « à la
|
|
76
|
+
main » dans chaque contrôleur, c'est garantir qu'un endpoint finira par être oublié — la faille la
|
|
77
|
+
plus banale et la plus grave.
|
|
78
|
+
|
|
79
|
+
Nodefony **centralise** la preuve d'identité dans le firewall : une zone déclare _quelles preuves
|
|
80
|
+
elle accepte_, et rien n'atteint le contrôleur sans être passé par là. Chaque authenticator ferme une
|
|
81
|
+
classe d'attaque précise — détaillées brique par brique dans le catalogue :
|
|
82
|
+
|
|
83
|
+
- **énumération de comptes** (messages d'échec uniformes) ;
|
|
84
|
+
- **brute-force et DoS par hachage** (backoff NIST avant tout hash) ;
|
|
85
|
+
- **algorithm confusion / injection de clé JWT** (allowlist + JWKS local) ;
|
|
86
|
+
- **jeton volé non révocable** (denylist `jti`, PAT opaque révocable) ;
|
|
87
|
+
- **identité périmée** (sujet re-vérifié à chaque requête).
|
|
88
|
+
|
|
89
|
+
## La vision Nodefony — un contrat, un registre, un firewall agnostique
|
|
90
|
+
|
|
91
|
+
### Le contrat `IAuthenticator`
|
|
92
|
+
|
|
93
|
+
Tout authenticator implémente le même cycle (`IAuthenticator.ts:18`), ce qui rend le firewall
|
|
94
|
+
totalement agnostique de la stratégie :
|
|
95
|
+
|
|
96
|
+
| Méthode | Rôle |
|
|
97
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
98
|
+
| `supports(ctx)` | Test **bon marché** : le credential de cette stratégie est-il présent ? (sinon, maillon suivant) |
|
|
99
|
+
| `createToken(ctx)` | Extrait le credential **brut, non vérifié**, dans un `UserToken`. |
|
|
100
|
+
| `authenticate(token)` | **Vérifie** (signature/hash/session), applique la **révocation**, **re-résout le sujet** — ou lève un 401. |
|
|
101
|
+
| `onSuccess(ctx,tok)` | Effet de bord au succès (poser l'identité en session, audit). |
|
|
102
|
+
| `onFailure(ctx,err)` | Slot d'audit (le 401 + challenge sont posés par le firewall). |
|
|
103
|
+
| `challenge()` | **Optionnel** (`IAuthenticator.ts:42`) — la valeur `WWW-Authenticate` (RFC 7235) des 401 de la zone. |
|
|
104
|
+
|
|
105
|
+
### Le registre pluggable
|
|
106
|
+
|
|
107
|
+
Les authenticators sont résolus par **nom** : `Firewall.#instantiateAuthenticators()`
|
|
108
|
+
(`firewall.ts:402`) interroge `getAuthenticatorFactory()` (`authenticatorRegistry.ts:59`) — jamais
|
|
109
|
+
un `if (name === "jwt")` dans le firewall, qui trahirait la promesse « pluggable ».
|
|
110
|
+
|
|
111
|
+
- Les **cinq builtins HTTP** (`anonymous`, `userpassword`, `session`, `jwt`, `apikey`)
|
|
112
|
+
s'enregistrent à l'import du module via `registerAuthenticatorFactory()`
|
|
113
|
+
(`authenticatorRegistry.ts:72-125`) — donc toujours avant le boot.
|
|
114
|
+
- Le sixième, `firewall-realtime`, n'est **pas dans le registre** : c'est le firewall qui le câble
|
|
115
|
+
lui-même au handshake WS des zones protégées (`Firewall.#wireRealtime()`, `firewall.ts:268`).
|
|
116
|
+
- La fabrique ne fait que **construire** ; les résolutions de services coûteuses (`users`,
|
|
117
|
+
`tokenStore`, keystore) restent **lazy** dans l'instance (cold path).
|
|
118
|
+
- Un nom inconnu en config = boot **fail-closed** — `#configError` posé + log CRITIC
|
|
119
|
+
(`firewall.ts:419`) : jamais de zone « protégée » silencieusement ouverte à cause d'une
|
|
120
|
+
faute de frappe.
|
|
121
|
+
|
|
122
|
+
## 🚀 Démarrage rapide
|
|
123
|
+
|
|
124
|
+
### Une zone, trois preuves — la même API pour le web et les machines
|
|
125
|
+
|
|
126
|
+
Dans une app `nodefony create app`, on déclare quelles preuves une zone accepte — un **objet par
|
|
127
|
+
nom**, validé Zod au boot (`areas: z.record(...)`, `config.ts:902`) :
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// nodefony.config.ts (extrait)
|
|
131
|
+
use("@nodefony/security", {
|
|
132
|
+
areas: {
|
|
133
|
+
// Une seule zone, trois preuves : le navigateur (cookie de session),
|
|
134
|
+
// un service (JWT), un script CI (clé API). `mode: "first"` (défaut) :
|
|
135
|
+
// le premier maillon qui reconnaît la requête authentifie.
|
|
136
|
+
api: {
|
|
137
|
+
pattern: "^/api",
|
|
138
|
+
authenticators: ["session", "jwt", "apikey"],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
> [!IMPORTANT]
|
|
145
|
+
> **Le login est FOURNI** : `POST /nodefony/security/api/auth/login` (body `{ username, password }`
|
|
146
|
+
> → `Set-Cookie` de session, ID régénéré anti-fixation), avec `logout` et `me`
|
|
147
|
+
> (`SessionAuthController.ts:37-39`). Pas de LoginController à écrire — tes routes ne font que
|
|
148
|
+
> consommer l'identité.
|
|
149
|
+
|
|
150
|
+
### Ce que TU écris : le controller qui consomme l'identité
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
// nodefony/controllers/WhoAmIController.ts — complet, compile tel quel
|
|
154
|
+
import {
|
|
155
|
+
controller,
|
|
156
|
+
Controller,
|
|
157
|
+
Get,
|
|
158
|
+
IsGranted,
|
|
159
|
+
CurrentUser,
|
|
160
|
+
} from "@nodefony/framework";
|
|
161
|
+
import type { IUser } from "@nodefony/user";
|
|
162
|
+
|
|
163
|
+
@controller("/api/v1")
|
|
164
|
+
class WhoAmIController extends Controller {
|
|
165
|
+
// Zone `api` : le firewall a DÉJÀ validé une des trois preuves (session,
|
|
166
|
+
// JWT ou clé API) — sinon 401 avant ce code. @IsGranted ajoute le rôle.
|
|
167
|
+
@IsGranted(["ROLE_USER"])
|
|
168
|
+
@Get("/whoami")
|
|
169
|
+
async whoami(@CurrentUser() user: IUser) {
|
|
170
|
+
// La même réponse quelle que soit la preuve présentée par le client.
|
|
171
|
+
return this.renderJson({ identifier: user.identifier, roles: user.roles });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default WhoAmIController;
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Ce qu'on observe
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
# 1) Sans preuve : Zero Trust → 401 + challenge du premier maillon qui en déclare
|
|
182
|
+
curl -si http://localhost:5151/api/v1/whoami | grep -E "^(HTTP|WWW)"
|
|
183
|
+
# HTTP/1.1 401 Unauthorized
|
|
184
|
+
# WWW-Authenticate: Bearer
|
|
185
|
+
|
|
186
|
+
# 2) Web — login BFF (compte dev seedé admin/admin) → cookie de session
|
|
187
|
+
curl -si -c /tmp/jar -H 'Content-Type: application/json' \
|
|
188
|
+
-d '{"username":"admin","password":"admin"}' \
|
|
189
|
+
http://localhost:5151/nodefony/security/api/auth/login | head -1
|
|
190
|
+
# HTTP/1.1 200 OK
|
|
191
|
+
|
|
192
|
+
# 3) La même route, deux preuves différentes → la même identité
|
|
193
|
+
curl -s -b /tmp/jar http://localhost:5151/api/v1/whoami # session (web)
|
|
194
|
+
curl -s -H 'Authorization: Bearer nf_…' \
|
|
195
|
+
http://localhost:5151/api/v1/whoami # clé API (CI)
|
|
196
|
+
# {"identifier":"admin","roles":["ROLE_NODEFONY_ADMIN", …]}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Requête par requête, qui répond :
|
|
200
|
+
|
|
201
|
+
| Le client envoie… | Maillon (`supports()`) | Résultat |
|
|
202
|
+
| ------------------------------------ | ---------------------- | --------------------------------------------------------- |
|
|
203
|
+
| le cookie de session | `session` | identifié — rôles frais re-résolus en base |
|
|
204
|
+
| `Authorization: Bearer eyJ…` (a.b.c) | `jwt` | identifié — signature EdDSA + claims vérifiés |
|
|
205
|
+
| `Authorization: Bearer nf_…` | `apikey` | identifié — clé vérifiée au store, révocable |
|
|
206
|
+
| rien | aucun | **401** + `WWW-Authenticate: Bearer` (`firewall.ts:1200`) |
|
|
207
|
+
|
|
208
|
+
## 🔐 Les six authenticators intégrés
|
|
209
|
+
|
|
210
|
+
| Nom | Credential | Vérité | Révocable | Pour… |
|
|
211
|
+
| ------------------- | ---------------------------------------- | ---------- | :-------: | ---------------------------------- |
|
|
212
|
+
| `anonymous` | (aucun) | — | — | accepter l'anonymat explicitement |
|
|
213
|
+
| `userpassword` | `Authorization: Basic base64(id:mdp)` | verifier | n/a | outils/scripts, brique login |
|
|
214
|
+
| `session` | cookie de session (identifiant en blob) | serveur | immédiate | le **web** après login (BFF) |
|
|
215
|
+
| `jwt` | `Authorization: Bearer <a.b.c>` | auto-porté | via état | API service↔service, agents |
|
|
216
|
+
| `apikey` | `Authorization: Bearer nf_…` | serveur | immédiate | API/CI/scripts d'un user |
|
|
217
|
+
| `firewall-realtime` | identité déjà résolue au handshake (ALS) | serveur | 1 fenêtre | le **WebSocket** de toute identité |
|
|
218
|
+
|
|
219
|
+
### `anonymous` — accepter explicitement l'anonymat
|
|
220
|
+
|
|
221
|
+
Le seul authenticator autorisé à produire un token **non authentifié** sans déclencher le Zero Trust
|
|
222
|
+
(`AnonymousAuthenticator.ts:19`).
|
|
223
|
+
|
|
224
|
+
- `supports()` accepte tout (`AnonymousAuthenticator.ts:22`) ; le token porte le **singleton gelé**
|
|
225
|
+
`anonymousUser` — zéro allocation d'utilisateur (`AnonymousToken.ts:9`).
|
|
226
|
+
- À ne lister **que volontairement** : `["jwt", "anonymous"]` en mode `first` signifie « identifié
|
|
227
|
+
si preuve présente, sinon visiteur anonyme accepté ». En mode `all`, utile en **dernier** :
|
|
228
|
+
« canal prouvé (ex. mTLS), identité utilisateur optionnelle ».
|
|
229
|
+
- Sans lui, zone protégée + aucune preuve = 401 : la défense en profondeur du firewall n'accepte un
|
|
230
|
+
token non authentifié que si `anonymous` est le maillon déclaré (`firewall.ts:827`).
|
|
231
|
+
- **Faille fermée** : l'anonymat _implicite_ — ici il est un choix explicite et auditable, jamais un
|
|
232
|
+
défaut.
|
|
233
|
+
|
|
234
|
+
### `userpassword` — HTTP Basic + backoff NIST
|
|
235
|
+
|
|
236
|
+
Schéma **HTTP Basic** (RFC 7617) : `Authorization: Basic base64(id:mdp)`, charset UTF-8, scheme
|
|
237
|
+
case-insensitive (`UserPasswordAuthenticator.ts:11`) ; `createToken()` split au **premier** `:` —
|
|
238
|
+
le mot de passe peut en contenir (`UserPasswordAuthenticator.ts:74`).
|
|
239
|
+
|
|
240
|
+
- **La vérification est déléguée** au `IPasswordVerifier` (le `UserService`) : hash, comparaison,
|
|
241
|
+
leurre anti-timing, re-hash transparent — l'authenticator ne voit que le verdict.
|
|
242
|
+
- **Message uniforme** `INVALID_CREDENTIALS` quelle que soit la cause — identifiant inconnu, compte
|
|
243
|
+
verrouillé, mot de passe faux (`UserPasswordAuthenticator.ts:16`) → anti-énumération de comptes.
|
|
244
|
+
- **Throttling NIST SP 800-63B AVANT le verifier** : `#throttler.check()` sur l'identifiant saisi
|
|
245
|
+
(`UserPasswordAuthenticator.ts:101-103`) — un identifiant bloqué ne coûte **aucun hash** → le
|
|
246
|
+
throttle protège aussi le serveur du **DoS argon2**. Échec compté, succès remis à zéro
|
|
247
|
+
(`UserPasswordAuthenticator.ts:111-114`). `ThrottledError` → **429 + `Retry-After`**
|
|
248
|
+
(`firewall.ts:764`).
|
|
249
|
+
- **Le throttler est PARTAGÉ** avec le login JSON du BFF — même `loginThrottler` du container : un
|
|
250
|
+
attaquant ne contourne pas le backoff en changeant de porte (`authenticatorRegistry.ts:75-79`).
|
|
251
|
+
- Challenge : `Basic realm="nodefony", charset="UTF-8"` (`UserPasswordAuthenticator.ts:130`).
|
|
252
|
+
- **Piège** : le login par formulaire (JSON) n'est **pas** ici — c'est le BFF
|
|
253
|
+
(`/nodefony/security/api/auth/login`). Basic sert l'outillage (scripts, CLI).
|
|
254
|
+
|
|
255
|
+
### `session` — la preuve du web (BFF)
|
|
256
|
+
|
|
257
|
+
Après le login, chaque requête web prouve son identité par la **session serveur** (cookie opaque).
|
|
258
|
+
Credential = l'**identifiant** posé dans le blob de session, jamais un secret.
|
|
259
|
+
|
|
260
|
+
- **N'ouvre jamais la session lui-même** : `supports()` exige une session **déjà reprise** porteuse
|
|
261
|
+
d'un utilisateur (`SessionAuthenticator.ts:43-46`) — le pipeline http démarre la session _avant_
|
|
262
|
+
le firewall ; c'est `AuthFlow.login()` qui ouvre et régénère l'ID (anti-fixation).
|
|
263
|
+
- **L'identité est re-résolue à CHAQUE requête** via `resolveSessionIdentity`
|
|
264
|
+
(`SessionAuthenticator.ts:70`) → rôles frais, révocation immédiate. Les contrôles d'état sont
|
|
265
|
+
partagés avec `AuthFlow.me()` : `isLocked()`/`isActive()` → rejet (`sessionIdentity.ts:40`).
|
|
266
|
+
- `onSuccess()` pose l'identifiant sur le contexte — la persistance de session lie le blob au
|
|
267
|
+
principal courant (`SessionAuthenticator.ts:78-80`).
|
|
268
|
+
- **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login,
|
|
269
|
+
jamais une popup Basic (`SessionAuthenticator.ts:25-27`).
|
|
270
|
+
|
|
271
|
+
### `jwt` — Bearer signé pour les API (RFC 6750 + BCP RFC 8725)
|
|
272
|
+
|
|
273
|
+
Réservé aux **API service↔service / agents** (le web reste sur la session). Vérifie un access token
|
|
274
|
+
**EdDSA** signé par le keystore du serveur ; `supports()` ne réclame que la structure compacte
|
|
275
|
+
`a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts:20`). Les défenses **dures** du JWT BCP, toutes
|
|
276
|
+
prouvées en test :
|
|
277
|
+
|
|
278
|
+
| Défense | Comment | Attaque fermée |
|
|
279
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
|
|
280
|
+
| **Allowlist d'algorithmes** | `algorithms: ["EdDSA"]` — jamais l'algo de l'en-tête du token (`JwtAuthenticator.ts:120`) | `alg=none`, algorithm confusion (§3.1) |
|
|
281
|
+
| **Clé par `kid` du keyset LOCAL** | `createLocalJWKSet` — jamais `jku`/`jwk` de l'en-tête (`JwtAuthenticator.ts:158`) | injection de clé / SSRF (§3.5) |
|
|
282
|
+
| **`aud` + `iss` + `typ` obligatoires** | `typ: "at+jwt"` (§3.11) sépare access et refresh (`JwtAuthenticator.ts:105-107`) | refresh présenté comme access, token d'un autre service (§3.8-3.9) |
|
|
283
|
+
| **Révocation** | denylist `isJtiDenied` + seuil `invalidBefore` par porteur (`JwtAuthenticator.ts:123-131`) | jeton auto-porté volé, logout global |
|
|
284
|
+
| **Sujet revérifié** | `loadUserByIdentifier(sub)` → disparu/inactif/verrouillé = rejet (`JwtAuthenticator.ts:174-186`) | compte banni encore « valide » via son token (§3.10) |
|
|
285
|
+
|
|
286
|
+
Le **message d'échec est uniforme** (`INVALID_TOKEN`, `JwtAuthenticator.ts:24`) : la cause fine
|
|
287
|
+
(expiré, `aud`, signature, sujet banni) part en **audit**, jamais au client — anti-oracle. Le token
|
|
288
|
+
promu porte `scopes`, `jti`, `claims` en attributs (`JwtAuthenticator.ts:162-171`). `jose` est
|
|
289
|
+
importé **lazy** — dépendance lourde (`JwtAuthenticator.ts:96`).
|
|
290
|
+
|
|
291
|
+
### `apikey` — PAT opaque révocable
|
|
292
|
+
|
|
293
|
+
Clé API personnelle en `Authorization: Bearer nf_…` (préfixe `apiKeys.prefix`, défaut `nf`).
|
|
294
|
+
Contrairement au JWT (auto-porté), un PAT est un **bearer opaque** dont la vérité vit **côté
|
|
295
|
+
serveur** (`ITokenStore`) → **révocable immédiatement**. Défenses :
|
|
296
|
+
|
|
297
|
+
- **Forme + CRC validés AVANT tout accès au store** (`parseApiKey`, `ApiKeyAuthenticator.ts:99-101`)
|
|
298
|
+
→ une valeur malformée n'atteint jamais la base (**anti-DoS**).
|
|
299
|
+
- **Lookup par hash SHA-256** (`findByHash`, `ApiKeyAuthenticator.ts:105`) — le secret n'existe
|
|
300
|
+
**nulle part au repos**.
|
|
301
|
+
- **Révocation** (`revokedAt`) + **expiration** (`expiresAt`) (`ApiKeyAuthenticator.ts:109-111`) +
|
|
302
|
+
**ban en masse** du porteur (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts:117-118`).
|
|
303
|
+
- **Sujet revérifié** à chaque requête → rôles frais (`ApiKeyAuthenticator.ts:123`).
|
|
304
|
+
- **`lastUsedAt` throttlé** : aucune écriture sur le hot path tant que la fenêtre
|
|
305
|
+
`apiKeys.lastUsedThrottleS` n'est pas dépassée (`ApiKeyAuthenticator.ts:127-133`).
|
|
306
|
+
|
|
307
|
+
Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:138-140`).
|
|
308
|
+
|
|
309
|
+
### `firewall-realtime` — la promotion, en WebSocket, de l'identité déjà posée
|
|
310
|
+
|
|
311
|
+
> [!IMPORTANT]
|
|
312
|
+
> Ce n'est **pas** « l'authenticator de la session ». Il promeut **toute** identité que le firewall
|
|
313
|
+
> a résolue — y compris un agent authentifié par jeton porteur, sans cookie ni session. Son nom
|
|
314
|
+
> d'origine (`SessionRealtimeAuthenticator`) décrivait le premier mode branché, pas son rôle ; la
|
|
315
|
+
> confusion a coûté un durcissement pensé pour la session appliqué à toutes les identités
|
|
316
|
+
> (`FirewallRealtimeAuthenticator.ts:32-39`).
|
|
317
|
+
|
|
318
|
+
Sur un handshake WS (une requête upgrade HTTP qui traverse **le même pipeline**), `startSession`
|
|
319
|
+
puis `handleSecurity` ont **déjà** tourné : session chargée, identité re-résolue, `IUser` posé dans
|
|
320
|
+
l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
|
|
321
|
+
(`FirewallRealtimeAuthenticator.ts:80`).
|
|
322
|
+
|
|
323
|
+
- **Perf : il ne relit pas la base.** `authenticate()` réutilise l'identité de l'ALS
|
|
324
|
+
(`FirewallRealtimeAuthenticator.ts:91`) au lieu de refaire deux lectures base par connexion —
|
|
325
|
+
un coût évitable sur le différenciateur temps réel.
|
|
326
|
+
- **Câblé automatiquement** par `Firewall.#wireRealtime()` (`firewall.ts:268`) pour toute zone
|
|
327
|
+
protégée `realtime !== false` — une instance par zone au handshake (`firewall.ts:289`).
|
|
328
|
+
- **Le mode de preuve suit le jeton du firewall**, il n'est pas deviné : absent (zone historique),
|
|
329
|
+
on retombe sur le mode le plus strict, la session (`FirewallRealtimeAuthenticator.ts:101-103`).
|
|
330
|
+
- **Filet Zero Trust** : il câble un revalidateur appelé avant chaque action data plane
|
|
331
|
+
(`FirewallRealtimeAuthenticator.ts:105-107`) — la socket peut survivre à l'identité qui l'a
|
|
332
|
+
ouverte (logout, changement de compte, `jti` denylisté). En mode session,
|
|
333
|
+
`buildSessionRevalidator()` re-lit `storage.read(id)` et compare l'identifiant ; toute erreur de
|
|
334
|
+
lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts:227`). En mode jeton porteur, la
|
|
335
|
+
preuve est autre : `exp`, `jti` denylisté, `invalidBefore`
|
|
336
|
+
(`FirewallRealtimeAuthenticator.ts:127`).
|
|
337
|
+
|
|
338
|
+
> [!NOTE]
|
|
339
|
+
> **Asymétrie de révocation HTTP↔WS (assumée)** : le jeton realtime est figé au handshake (les
|
|
340
|
+
> frames lisent un cache O(1)) → une révocation prend effet **à la reconnexion**, pas à la frame
|
|
341
|
+
> suivante. C'est l'état de l'art (Socket.IO/Phoenix figent aussi au handshake) ; la révocation
|
|
342
|
+
> immédiate forte passe par le JWT + un canal « token révoqué ».
|
|
343
|
+
|
|
344
|
+
## ⚙️ Composer une zone — ordre, mode, cohabitation
|
|
345
|
+
|
|
346
|
+
La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
|
|
347
|
+
(`firewall.ts:1112`) selon le `mode` de la zone (`first` par défaut, `config.ts:87-92`).
|
|
348
|
+
|
|
349
|
+
### Situation 1 — humains ET machines sur la même API (`first`)
|
|
350
|
+
|
|
351
|
+
Ton back-office est appelé par le **navigateur** des utilisateurs connectés ET par un **script CI**.
|
|
352
|
+
Deux preuves différentes, mêmes routes — c'est la config du Démarrage rapide ci-dessus. Deux règles
|
|
353
|
+
de lecture :
|
|
354
|
+
|
|
355
|
+
- un maillon dont `supports()` est faux est simplement **sauté** en mode `first`
|
|
356
|
+
(`firewall.ts:1128`) ;
|
|
357
|
+
- un credential **présenté mais invalide échoue immédiatement** — l'échec d'`authenticate()`
|
|
358
|
+
remonte, jamais de fallback silencieux vers le maillon suivant (`firewall.ts:1112`). Une clé
|
|
359
|
+
API révoquée donne un 401 direct, même si un autre maillon aurait pu réussir.
|
|
360
|
+
- aucune preuve présentée sur toute la chaîne → `handleSecurity()` lève l'`AuthenticationError`
|
|
361
|
+
Zero Trust (`firewall.ts:738`).
|
|
362
|
+
|
|
363
|
+
### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
|
|
364
|
+
|
|
365
|
+
Tu veux « identifié si connecté, sinon visiteur » :
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
authenticators: ["session", "anonymous"], // ✅ session d'abord
|
|
369
|
+
authenticators: ["anonymous", "session"], // ❌ anonymous accepte TOUT le monde
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
> [!WARNING]
|
|
373
|
+
> `AnonymousAuthenticator.supports()` accepte **toutes** les requêtes — placé en premier en mode
|
|
374
|
+
> `first`, il court-circuite la liste : **personne n'est jamais identifié**, même avec un cookie
|
|
375
|
+
> valide. L'ordre est ta politique.
|
|
376
|
+
|
|
377
|
+
### Situation 3 — empiler les preuves (`all`)
|
|
378
|
+
|
|
379
|
+
En mode `all`, **chaque** maillon est obligatoire : `supports()` faux = 401 immédiat
|
|
380
|
+
(`firewall.ts:937`) et le **dernier** token de la chaîne porte l'identité (`firewall.ts:973`) —
|
|
381
|
+
utile pour exiger une preuve de canal (mTLS) **et** une identité, ou un « sudo mode » session +
|
|
382
|
+
re-saisie du mot de passe. Scénario complet côté zones : [firewall](./firewall.md).
|
|
383
|
+
|
|
384
|
+
### Cohabitation JWT + clé API dans une même zone
|
|
385
|
+
|
|
386
|
+
Les deux sont des `Bearer`, mais Nodefony les **discrimine sur la forme** — un JWT a la structure
|
|
387
|
+
compacte `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts:20`), un PAT porte le préfixe `nf_` sans
|
|
388
|
+
point (`looksLikeApiKey`, `ApiKeyAuthenticator.ts:72`). Chaque `supports()` ne réclame que _son_
|
|
389
|
+
format → aucun conflit, aucune double vérification.
|
|
390
|
+
|
|
391
|
+
## Le fil rouge : le message d'échec uniforme
|
|
392
|
+
|
|
393
|
+
Les cinq authenticators vérifiants renvoient **le même message** (`"Invalid credentials"` /
|
|
394
|
+
`"Invalid token"` / `"Invalid session"`) quelle que soit la cause réelle. Ce n'est pas de la
|
|
395
|
+
paresse : c'est une **défense anti-énumération / anti-oracle**.
|
|
396
|
+
|
|
397
|
+
Distinguer « compte inconnu » de « mot de passe faux », ou « token expiré » de « signature
|
|
398
|
+
invalide », donnerait à un attaquant une sonde. La cause fine part **toujours** en log d'audit ; le
|
|
399
|
+
client n'obtient qu'un 401 + son challenge — posé par le firewall, premier maillon de la zone qui
|
|
400
|
+
en déclare un (`Firewall.#setChallenge()`, `firewall.ts:1191`).
|
|
401
|
+
|
|
402
|
+
## 🧩 Ajouter un authenticator maison
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { registerAuthenticatorFactory } from "@nodefony/security";
|
|
406
|
+
|
|
407
|
+
registerAuthenticatorFactory("ldap", ({ container, config }) => {
|
|
408
|
+
return new LdapAuthenticator(() => container.get("ldapClient"));
|
|
409
|
+
});
|
|
410
|
+
// puis en config : areas.<zone>.authenticators = ["ldap", "anonymous"]
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
À faire au chargement du module (avant le boot). Trois règles, calquées sur les builtins :
|
|
414
|
+
|
|
415
|
+
- implémenter le contrat `IAuthenticator` (`IAuthenticator.ts:23`) — `challenge()` seulement si
|
|
416
|
+
un en-tête `WWW-Authenticate` a du sens pour la stratégie ;
|
|
417
|
+
- renvoyer le **message uniforme** en cas d'échec (la cause fine part en audit) ;
|
|
418
|
+
- laisser les résolutions de services **lazy** dans l'instance — la fabrique ne fait que
|
|
419
|
+
construire (`authenticatorRegistry.ts:29-31`).
|
|
420
|
+
|
|
421
|
+
## 📜 Normes appliquées
|
|
422
|
+
|
|
423
|
+
<!-- prettier-ignore -->
|
|
424
|
+
| Domaine | Norme | Ancrage |
|
|
425
|
+
| --- | --- | --- |
|
|
426
|
+
| Challenge d'auth (401) | RFC 7235 | `Firewall.#setChallenge()` (`firewall.ts:1191`) |
|
|
427
|
+
| Bearer | RFC 6750 | `readBearerHeader()` (`runtime/bearer.ts:68`, cœur) — une porte UNIQUE au cœur, plus une constante par authenticator |
|
|
428
|
+
| JWT (BCP) | RFC 7519, 8725 | `jwtVerify` durci : allowlist + claims (`JwtAuthenticator.ts:103-107`) |
|
|
429
|
+
| HTTP Basic | RFC 7617 | `UserPasswordAuthenticator` (`UserPasswordAuthenticator.ts:25-27`) |
|
|
430
|
+
| Backoff de login | NIST SP 800-63B | `#throttler.check()` avant le verifier (`UserPasswordAuthenticator.ts:101-103`) |
|
|
431
|
+
| Rate limit (429) | RFC 6585 | `Retry-After` posé par le firewall (`firewall.ts:764`) |
|
|
432
|
+
| Anti-énumération | OWASP | `INVALID_TOKEN` (`JwtAuthenticator.ts:24`) · `INVALID_CREDENTIALS` (`UserPasswordAuthenticator.ts:16`) |
|
|
433
|
+
|
|
434
|
+
## ⚡ Performance & mémoire
|
|
435
|
+
|
|
436
|
+
- `supports()` est un test **bon marché** (en-tête + regex) — et n'est payé que sur zone protégée
|
|
437
|
+
(le chemin chaud/froid vit dans le [firewall](./firewall.md)).
|
|
438
|
+
- Résolutions **lazy** : le verifier `#resolveVerifier` est mémoïsé au premier login
|
|
439
|
+
(`UserPasswordAuthenticator.ts:105`) ; keystore, `tokenStore` et `users` sont résolus du
|
|
440
|
+
container au premier usage ; `jose` est importé lazy (`JwtAuthenticator.ts:96`).
|
|
441
|
+
- `anonymous` : singleton gelé `anonymousUser`, zéro allocation d'utilisateur
|
|
442
|
+
(`AnonymousToken.ts:9`).
|
|
443
|
+
- `apikey` : écriture `lastUsedAt` **coalescée** — pas une écriture par requête
|
|
444
|
+
(`ApiKeyAuthenticator.ts:127-133`).
|
|
445
|
+
- `firewall-realtime` : **zéro lecture base** au handshake — réutilise l'ALS
|
|
446
|
+
(`FirewallRealtimeAuthenticator.ts:91`).
|
|
447
|
+
- Le throttle NIST **avant** le hash : un 429 ne coûte aucun argon2.
|
|
448
|
+
|
|
449
|
+
## ⚠️ Pièges (symptôme → cause → correction)
|
|
450
|
+
|
|
451
|
+
<!-- prettier-ignore -->
|
|
452
|
+
| Symptôme | Cause (dans le code) | Correction |
|
|
453
|
+
| --- | --- | --- |
|
|
454
|
+
| 401 systématique sur une zone protégée | Aucune preuve + `anonymous` non listé — Zero Trust (`firewall.ts:827`) | Ajouter `anonymous` en dernier si l'anonymat est voulu |
|
|
455
|
+
| 401 générique + log ERROR « service `users` » | Câblage : pas de `UserService` au container (`authenticatorRegistry.ts:85-88`) | Enregistrer un `UserService` au boot de l'app |
|
|
456
|
+
| JWT rejeté alors qu'il « semble » valide | `aud`/`iss`/`typ` non conformes, ou `alg` ≠ EdDSA (`JwtAuthenticator.ts:103-107`) | Émettre via le `TokenService` (mêmes iss/aud/typ) |
|
|
457
|
+
| Clé API révoquée encore acceptée quelques secondes | Confusion avec un JWT (auto-porté) — `revokedAt` est lu à chaque requête (`ApiKeyAuthenticator.ts:110`) | Un PAT est révoqué immédiatement ; vérifier `revokedAt` |
|
|
458
|
+
| Révocation WS pas immédiate | Identité figée au handshake — asymétrie assumée du `FirewallRealtimeAuthenticator` (`FirewallRealtimeAuthenticator.ts:51-55`) | Attendre la fenêtre de re-validation, ou utiliser JWT + canal révocation |
|
|
459
|
+
| Brute-force pas ralenti | `loginThrottler` absent du container — `rateLimit.enabled` off (`firewall.ts:617`) | Configurer `rateLimit` pour poser le throttler |
|
|
460
|
+
|
|
461
|
+
## 🧪 Tests & couverture
|
|
462
|
+
|
|
463
|
+
Trois familles couvrent la brique — les **chiffres exacts vivent dans la carte de l'aperçu**
|
|
464
|
+
(régénérée par `gen-counters.mjs` depuis vitest, jamais figée ici) :
|
|
465
|
+
|
|
466
|
+
- **unit** (`security/tests/unit/`) : la chaîne + les modes (`authenticators`), les défenses JWT
|
|
467
|
+
RFC 8725 (`jwt.attack`) et le pipeline JWT bout en bout (`jwtPipeline`), la clé API — flux,
|
|
468
|
+
service et forme/CRC (`apiKeyAuthenticator`, `apiKeyService`, `apiKeyFormat`), la session
|
|
469
|
+
(`sessionAuthenticator`), le backoff NIST (`loginThrottler`), le step-up 2FA (`mfaStepUp`) ;
|
|
470
|
+
- **intégration** (serveur réel, `@nodefony/http`) : le flux clé API de bout en bout
|
|
471
|
+
(`apikey-flow`), un JWT autorisé sur WebSocket (`ws-isgranted-jwt`) ;
|
|
472
|
+
- l'**émission** des jetons (keystore, tokenStore) est couverte sur la page
|
|
473
|
+
[tokens](./tokens.md) ; les bancs d'attaque transverses (csrf, cors) sur leurs pages.
|
|
474
|
+
|
|
475
|
+
Couverture : `npm run coverage` dans `@nodefony/security`.
|
|
476
|
+
|
|
477
|
+
## 🔗 Pour aller plus loin
|
|
478
|
+
|
|
479
|
+
- ⬆️ **Retour au hub** : [Sécurité — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
|
|
480
|
+
- 🧭 **Pages sœurs** : [Firewall](firewall.md) · [Jetons](tokens.md)
|
|
481
|
+
|
|
482
|
+
- Le firewall qui enchaîne les authenticators (zones, modes, en-têtes) → [firewall](./firewall.md)
|
|
483
|
+
- L'autorisation (voters, rôles, scopes) après l'authentification → [authorization](./authorization.md)
|
|
484
|
+
- Émission/révocation des jetons (keystore, tokenStore) → [tokens](./tokens.md)
|
|
485
|
+
- Les autres preuves — flux BFF, pas des authenticators de zone : [oauth2](./oauth2.md) ·
|
|
486
|
+
[webauthn](./webauthn.md) · [totp](./totp.md)
|
|
487
|
+
- Vue d'ensemble sécurité → [index](./index.md)
|