@nodefony/security 10.0.0-beta.2 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +48 -16
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +62 -24
- package/dist/nodefony/command/security-oauth-doctor.js +68 -0
- package/dist/nodefony/command/security-secrets.js +52 -29
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +10 -7
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +9 -14
- package/dist/nodefony/config/config.js +99 -8
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/contracts/index.js +1 -2
- package/dist/nodefony/errors/AccessDeniedError.js +1 -2
- package/dist/nodefony/errors/ApiKeyError.js +3 -2
- package/dist/nodefony/errors/AuthenticationError.js +1 -2
- package/dist/nodefony/errors/CsrfError.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +1 -2
- package/dist/nodefony/errors/SsrfError.js +1 -2
- package/dist/nodefony/errors/ThrottledError.js +1 -2
- package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
- package/dist/nodefony/errors/WebAuthnError.js +1 -2
- package/dist/nodefony/errors/index.js +2 -1
- package/dist/nodefony/keycloak/keycloakAdmin.js +254 -0
- package/dist/nodefony/keycloak/keycloakRealm.js +259 -0
- package/dist/nodefony/keycloak/keycloakRealmInput.js +123 -0
- package/dist/nodefony/keycloak/scaffold.js +231 -0
- package/dist/nodefony/keycloak/security-keycloak-realm.js +130 -0
- package/dist/nodefony/scaffold/contribute.js +19 -0
- package/dist/nodefony/service/accessTokenVerifier.js +101 -34
- package/dist/nodefony/service/apiKeys.js +45 -5
- package/dist/nodefony/service/auditService.js +4 -5
- package/dist/nodefony/service/authFlow.js +118 -16
- package/dist/nodefony/service/authorization.js +1 -2
- package/dist/nodefony/service/cors.js +1 -2
- package/dist/nodefony/service/csrf.js +1 -2
- package/dist/nodefony/service/firewall.js +116 -25
- package/dist/nodefony/service/keycloak.js +208 -0
- package/dist/nodefony/service/oauth2.js +403 -28
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +94 -27
- package/dist/nodefony/service/totp.js +55 -11
- package/dist/nodefony/service/webAuthn.js +24 -7
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +15 -5
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +71 -42
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -3
- package/dist/nodefony/src/admin/userRevocationCascade.js +148 -20
- package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
- package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
- package/dist/nodefony/src/audit/auditBridge.js +1 -2
- package/dist/nodefony/src/audit/auditFilters.js +1 -2
- package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
- package/dist/nodefony/src/audit/readAuditContext.js +1 -2
- package/dist/nodefony/src/audit/recordAudit.js +1 -2
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +14 -59
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +80 -19
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +92 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +14 -5
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +5 -12
- package/dist/nodefony/src/authenticator/bearer.js +1 -2
- package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
- package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +26 -0
- package/dist/nodefony/src/crypto/secretCipher.js +1 -2
- package/dist/nodefony/src/csp.js +1 -2
- package/dist/nodefony/src/csrfToken.js +1 -2
- package/dist/nodefony/src/entryPoint.js +38 -0
- package/dist/nodefony/src/net/ssrfGuard.js +1 -2
- package/dist/nodefony/src/net/transportFailure.js +57 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +39 -0
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +28 -12
- package/dist/nodefony/src/oauth/oauth2Client.js +61 -35
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +40 -8
- package/dist/nodefony/src/oauth/providerDiagnosis.js +282 -0
- package/dist/nodefony/src/oauth/providerRoles.js +78 -0
- package/dist/nodefony/src/oauth/providers/github.js +48 -12
- package/dist/nodefony/src/oauth/providers/oidc.js +90 -8
- package/dist/nodefony/src/openToSignup.js +46 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
- package/dist/nodefony/src/realtime/frameAuthorizer.js +1 -2
- package/dist/nodefony/src/rolesWithin.js +57 -0
- package/dist/nodefony/src/sessionAuthentication.js +25 -0
- package/dist/nodefony/src/sessionIdentity.js +29 -3
- package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
- package/dist/nodefony/src/token/AnonymousToken.js +1 -2
- package/dist/nodefony/src/token/JwtKeystore.js +89 -48
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +46 -16
- package/dist/nodefony/src/token/UserToken.js +1 -2
- package/dist/nodefony/src/token/jwtRuntime.js +1 -2
- package/dist/nodefony/src/token/scopeGrants.js +109 -0
- package/dist/nodefony/src/token/secretFile.js +60 -4
- package/dist/nodefony/src/token/tokenCriteria.js +1 -2
- package/dist/nodefony/src/token/tokenFilters.js +1 -2
- package/dist/nodefony/src/token/tokenSort.js +1 -2
- package/dist/nodefony/src/token/tokenStatus.js +1 -2
- package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
- package/dist/nodefony/src/totp/totpCipher.js +1 -2
- package/dist/nodefony/src/totp/totpCrypto.js +1 -2
- package/dist/nodefony/src/totp/totpOperations.js +1 -2
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +50 -0
- package/dist/nodefony/src/voter/RoleVoter.js +1 -2
- package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
- package/dist/nodefony/src/voter/voterRegistry.js +1 -2
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
- package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
- package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
- package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
- package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
- package/dist/nodefony/src/webhook/webhookSort.js +1 -2
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
- package/dist/types/index.d.ts +26 -6
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +16 -6
- package/dist/types/nodefony/command/security-oauth-doctor.d.ts +23 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +2 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +4 -3
- package/dist/types/nodefony/command/security-user-password.d.ts +4 -4
- package/dist/types/nodefony/config/config.d.ts +64 -1
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +1 -1
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +42 -1
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +48 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +22 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +4 -1
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +7 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +4 -1
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +6 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
- package/dist/types/nodefony/errors/InsecureTransportError.d.ts +20 -0
- package/dist/types/nodefony/errors/index.d.ts +1 -0
- package/dist/types/nodefony/keycloak/keycloakAdmin.d.ts +154 -0
- package/dist/types/nodefony/keycloak/keycloakRealm.d.ts +138 -0
- package/dist/types/nodefony/keycloak/keycloakRealmInput.d.ts +92 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +98 -0
- package/dist/types/nodefony/keycloak/security-keycloak-realm.d.ts +43 -0
- package/dist/types/nodefony/scaffold/contribute.d.ts +15 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- package/dist/types/nodefony/service/authFlow.d.ts +35 -3
- package/dist/types/nodefony/service/firewall.d.ts +17 -3
- package/dist/types/nodefony/service/keycloak.d.ts +115 -0
- package/dist/types/nodefony/service/oauth2.d.ts +145 -9
- package/dist/types/nodefony/service/tokenService.d.ts +7 -1
- package/dist/types/nodefony/service/totp.d.ts +28 -4
- package/dist/types/nodefony/service/webAuthn.d.ts +23 -1
- package/dist/types/nodefony/src/SecuredArea.d.ts +4 -0
- package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +72 -12
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -26
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +17 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +11 -3
- package/dist/types/nodefony/src/boot/publicRoutesNotice.d.ts +16 -0
- package/dist/types/nodefony/src/entryPoint.d.ts +37 -0
- package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/clientCredentials.d.ts +54 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- package/dist/types/nodefony/src/oauth/metadata.d.ts +24 -1
- package/dist/types/nodefony/src/oauth/oauth2Client.d.ts +50 -1
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +46 -2
- package/dist/types/nodefony/src/oauth/providerDiagnosis.d.ts +117 -0
- package/dist/types/nodefony/src/oauth/providerRoles.d.ts +58 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +34 -1
- package/dist/types/nodefony/src/openToSignup.d.ts +36 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
- package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
- package/dist/types/nodefony/src/sessionAuthentication.d.ts +14 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +47 -5
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +29 -2
- package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +22 -0
- package/dist/types/nodefony/src/transport/CredentialTransportPolicy.d.ts +47 -0
- package/docs/api-keys.md +182 -175
- package/docs/audit.md +141 -141
- package/docs/authenticators.md +129 -96
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +77 -72
- package/docs/external-jwt.md +83 -11
- package/docs/firewall.md +119 -83
- package/docs/headers.md +82 -82
- package/docs/index.md +16 -8
- package/docs/keycloak.md +835 -0
- package/docs/login-page.md +361 -0
- package/docs/oauth2.md +291 -166
- package/docs/obtenir-un-jeton.md +131 -34
- package/docs/tokens.md +197 -140
- package/docs/totp.md +177 -136
- package/docs/webauthn.md +157 -154
- package/docs/webhooks.md +128 -125
- package/keycloak/themes/nodefony/account/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/account/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/account/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/account/theme.properties +14 -0
- package/keycloak/themes/nodefony/admin/resources/css/nodefony.css +66 -0
- package/keycloak/themes/nodefony/admin/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/admin/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/admin/theme.properties +12 -0
- package/keycloak/themes/nodefony/email/html/template.ftl +73 -0
- package/keycloak/themes/nodefony/email/messages/messages_en.properties +2 -0
- package/keycloak/themes/nodefony/email/messages/messages_fr.properties +3 -0
- package/keycloak/themes/nodefony/email/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/email/theme.properties +8 -0
- package/keycloak/themes/nodefony/login/messages/messages_en.properties +22 -0
- package/keycloak/themes/nodefony/login/messages/messages_fr.properties +25 -0
- package/keycloak/themes/nodefony/login/resources/css/login.css +1141 -0
- package/keycloak/themes/nodefony/login/resources/css/nodefony.css +688 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/blueprint.css +70 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/console.css +57 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/dots.css +67 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/enterprise.css +84 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/horizon.css +158 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/ledger.css +224 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/minimal.css +63 -0
- package/keycloak/themes/nodefony/login/resources/css/skins/photo-card.css +72 -0
- package/keycloak/themes/nodefony/login/resources/img/favicon.ico +0 -0
- package/keycloak/themes/nodefony/login/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/login/template.ftl +423 -0
- package/keycloak/themes/nodefony/login/theme.properties +17 -0
- package/keycloak/themes/nodefony/welcome/resources/css/nodefony.css +32 -0
- package/keycloak/themes/nodefony/welcome/resources/img/logo.png +0 -0
- package/keycloak/themes/nodefony/welcome/theme.properties +11 -0
- package/keycloak/themes/nodefony-blueprint/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-console/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-dots/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-enterprise/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-horizon/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-ledger/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-minimal/login/theme.properties +12 -0
- package/keycloak/themes/nodefony-photo-card/login/theme.properties +12 -0
- package/package.json +19 -17
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/authenticators.md
CHANGED
|
@@ -49,7 +49,7 @@ flowchart TD
|
|
|
49
49
|
S --> CTRL["→ autorisation → contrôleur"]
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
C'est `Firewall.#authenticate()` (`firewall.ts
|
|
52
|
+
C'est `Firewall.#authenticate()` (`firewall.ts#Firewall.#authenticate`) qui déroule ce cycle pour chaque maillon de la
|
|
53
53
|
zone, dans l'ordre déclaré. Le succès pose l'identité dans l'ALS ; l'échec remonte au firewall qui
|
|
54
54
|
pose le 401 et son challenge — l'authenticator, lui, ne touche jamais à la réponse.
|
|
55
55
|
|
|
@@ -67,7 +67,7 @@ pose le 401 et son challenge — l'authenticator, lui, ne touche jamais à la r
|
|
|
67
67
|
| EdDSA | Algorithme de signature asymétrique (Ed25519) — le seul accepté par le vérificateur JWT. |
|
|
68
68
|
| CRC | Somme de contrôle embarquée dans une clé API — filtre les valeurs malformées avant la base. |
|
|
69
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
|
|
70
|
+
| Challenge | En-tête `WWW-Authenticate` renvoyé avec un 401 (RFC 9110 §11) indiquant comment s'authentifier. |
|
|
71
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
72
|
|
|
73
73
|
## Qu'est-ce qu'un authenticator — et quelle faille il ferme
|
|
@@ -90,33 +90,33 @@ classe d'attaque précise — détaillées brique par brique dans le catalogue :
|
|
|
90
90
|
|
|
91
91
|
### Le contrat `IAuthenticator`
|
|
92
92
|
|
|
93
|
-
Tout authenticator implémente le même cycle (`IAuthenticator.ts
|
|
93
|
+
Tout authenticator implémente le même cycle (`IAuthenticator.ts#IAuthenticator`), ce qui rend le firewall
|
|
94
94
|
totalement agnostique de la stratégie :
|
|
95
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
|
|
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#IAuthenticator.challenge`) — la valeur `WWW-Authenticate` (RFC 9110 §11) des 401 de la zone. |
|
|
104
104
|
|
|
105
105
|
### Le registre pluggable
|
|
106
106
|
|
|
107
107
|
Les authenticators sont résolus par **nom** : `Firewall.#instantiateAuthenticators()`
|
|
108
|
-
(`firewall.ts
|
|
108
|
+
(`firewall.ts#Firewall.#instantiateAuthenticators`) interroge `getAuthenticatorFactory()` (`authenticatorRegistry.ts#getAuthenticatorFactory`) — jamais
|
|
109
109
|
un `if (name === "jwt")` dans le firewall, qui trahirait la promesse « pluggable ».
|
|
110
110
|
|
|
111
111
|
- Les **six builtins HTTP** (`anonymous`, `userpassword`, `session`, `jwt`, `external-jwt`, `apikey`)
|
|
112
112
|
s'enregistrent à l'import du module via `registerAuthenticatorFactory()`
|
|
113
|
-
(`authenticatorRegistry.ts
|
|
113
|
+
(`authenticatorRegistry.ts#registerAuthenticatorFactory`) — donc toujours avant le boot.
|
|
114
114
|
- Le septiè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
|
|
115
|
+
lui-même au handshake WS des zones protégées (`Firewall.#wireRealtime()`, `firewall.ts#Firewall.#wireRealtime`).
|
|
116
116
|
- La fabrique ne fait que **construire** ; les résolutions de services coûteuses (`users`,
|
|
117
117
|
`tokenStore`, keystore) restent **lazy** dans l'instance (cold path).
|
|
118
118
|
- Un nom inconnu en config = boot **fail-closed** — `#configError` posé + log CRITIC
|
|
119
|
-
(`firewall.ts
|
|
119
|
+
(`firewall.ts#Firewall.#configError`) : jamais de zone « protégée » silencieusement ouverte à cause d'une
|
|
120
120
|
faute de frappe.
|
|
121
121
|
|
|
122
122
|
## 🚀 Démarrage rapide
|
|
@@ -124,7 +124,7 @@ un `if (name === "jwt")` dans le firewall, qui trahirait la promesse « pluggabl
|
|
|
124
124
|
### Une zone, trois preuves — la même API pour le web et les machines
|
|
125
125
|
|
|
126
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
|
|
127
|
+
nom**, validé Zod au boot (`areas: z.record(...)`, `config.ts#oauth2Schema`) :
|
|
128
128
|
|
|
129
129
|
```typescript
|
|
130
130
|
// nodefony.config.ts (extrait)
|
|
@@ -144,7 +144,7 @@ use("@nodefony/security", {
|
|
|
144
144
|
> [!IMPORTANT]
|
|
145
145
|
> **Le login est FOURNI** : `POST /nodefony/security/api/auth/login` (body `{ username, password }`
|
|
146
146
|
> → `Set-Cookie` de session, ID régénéré anti-fixation), avec `logout` et `me`
|
|
147
|
-
> (`SessionAuthController.ts
|
|
147
|
+
> (`SessionAuthController.ts#SessionAuthController`). Pas de LoginController à écrire — tes routes ne font que
|
|
148
148
|
> consommer l'identité.
|
|
149
149
|
|
|
150
150
|
### Ce que TU écris : le controller qui consomme l'identité
|
|
@@ -198,12 +198,12 @@ curl -s -H 'Authorization: Bearer nf_…' \
|
|
|
198
198
|
|
|
199
199
|
Requête par requête, qui répond :
|
|
200
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
|
|
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#Firewall.#authenticate`) |
|
|
207
207
|
|
|
208
208
|
## 🔐 Les authenticators intégrés
|
|
209
209
|
|
|
@@ -220,36 +220,40 @@ Requête par requête, qui répond :
|
|
|
220
220
|
### `anonymous` — accepter explicitement l'anonymat
|
|
221
221
|
|
|
222
222
|
Le seul authenticator autorisé à produire un token **non authentifié** sans déclencher le Zero Trust
|
|
223
|
-
(`AnonymousAuthenticator.ts
|
|
223
|
+
(`AnonymousAuthenticator.ts#AnonymousAuthenticator`).
|
|
224
224
|
|
|
225
|
-
- `supports()` accepte tout (`AnonymousAuthenticator.ts
|
|
226
|
-
`anonymousUser` — zéro allocation d'utilisateur (`AnonymousToken.ts
|
|
225
|
+
- `supports()` accepte tout (`AnonymousAuthenticator.ts#AnonymousAuthenticator.supports`) ; le token porte le **singleton gelé**
|
|
226
|
+
`anonymousUser` — zéro allocation d'utilisateur (`AnonymousToken.ts#AnonymousToken`).
|
|
227
227
|
- À ne lister **que volontairement** : `["jwt", "anonymous"]` en mode `first` signifie « identifié
|
|
228
228
|
si preuve présente, sinon visiteur anonyme accepté ». En mode `all`, utile en **dernier** :
|
|
229
229
|
« canal prouvé (ex. mTLS), identité utilisateur optionnelle ».
|
|
230
230
|
- Sans lui, zone protégée + aucune preuve = 401 : la défense en profondeur du firewall n'accepte un
|
|
231
|
-
token non authentifié que si `anonymous` est le maillon déclaré (`firewall.ts
|
|
231
|
+
token non authentifié que si `anonymous` est le maillon déclaré (`firewall.ts#Firewall.handleSecurity`).
|
|
232
232
|
- **Faille fermée** : l'anonymat _implicite_ — ici il est un choix explicite et auditable, jamais un
|
|
233
233
|
défaut.
|
|
234
234
|
|
|
235
235
|
### `userpassword` — HTTP Basic + backoff NIST
|
|
236
236
|
|
|
237
237
|
Schéma **HTTP Basic** (RFC 7617) : `Authorization: Basic base64(id:mdp)`, charset UTF-8, scheme
|
|
238
|
-
case-insensitive (`UserPasswordAuthenticator.ts
|
|
239
|
-
le mot de passe peut en contenir (`UserPasswordAuthenticator.ts
|
|
238
|
+
case-insensitive (`UserPasswordAuthenticator.ts#BASIC_SCHEME`) ; `createToken()` split au **premier** `:` —
|
|
239
|
+
le mot de passe peut en contenir (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.createToken`).
|
|
240
240
|
|
|
241
241
|
- **La vérification est déléguée** au `IPasswordVerifier` (le `UserService`) : hash, comparaison,
|
|
242
242
|
leurre anti-timing, re-hash transparent — l'authenticator ne voit que le verdict.
|
|
243
243
|
- **Message uniforme** `INVALID_CREDENTIALS` quelle que soit la cause — identifiant inconnu, compte
|
|
244
|
-
verrouillé, mot de passe faux (`UserPasswordAuthenticator.ts
|
|
244
|
+
verrouillé, mot de passe faux (`UserPasswordAuthenticator.ts#INVALID_CREDENTIALS`) → anti-énumération de comptes.
|
|
245
245
|
- **Throttling NIST SP 800-63B AVANT le verifier** : `#throttler.check()` sur l'identifiant saisi
|
|
246
|
-
(`UserPasswordAuthenticator.ts
|
|
246
|
+
(`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`) — un identifiant bloqué ne coûte **aucun hash** → le
|
|
247
247
|
throttle protège aussi le serveur du **DoS argon2**. Échec compté, succès remis à zéro
|
|
248
|
-
(`UserPasswordAuthenticator.ts
|
|
249
|
-
(`firewall.ts
|
|
248
|
+
(`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`). `ThrottledError` → **429 + `Retry-After`**
|
|
249
|
+
(`firewall.ts#Firewall.handleSecurity`).
|
|
250
250
|
- **Le throttler est PARTAGÉ** avec le login JSON du BFF — même `loginThrottler` du container : un
|
|
251
|
-
attaquant ne contourne pas le backoff en changeant de porte (`authenticatorRegistry.ts
|
|
252
|
-
-
|
|
251
|
+
attaquant ne contourne pas le backoff en changeant de porte (`authenticatorRegistry.ts`).
|
|
252
|
+
- **En production, un mot de passe reçu en clair est refusé avant d'être décodé** :
|
|
253
|
+
`createToken()` interroge la politique `credentialTransport`
|
|
254
|
+
(`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.createToken`) → **403**, sans challenge — un `Basic` ferait renvoyer le
|
|
255
|
+
mot de passe par le même canal (`firewall.ts#Firewall.handleSecurity`). Voir [Mot de passe en clair](#mot-de-passe-en-clair--refusé-en-production).
|
|
256
|
+
- Challenge : `Basic realm="nodefony", charset="UTF-8"` (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`).
|
|
253
257
|
- **Piège** : le login par formulaire (JSON) n'est **pas** ici — c'est le BFF
|
|
254
258
|
(`/nodefony/security/api/auth/login`). Basic sert l'outillage (scripts, CLI).
|
|
255
259
|
|
|
@@ -259,37 +263,37 @@ Après le login, chaque requête web prouve son identité par la **session serve
|
|
|
259
263
|
Credential = l'**identifiant** posé dans le blob de session, jamais un secret.
|
|
260
264
|
|
|
261
265
|
- **N'ouvre jamais la session lui-même** : `supports()` exige une session **déjà reprise** porteuse
|
|
262
|
-
d'un utilisateur (`SessionAuthenticator.ts
|
|
266
|
+
d'un utilisateur (`SessionAuthenticator.ts#SessionAuthenticator.supports`) — le pipeline http démarre la session _avant_
|
|
263
267
|
le firewall ; c'est `AuthFlow.login()` qui ouvre et régénère l'ID (anti-fixation).
|
|
264
268
|
- **L'identité est re-résolue à CHAQUE requête** via `resolveSessionIdentity`
|
|
265
|
-
(`SessionAuthenticator.ts
|
|
266
|
-
partagés avec `AuthFlow.me()` : `isLocked()`/`isActive()` → rejet (`sessionIdentity.ts
|
|
269
|
+
(`SessionAuthenticator.ts#SessionAuthenticator.authenticate`) → rôles frais, révocation immédiate. Les contrôles d'état sont
|
|
270
|
+
partagés avec `AuthFlow.me()` : `isLocked()`/`isActive()` → rejet (`sessionIdentity.ts#resolveSessionIdentity`).
|
|
267
271
|
- `onSuccess()` pose l'identifiant sur le contexte — la persistance de session lie le blob au
|
|
268
|
-
principal courant (`SessionAuthenticator.ts
|
|
272
|
+
principal courant (`SessionAuthenticator.ts#SessionAuthenticator.onSuccess`).
|
|
269
273
|
- **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login,
|
|
270
|
-
jamais une popup Basic (`SessionAuthenticator.ts
|
|
274
|
+
jamais une popup Basic (`SessionAuthenticator.ts#SessionAuthenticator`).
|
|
271
275
|
|
|
272
276
|
### `jwt` — Bearer signé pour les API (RFC 6750 + BCP RFC 8725)
|
|
273
277
|
|
|
274
278
|
Réservé aux **API service↔service / agents** (le web reste sur la session). Vérifie un access token
|
|
275
279
|
**EdDSA** signé par le keystore du serveur ; `supports()` ne réclame que la structure compacte
|
|
276
|
-
`a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts
|
|
280
|
+
`a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts#COMPACT_JWS`). Les défenses **dures** du JWT BCP, toutes
|
|
277
281
|
prouvées en test :
|
|
278
282
|
|
|
279
|
-
| Défense | Comment
|
|
280
|
-
| -------------------------------------- |
|
|
281
|
-
| **Allowlist d'algorithmes** | `algorithms: ["EdDSA"]` — jamais l'algo de l'en-tête du token (`JwtAuthenticator.ts
|
|
282
|
-
| **Clé par `kid` du keyset LOCAL** | `createLocalJWKSet` — jamais `jku`/`jwk` de l'en-tête (`JwtAuthenticator.ts
|
|
283
|
-
| **`aud` + `iss` + `typ` obligatoires** | `typ: "at+jwt"` (§3.11) sépare access et refresh (`JwtAuthenticator.ts
|
|
284
|
-
| **Révocation** | denylist `isJtiDenied` + seuil `invalidBefore` par porteur (`JwtAuthenticator.ts
|
|
285
|
-
| **Sujet revérifié** | `loadUserByIdentifier(sub)` → disparu/inactif/verrouillé = rejet (`JwtAuthenticator.ts
|
|
283
|
+
| Défense | Comment | Attaque fermée |
|
|
284
|
+
| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
285
|
+
| **Allowlist d'algorithmes** | `algorithms: ["EdDSA"]` — jamais l'algo de l'en-tête du token (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) | `alg=none`, algorithm confusion (§3.1) |
|
|
286
|
+
| **Clé par `kid` du keyset LOCAL** | `createLocalJWKSet` — jamais `jku`/`jwk` de l'en-tête (`JwtAuthenticator.ts#JwtAuthenticator.#ensureGetKey`) | injection de clé / SSRF (§3.5) |
|
|
287
|
+
| **`aud` + `iss` + `typ` obligatoires** | `typ: "at+jwt"` (§3.11) sépare access et refresh (`JwtAuthenticator.ts#JwtAuthenticator.verifyForResource`) | refresh présenté comme access, token d'un autre service (§3.8-3.9) |
|
|
288
|
+
| **Révocation** | denylist `isJtiDenied` + seuil `invalidBefore` par porteur (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) | jeton auto-porté volé, logout global |
|
|
289
|
+
| **Sujet revérifié** | `loadUserByIdentifier(sub)` → disparu/inactif/verrouillé = rejet (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) | compte banni encore « valide » via son token (§3.10) |
|
|
286
290
|
|
|
287
|
-
Le **message d'échec est uniforme** (`INVALID_TOKEN`, `JwtAuthenticator.ts
|
|
291
|
+
Le **message d'échec est uniforme** (`INVALID_TOKEN`, `JwtAuthenticator.ts#INVALID_TOKEN`) : la cause fine
|
|
288
292
|
(expiré, `aud`, signature, sujet banni) part en **audit**, jamais au client — anti-oracle. Le token
|
|
289
|
-
promu porte `scopes`, `jti`, `claims` en attributs (`JwtAuthenticator.ts
|
|
290
|
-
importé **lazy** — dépendance lourde (`JwtAuthenticator.ts
|
|
293
|
+
promu porte `scopes`, `jti`, `claims` en attributs (`JwtAuthenticator.ts#JwtAuthenticator.#verify`). `jose` est
|
|
294
|
+
importé **lazy** — dépendance lourde (`JwtAuthenticator.ts#JwtAuthenticator.#jose`).
|
|
291
295
|
|
|
292
|
-
Les six premiers se déclarent dans une zone (registre `authenticatorRegistry.ts
|
|
296
|
+
Les six premiers se déclarent dans une zone (registre `authenticatorRegistry.ts`) ;
|
|
293
297
|
`firewall-realtime` ne se déclare pas — le firewall le câble lui-même au handshake WebSocket.
|
|
294
298
|
|
|
295
299
|
### `external-jwt` — un jeton émis par un serveur d'autorisation TIERS
|
|
@@ -306,17 +310,17 @@ Clé API personnelle en `Authorization: Bearer nf_…` (préfixe `apiKeys.prefix
|
|
|
306
310
|
Contrairement au JWT (auto-porté), un PAT est un **bearer opaque** dont la vérité vit **côté
|
|
307
311
|
serveur** (`ITokenStore`) → **révocable immédiatement**. Défenses :
|
|
308
312
|
|
|
309
|
-
- **Forme + CRC validés AVANT tout accès au store** (`parseApiKey`, `ApiKeyAuthenticator.ts
|
|
313
|
+
- **Forme + CRC validés AVANT tout accès au store** (`parseApiKey`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`)
|
|
310
314
|
→ une valeur malformée n'atteint jamais la base (**anti-DoS**).
|
|
311
|
-
- **Lookup par hash SHA-256** (`findByHash`, `ApiKeyAuthenticator.ts
|
|
315
|
+
- **Lookup par hash SHA-256** (`findByHash`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) — le secret n'existe
|
|
312
316
|
**nulle part au repos**.
|
|
313
|
-
- **Révocation** (`revokedAt`) + **expiration** (`expiresAt`) (`ApiKeyAuthenticator.ts
|
|
314
|
-
**ban en masse** du porteur (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts
|
|
315
|
-
- **Sujet revérifié** à chaque requête → rôles frais (`ApiKeyAuthenticator.ts
|
|
317
|
+
- **Révocation** (`revokedAt`) + **expiration** (`expiresAt`) (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) +
|
|
318
|
+
**ban en masse** du porteur (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
|
|
319
|
+
- **Sujet revérifié** à chaque requête → rôles frais (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
|
|
316
320
|
- **`lastUsedAt` throttlé** : aucune écriture sur le hot path tant que la fenêtre
|
|
317
|
-
`apiKeys.lastUsedThrottleS` n'est pas dépassée (`ApiKeyAuthenticator.ts
|
|
321
|
+
`apiKeys.lastUsedThrottleS` n'est pas dépassée (`ApiKeyAuthenticator.ts#IApiKeyAuthenticatorRuntime.lastUsedThrottleS`).
|
|
318
322
|
|
|
319
|
-
Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts
|
|
323
|
+
Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
|
|
320
324
|
|
|
321
325
|
### `firewall-realtime` — la promotion, en WebSocket, de l'identité déjà posée
|
|
322
326
|
|
|
@@ -325,27 +329,27 @@ Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:1
|
|
|
325
329
|
> a résolue — y compris un agent authentifié par jeton porteur, sans cookie ni session. Son nom
|
|
326
330
|
> d'origine (`SessionRealtimeAuthenticator`) décrivait le premier mode branché, pas son rôle ; la
|
|
327
331
|
> confusion a coûté un durcissement pensé pour la session appliqué à toutes les identités
|
|
328
|
-
> (`FirewallRealtimeAuthenticator.ts
|
|
332
|
+
> (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`).
|
|
329
333
|
|
|
330
334
|
Sur un handshake WS (une requête upgrade HTTP qui traverse **le même pipeline**), `startSession`
|
|
331
335
|
puis `handleSecurity` ont **déjà** tourné : session chargée, identité re-résolue, `IUser` posé dans
|
|
332
336
|
l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
|
|
333
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
337
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.supports`).
|
|
334
338
|
|
|
335
339
|
- **Perf : il ne relit pas la base.** `authenticate()` réutilise l'identité de l'ALS
|
|
336
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
340
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`) au lieu de refaire deux lectures base par connexion —
|
|
337
341
|
un coût évitable sur le différenciateur temps réel.
|
|
338
|
-
- **Câblé automatiquement** par `Firewall.#wireRealtime()` (`firewall.ts
|
|
339
|
-
protégée `realtime !== false` — une instance par zone au handshake (`firewall.ts
|
|
342
|
+
- **Câblé automatiquement** par `Firewall.#wireRealtime()` (`firewall.ts#Firewall.#wireRealtime`) pour toute zone
|
|
343
|
+
protégée `realtime !== false` — une instance par zone au handshake (`firewall.ts#Firewall.#build`).
|
|
340
344
|
- **Le mode de preuve suit le jeton du firewall**, il n'est pas deviné : absent (zone historique),
|
|
341
|
-
on retombe sur le mode le plus strict, la session (`FirewallRealtimeAuthenticator.ts
|
|
345
|
+
on retombe sur le mode le plus strict, la session (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`).
|
|
342
346
|
- **Filet Zero Trust** : il câble un revalidateur appelé avant chaque action data plane
|
|
343
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
347
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`) — la socket peut survivre à l'identité qui l'a
|
|
344
348
|
ouverte (logout, changement de compte, `jti` denylisté). En mode session,
|
|
345
349
|
`buildSessionRevalidator()` re-lit `storage.read(id)` et compare l'identifiant ; toute erreur de
|
|
346
|
-
lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts
|
|
350
|
+
lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts#SessionLike.storage`). En mode jeton porteur, la
|
|
347
351
|
preuve est autre : `exp`, `jti` denylisté, `invalidBefore`
|
|
348
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
352
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.#buildBearerRevalidator`).
|
|
349
353
|
|
|
350
354
|
> [!NOTE]
|
|
351
355
|
> **Asymétrie de révocation HTTP↔WS (assumée)** : le jeton realtime est figé au handshake (les
|
|
@@ -356,7 +360,7 @@ l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
|
|
|
356
360
|
## ⚙️ Composer une zone — ordre, mode, cohabitation
|
|
357
361
|
|
|
358
362
|
La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
|
|
359
|
-
(`firewall.ts
|
|
363
|
+
(`firewall.ts#Firewall.#authenticate`) selon le `mode` de la zone (`first` par défaut, `config.ts#areaSchema`).
|
|
360
364
|
|
|
361
365
|
### Situation 1 — humains ET machines sur la même API (`first`)
|
|
362
366
|
|
|
@@ -365,12 +369,12 @@ Deux preuves différentes, mêmes routes — c'est la config du Démarrage rapid
|
|
|
365
369
|
de lecture :
|
|
366
370
|
|
|
367
371
|
- un maillon dont `supports()` est faux est simplement **sauté** en mode `first`
|
|
368
|
-
(`firewall.ts
|
|
372
|
+
(`firewall.ts#Firewall.#rebuildSecurityHeaders`) ;
|
|
369
373
|
- un credential **présenté mais invalide échoue immédiatement** — l'échec d'`authenticate()`
|
|
370
|
-
remonte, jamais de fallback silencieux vers le maillon suivant (`firewall.ts
|
|
374
|
+
remonte, jamais de fallback silencieux vers le maillon suivant (`firewall.ts#Firewall.#authenticate`). Une clé
|
|
371
375
|
API révoquée donne un 401 direct, même si un autre maillon aurait pu réussir.
|
|
372
376
|
- aucune preuve présentée sur toute la chaîne → `handleSecurity()` lève l'`AuthenticationError`
|
|
373
|
-
Zero Trust (`firewall.ts
|
|
377
|
+
Zero Trust (`firewall.ts#Firewall.handleSecurity`).
|
|
374
378
|
|
|
375
379
|
### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
|
|
376
380
|
|
|
@@ -389,15 +393,15 @@ authenticators: ["anonymous", "session"], // ❌ anonymous accepte TOUT le mon
|
|
|
389
393
|
### Situation 3 — empiler les preuves (`all`)
|
|
390
394
|
|
|
391
395
|
En mode `all`, **chaque** maillon est obligatoire : `supports()` faux = 401 immédiat
|
|
392
|
-
(`firewall.ts
|
|
396
|
+
(`firewall.ts#Firewall.applySecurityHeaders`) et le **dernier** token de la chaîne porte l'identité (`firewall.ts#Firewall.unregisterCspOrigins`) —
|
|
393
397
|
utile pour exiger une preuve de canal (mTLS) **et** une identité, ou un « sudo mode » session +
|
|
394
398
|
re-saisie du mot de passe. Scénario complet côté zones : [firewall](./firewall.md).
|
|
395
399
|
|
|
396
400
|
### Cohabitation JWT + clé API dans une même zone
|
|
397
401
|
|
|
398
402
|
Les deux sont des `Bearer`, mais Nodefony les **discrimine sur la forme** — un JWT a la structure
|
|
399
|
-
compacte `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts
|
|
400
|
-
point (`looksLikeApiKey`, `ApiKeyAuthenticator.ts
|
|
403
|
+
compacte `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts#COMPACT_JWS`), un PAT porte le préfixe `nf_` sans
|
|
404
|
+
point (`looksLikeApiKey`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.constructor`). Chaque `supports()` ne réclame que _son_
|
|
401
405
|
format → aucun conflit, aucune double vérification.
|
|
402
406
|
|
|
403
407
|
## Le fil rouge : le message d'échec uniforme
|
|
@@ -409,7 +413,35 @@ paresse : c'est une **défense anti-énumération / anti-oracle**.
|
|
|
409
413
|
Distinguer « compte inconnu » de « mot de passe faux », ou « token expiré » de « signature
|
|
410
414
|
invalide », donnerait à un attaquant une sonde. La cause fine part **toujours** en log d'audit ; le
|
|
411
415
|
client n'obtient qu'un 401 + son challenge — posé par le firewall, premier maillon de la zone qui
|
|
412
|
-
en déclare un (`Firewall.#setChallenge()`, `firewall.ts
|
|
416
|
+
en déclare un (`Firewall.#setChallenge()`, `firewall.ts#Firewall.#setChallenge`).
|
|
417
|
+
|
|
418
|
+
## Mot de passe en clair — refusé en production
|
|
419
|
+
|
|
420
|
+
Un mot de passe, ou un code de second facteur, qui arrive sur une requête HTTP en clair a déjà
|
|
421
|
+
traversé le réseau en lisible : le vérifier ne le protège plus. En production, toutes les
|
|
422
|
+
portes qui reçoivent un secret le refusent donc **avant toute vérification** — formulaire de
|
|
423
|
+
session, second facteur, Basic, émission de jeton (`/nodefony/security/api/token`), et les
|
|
424
|
+
endpoints d'administration marqués `credentials: true` (création d'un compte, changement de mot
|
|
425
|
+
de passe — cf la doc du plan d'administration de `@nodefony/framework`) :
|
|
426
|
+
|
|
427
|
+
- **403**, message constant `Credentials must be sent over HTTPS`
|
|
428
|
+
(`INSECURE_TRANSPORT_MESSAGE`, exporté par `nodefony`) ; la page de connexion le reconnaît et
|
|
429
|
+
n'affiche pas « mot de passe incorrect ».
|
|
430
|
+
- Le refus précède le throttle et le verifier : aucun hash calculé, aucun échec compté. Il est
|
|
431
|
+
audité (`reason: "insecure_transport"`).
|
|
432
|
+
- Le **premier** refus du process part en WARNING, les suivants non : un pod mal configuré
|
|
433
|
+
refuserait chaque connexion (`#warned`, `CredentialTransportPolicy.ts#CredentialTransportPolicy.#warned`).
|
|
434
|
+
- **La décision se prend sur le scheme EFFECTIF** (`context.scheme`), pas sur le transport du
|
|
435
|
+
pod. Derrière un proxy qui termine TLS — le cas de la plupart des déploiements — il suffit de
|
|
436
|
+
déclarer ce proxy de confiance : `trustProxy` dans la config de `@nodefony/http`, pour que
|
|
437
|
+
`X-Forwarded-Proto: https` soit lu.
|
|
438
|
+
- Une seule décision, posée au conteneur par le firewall au boot (`credentialTransport`,
|
|
439
|
+
`firewall.ts#Firewall.#provisionSharedServices`) : production ⇒ refus, sauf échappement **écrit**
|
|
440
|
+
`security.allowInsecureCredentials: true` (`CredentialTransportPolicy.ts#shouldEnforceCredentialTransport`). Hors
|
|
441
|
+
production, rien n'est refusé.
|
|
442
|
+
|
|
443
|
+
Pourquoi un 403 et pas un 401 : aucune preuve n'a été jugée. Le client ne corrigera rien dans
|
|
444
|
+
son secret — c'est le canal qu'il doit changer.
|
|
413
445
|
|
|
414
446
|
## 🧩 Ajouter un authenticator maison
|
|
415
447
|
|
|
@@ -424,38 +456,38 @@ registerAuthenticatorFactory("ldap", ({ container, config }) => {
|
|
|
424
456
|
|
|
425
457
|
À faire au chargement du module (avant le boot). Trois règles, calquées sur les builtins :
|
|
426
458
|
|
|
427
|
-
- implémenter le contrat `IAuthenticator` (`IAuthenticator.ts
|
|
459
|
+
- implémenter le contrat `IAuthenticator` (`IAuthenticator.ts#IAuthenticator`) — `challenge()` seulement si
|
|
428
460
|
un en-tête `WWW-Authenticate` a du sens pour la stratégie ;
|
|
429
461
|
- renvoyer le **message uniforme** en cas d'échec (la cause fine part en audit) ;
|
|
430
462
|
- laisser les résolutions de services **lazy** dans l'instance — la fabrique ne fait que
|
|
431
|
-
construire (`authenticatorRegistry.ts
|
|
463
|
+
construire (`authenticatorRegistry.ts#IAuthenticatorFactoryContext`).
|
|
432
464
|
|
|
433
465
|
## 📜 Normes appliquées
|
|
434
466
|
|
|
435
467
|
<!-- prettier-ignore -->
|
|
436
468
|
| Domaine | Norme | Ancrage |
|
|
437
469
|
| --- | --- | --- |
|
|
438
|
-
| Challenge d'auth (401) | RFC
|
|
439
|
-
| Bearer | RFC 6750 | `readBearerHeader()` (`runtime/bearer.ts
|
|
440
|
-
| JWT (BCP) | RFC 7519, 8725 | `jwtVerify` durci : allowlist + claims (`JwtAuthenticator.ts
|
|
441
|
-
| HTTP Basic | RFC 7617 | `UserPasswordAuthenticator` (`UserPasswordAuthenticator.ts
|
|
442
|
-
| Backoff de login | NIST SP 800-63B | `#throttler.check()` avant le verifier (`UserPasswordAuthenticator.ts
|
|
443
|
-
| Rate limit (429) | RFC 6585 | `Retry-After` posé par le firewall (`firewall.ts
|
|
444
|
-
| Anti-énumération | OWASP | `INVALID_TOKEN` (`JwtAuthenticator.ts
|
|
470
|
+
| Challenge d'auth (401) | RFC 9110 §11 | `Firewall.#setChallenge()` (`firewall.ts#Firewall.#setChallenge`) |
|
|
471
|
+
| Bearer | RFC 6750 | `readBearerHeader()` (`runtime/bearer.ts#readBearerHeader`, cœur) — une porte UNIQUE au cœur, plus une constante par authenticator |
|
|
472
|
+
| JWT (BCP) | RFC 7519, 8725 | `jwtVerify` durci : allowlist + claims (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) |
|
|
473
|
+
| HTTP Basic | RFC 7617 | `UserPasswordAuthenticator` (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator`) |
|
|
474
|
+
| Backoff de login | NIST SP 800-63B | `#throttler.check()` avant le verifier (`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.authenticate`) |
|
|
475
|
+
| Rate limit (429) | RFC 6585 | `Retry-After` posé par le firewall (`firewall.ts#Firewall.handleSecurity`) |
|
|
476
|
+
| Anti-énumération | OWASP | `INVALID_TOKEN` (`JwtAuthenticator.ts#INVALID_TOKEN`) · `INVALID_CREDENTIALS` (`UserPasswordAuthenticator.ts#INVALID_CREDENTIALS`) |
|
|
445
477
|
|
|
446
478
|
## ⚡ Performance & mémoire
|
|
447
479
|
|
|
448
480
|
- `supports()` est un test **bon marché** (en-tête + regex) — et n'est payé que sur zone protégée
|
|
449
481
|
(le chemin chaud/froid vit dans le [firewall](./firewall.md)).
|
|
450
482
|
- Résolutions **lazy** : le verifier `#resolveVerifier` est mémoïsé au premier login
|
|
451
|
-
(`UserPasswordAuthenticator.ts
|
|
452
|
-
container au premier usage ; `jose` est importé lazy (`JwtAuthenticator.ts
|
|
483
|
+
(`UserPasswordAuthenticator.ts#UserPasswordAuthenticator.#resolveVerifier`) ; keystore, `tokenStore` et `users` sont résolus du
|
|
484
|
+
container au premier usage ; `jose` est importé lazy (`JwtAuthenticator.ts#JwtAuthenticator.#jose`).
|
|
453
485
|
- `anonymous` : singleton gelé `anonymousUser`, zéro allocation d'utilisateur
|
|
454
|
-
(`AnonymousToken.ts
|
|
486
|
+
(`AnonymousToken.ts#AnonymousToken`).
|
|
455
487
|
- `apikey` : écriture `lastUsedAt` **coalescée** — pas une écriture par requête
|
|
456
|
-
(`ApiKeyAuthenticator.ts
|
|
488
|
+
(`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
|
|
457
489
|
- `firewall-realtime` : **zéro lecture base** au handshake — réutilise l'ALS
|
|
458
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
490
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`).
|
|
459
491
|
- Le throttle NIST **avant** le hash : un 429 ne coûte aucun argon2.
|
|
460
492
|
|
|
461
493
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
@@ -463,12 +495,13 @@ registerAuthenticatorFactory("ldap", ({ container, config }) => {
|
|
|
463
495
|
<!-- prettier-ignore -->
|
|
464
496
|
| Symptôme | Cause (dans le code) | Correction |
|
|
465
497
|
| --- | --- | --- |
|
|
466
|
-
| 401 systématique sur une zone protégée | Aucune preuve + `anonymous` non listé — Zero Trust (`firewall.ts
|
|
467
|
-
| 401 générique + log ERROR « service `users` » | Câblage : pas de `UserService` au container (`authenticatorRegistry.ts
|
|
468
|
-
| JWT rejeté alors qu'il « semble » valide | `aud`/`iss`/`typ` non conformes, ou `alg` ≠ EdDSA (`JwtAuthenticator.ts
|
|
469
|
-
| Clé API révoquée encore acceptée quelques secondes | Confusion avec un JWT (auto-porté) — `revokedAt` est lu à chaque requête (`ApiKeyAuthenticator.ts
|
|
470
|
-
| Révocation WS pas immédiate | Identité figée au handshake — asymétrie assumée du `FirewallRealtimeAuthenticator` (`FirewallRealtimeAuthenticator.ts
|
|
471
|
-
| Brute-force pas ralenti | `loginThrottler` absent du container — `rateLimit.enabled` off (`firewall.ts
|
|
498
|
+
| 401 systématique sur une zone protégée | Aucune preuve + `anonymous` non listé — Zero Trust (`firewall.ts#Firewall.handleSecurity`) | Ajouter `anonymous` en dernier si l'anonymat est voulu |
|
|
499
|
+
| 401 générique + log ERROR « service `users` » | Câblage : pas de `UserService` au container (`authenticatorRegistry.ts`) | Enregistrer un `UserService` au boot de l'app |
|
|
500
|
+
| JWT rejeté alors qu'il « semble » valide | `aud`/`iss`/`typ` non conformes, ou `alg` ≠ EdDSA (`JwtAuthenticator.ts#JwtAuthenticator.#verify`) | Émettre via le `TokenService` (mêmes iss/aud/typ) |
|
|
501
|
+
| Clé API révoquée encore acceptée quelques secondes | Confusion avec un JWT (auto-porté) — `revokedAt` est lu à chaque requête (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) | Un PAT est révoqué immédiatement ; vérifier `revokedAt` |
|
|
502
|
+
| Révocation WS pas immédiate | Identité figée au handshake — asymétrie assumée du `FirewallRealtimeAuthenticator` (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`) | Attendre la fenêtre de re-validation, ou utiliser JWT + canal révocation |
|
|
503
|
+
| Brute-force pas ralenti | `loginThrottler` absent du container — `rateLimit.enabled` off (`firewall.ts#Firewall.#describeDefenses`) | Configurer `rateLimit` pour poser le throttler |
|
|
504
|
+
| 403 `Credentials must be sent over HTTPS` sur TOUTES les connexions en production | Pod en HTTP derrière un proxy TLS non déclaré de confiance : `context.scheme` reste `http` (`CredentialTransportPolicy.ts#shouldEnforceCredentialTransport`) | Déclarer le proxy dans `trustProxy` (`@nodefony/http`) ; `allowInsecureCredentials` seulement si le clair est un choix assumé |
|
|
472
505
|
|
|
473
506
|
## 🧪 Tests & couverture
|
|
474
507
|
|