@nodefony/security 10.0.0-beta.3 → 10.0.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +6 -8
- package/dist/nodefony/command/{security-token.js → security-mcp-token.js} +18 -11
- package/dist/nodefony/command/security-oauth-doctor.js +5 -3
- package/dist/nodefony/command/security-secrets.js +10 -4
- package/dist/nodefony/command/security-user-add.js +9 -3
- package/dist/nodefony/command/security-user-delete.js +5 -3
- package/dist/nodefony/command/security-user-list.js +5 -3
- package/dist/nodefony/command/security-user-password.js +5 -3
- package/dist/nodefony/config/config.js +20 -4
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/contracts/IAccessVoter.js +1 -2
- package/dist/nodefony/errors/AccessDeniedError.js +1 -2
- package/dist/nodefony/errors/ApiKeyError.js +3 -2
- package/dist/nodefony/errors/AuthenticationError.js +1 -2
- package/dist/nodefony/errors/CsrfError.js +1 -2
- package/dist/nodefony/errors/InsecureTransportError.js +1 -2
- package/dist/nodefony/errors/InvalidTargetError.js +1 -2
- package/dist/nodefony/errors/SsrfError.js +1 -2
- package/dist/nodefony/errors/ThrottledError.js +1 -2
- package/dist/nodefony/errors/UnverifiableTokenError.js +1 -2
- package/dist/nodefony/errors/WebAuthnError.js +1 -2
- package/dist/nodefony/keycloak/keycloakAdmin.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealm.js +1 -2
- package/dist/nodefony/keycloak/keycloakRealmInput.js +1 -2
- package/dist/nodefony/keycloak/scaffold.js +3 -3
- package/dist/nodefony/keycloak/security-keycloak-realm.js +5 -3
- package/dist/nodefony/scaffold/contribute.js +1 -2
- package/dist/nodefony/service/accessTokenVerifier.js +101 -34
- package/dist/nodefony/service/apiKeys.js +45 -5
- package/dist/nodefony/service/auditService.js +4 -5
- package/dist/nodefony/service/authFlow.js +10 -8
- package/dist/nodefony/service/authorization.js +1 -2
- package/dist/nodefony/service/cors.js +1 -2
- package/dist/nodefony/service/csrf.js +1 -2
- package/dist/nodefony/service/firewall.js +5 -4
- package/dist/nodefony/service/keycloak.js +1 -2
- package/dist/nodefony/service/oauth2.js +29 -7
- package/dist/nodefony/service/securityHeaders.js +1 -2
- package/dist/nodefony/service/tokenService.js +38 -22
- package/dist/nodefony/service/totp.js +4 -5
- package/dist/nodefony/service/webAuthn.js +4 -5
- package/dist/nodefony/service/webhooks.js +4 -5
- package/dist/nodefony/src/RoleHierarchyWalker.js +1 -2
- package/dist/nodefony/src/SecuredArea.js +1 -2
- package/dist/nodefony/src/accountBinding.js +96 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +1 -2
- package/dist/nodefony/src/admin/WebhookAdminApi.js +23 -7
- package/dist/nodefony/src/admin/adminAudit.js +1 -2
- package/dist/nodefony/src/admin/userRevocationCascade.js +79 -15
- package/dist/nodefony/src/apikey/apiKeyFormat.js +1 -2
- package/dist/nodefony/src/audit/MemoryAuditStore.js +1 -2
- package/dist/nodefony/src/audit/auditBridge.js +1 -2
- package/dist/nodefony/src/audit/auditFilters.js +1 -2
- package/dist/nodefony/src/audit/auditStoreRegistry.js +1 -2
- package/dist/nodefony/src/audit/readAuditContext.js +1 -2
- package/dist/nodefony/src/audit/recordAudit.js +1 -2
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +3 -2
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +8 -113
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +180 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +42 -4
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +91 -16
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +9 -5
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +1 -2
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +3 -17
- package/dist/nodefony/src/authenticator/externalSubject.js +1 -2
- package/dist/nodefony/src/authenticator/peekIssuer.js +1 -2
- package/dist/nodefony/src/boot/publicRoutesNotice.js +1 -2
- package/dist/nodefony/src/crypto/secretCipher.js +1 -2
- package/dist/nodefony/src/csp.js +1 -2
- package/dist/nodefony/src/csrfToken.js +1 -2
- package/dist/nodefony/src/entryPoint.js +1 -2
- package/dist/nodefony/src/net/ssrfGuard.js +1 -2
- package/dist/nodefony/src/net/transportFailure.js +57 -0
- package/dist/nodefony/src/oauth/clientCredentials.js +1 -2
- package/dist/nodefony/src/oauth/httpJson.js +3 -3
- package/dist/nodefony/src/oauth/metadata.js +5 -16
- package/dist/nodefony/src/oauth/oauth2Client.js +1 -2
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +1 -2
- package/dist/nodefony/src/oauth/providerDiagnosis.js +5 -6
- package/dist/nodefony/src/oauth/providerRoles.js +1 -2
- package/dist/nodefony/src/oauth/providers/github.js +16 -6
- package/dist/nodefony/src/oauth/providers/oidc.js +9 -6
- package/dist/nodefony/src/openToSignup.js +1 -2
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +17 -3
- package/dist/nodefony/src/realtime/frameAuthorizer.js +2 -3
- package/dist/nodefony/src/rolesWithin.js +57 -0
- package/dist/nodefony/src/sessionAuthentication.js +1 -2
- package/dist/nodefony/src/sessionIdentity.js +29 -3
- package/dist/nodefony/src/throttle/LoginThrottler.js +1 -2
- package/dist/nodefony/src/token/AnonymousToken.js +1 -2
- package/dist/nodefony/src/token/JwtKeystore.js +14 -5
- package/dist/nodefony/src/token/MemoryTokenStore.js +1 -2
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +6 -3
- package/dist/nodefony/src/token/UserToken.js +1 -2
- package/dist/nodefony/src/token/jwtRuntime.js +1 -2
- package/dist/nodefony/src/token/scopeGrants.js +109 -0
- package/dist/nodefony/src/token/secretFile.js +1 -2
- package/dist/nodefony/src/token/tokenCriteria.js +1 -2
- package/dist/nodefony/src/token/tokenFilters.js +1 -2
- package/dist/nodefony/src/token/tokenSort.js +1 -2
- package/dist/nodefony/src/token/tokenStatus.js +1 -2
- package/dist/nodefony/src/token/tokenStoreRegistry.js +1 -2
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +1 -2
- package/dist/nodefony/src/totp/totpCipher.js +1 -2
- package/dist/nodefony/src/totp/totpCrypto.js +1 -2
- package/dist/nodefony/src/totp/totpOperations.js +1 -2
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +1 -2
- package/dist/nodefony/src/transport/CredentialTransportPolicy.js +1 -2
- package/dist/nodefony/src/voter/RoleVoter.js +1 -2
- package/dist/nodefony/src/voter/ScopeVoter.js +1 -2
- package/dist/nodefony/src/voter/voterRegistry.js +1 -2
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +1 -2
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +1 -2
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +1 -2
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +1 -2
- package/dist/nodefony/src/webhook/webhookCipher.js +1 -2
- package/dist/nodefony/src/webhook/webhookDelivery.js +1 -2
- package/dist/nodefony/src/webhook/webhookFilters.js +1 -2
- package/dist/nodefony/src/webhook/webhookSignature.js +1 -2
- package/dist/nodefony/src/webhook/webhookSort.js +1 -2
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +1 -2
- package/dist/nodefony/testing/index.js +108 -0
- package/dist/types/nodefony/command/{security-token.d.ts → security-mcp-token.d.ts} +7 -6
- package/dist/types/nodefony/config/config.d.ts +1 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +3 -1
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +35 -14
- package/dist/types/nodefony/service/apiKeys.d.ts +13 -2
- package/dist/types/nodefony/service/oauth2.d.ts +20 -0
- package/dist/types/nodefony/src/accountBinding.d.ts +82 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +23 -5
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +2 -44
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +99 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +4 -1
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +37 -1
- package/dist/types/nodefony/src/net/transportFailure.d.ts +22 -0
- package/dist/types/nodefony/src/oauth/httpJson.d.ts +0 -8
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +7 -1
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +2 -2
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +6 -0
- package/dist/types/nodefony/src/rolesWithin.d.ts +27 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +22 -0
- package/dist/types/nodefony/src/token/scopeGrants.d.ts +77 -0
- package/dist/types/nodefony/testing/index.d.ts +51 -0
- package/docs/api-keys.md +181 -174
- package/docs/audit.md +140 -140
- package/docs/authenticators.md +101 -101
- package/docs/authorization.md +116 -69
- package/docs/cors.md +64 -64
- package/docs/csrf.md +74 -71
- package/docs/external-jwt.md +22 -12
- package/docs/firewall.md +85 -82
- package/docs/headers.md +81 -81
- package/docs/index.md +4 -4
- package/docs/keycloak.md +95 -78
- package/docs/login-page.md +23 -23
- package/docs/oauth2.md +157 -146
- package/docs/obtenir-un-jeton.md +116 -26
- package/docs/tokens.md +128 -122
- package/docs/totp.md +136 -136
- package/docs/webauthn.md +153 -153
- package/docs/webhooks.md +136 -124
- package/package.json +19 -13
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
package/docs/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
|
|
|
@@ -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,40 +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
|
|
251
|
+
attaquant ne contourne pas le backoff en changeant de porte (`authenticatorRegistry.ts`).
|
|
252
252
|
- **En production, un mot de passe reçu en clair est refusé avant d'être décodé** :
|
|
253
253
|
`createToken()` interroge la politique `credentialTransport`
|
|
254
|
-
(`UserPasswordAuthenticator.ts
|
|
255
|
-
mot de passe par le même canal (`firewall.ts
|
|
256
|
-
- Challenge : `Basic realm="nodefony", charset="UTF-8"` (`UserPasswordAuthenticator.ts
|
|
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`).
|
|
257
257
|
- **Piège** : le login par formulaire (JSON) n'est **pas** ici — c'est le BFF
|
|
258
258
|
(`/nodefony/security/api/auth/login`). Basic sert l'outillage (scripts, CLI).
|
|
259
259
|
|
|
@@ -263,37 +263,37 @@ Après le login, chaque requête web prouve son identité par la **session serve
|
|
|
263
263
|
Credential = l'**identifiant** posé dans le blob de session, jamais un secret.
|
|
264
264
|
|
|
265
265
|
- **N'ouvre jamais la session lui-même** : `supports()` exige une session **déjà reprise** porteuse
|
|
266
|
-
d'un utilisateur (`SessionAuthenticator.ts
|
|
266
|
+
d'un utilisateur (`SessionAuthenticator.ts#SessionAuthenticator.supports`) — le pipeline http démarre la session _avant_
|
|
267
267
|
le firewall ; c'est `AuthFlow.login()` qui ouvre et régénère l'ID (anti-fixation).
|
|
268
268
|
- **L'identité est re-résolue à CHAQUE requête** via `resolveSessionIdentity`
|
|
269
|
-
(`SessionAuthenticator.ts
|
|
270
|
-
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`).
|
|
271
271
|
- `onSuccess()` pose l'identifiant sur le contexte — la persistance de session lie le blob au
|
|
272
|
-
principal courant (`SessionAuthenticator.ts
|
|
272
|
+
principal courant (`SessionAuthenticator.ts#SessionAuthenticator.onSuccess`).
|
|
273
273
|
- **Pas de `challenge()`** : session absente = 401 nu → le front redirige vers son écran de login,
|
|
274
|
-
jamais une popup Basic (`SessionAuthenticator.ts
|
|
274
|
+
jamais une popup Basic (`SessionAuthenticator.ts#SessionAuthenticator`).
|
|
275
275
|
|
|
276
276
|
### `jwt` — Bearer signé pour les API (RFC 6750 + BCP RFC 8725)
|
|
277
277
|
|
|
278
278
|
Réservé aux **API service↔service / agents** (le web reste sur la session). Vérifie un access token
|
|
279
279
|
**EdDSA** signé par le keystore du serveur ; `supports()` ne réclame que la structure compacte
|
|
280
|
-
`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
|
|
281
281
|
prouvées en test :
|
|
282
282
|
|
|
283
|
-
| Défense | Comment
|
|
284
|
-
| -------------------------------------- |
|
|
285
|
-
| **Allowlist d'algorithmes** | `algorithms: ["EdDSA"]` — jamais l'algo de l'en-tête du token (`JwtAuthenticator.ts
|
|
286
|
-
| **Clé par `kid` du keyset LOCAL** | `createLocalJWKSet` — jamais `jku`/`jwk` de l'en-tête (`JwtAuthenticator.ts
|
|
287
|
-
| **`aud` + `iss` + `typ` obligatoires** | `typ: "at+jwt"` (§3.11) sépare access et refresh (`JwtAuthenticator.ts
|
|
288
|
-
| **Révocation** | denylist `isJtiDenied` + seuil `invalidBefore` par porteur (`JwtAuthenticator.ts
|
|
289
|
-
| **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) |
|
|
290
290
|
|
|
291
|
-
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
|
|
292
292
|
(expiré, `aud`, signature, sujet banni) part en **audit**, jamais au client — anti-oracle. Le token
|
|
293
|
-
promu porte `scopes`, `jti`, `claims` en attributs (`JwtAuthenticator.ts
|
|
294
|
-
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`).
|
|
295
295
|
|
|
296
|
-
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`) ;
|
|
297
297
|
`firewall-realtime` ne se déclare pas — le firewall le câble lui-même au handshake WebSocket.
|
|
298
298
|
|
|
299
299
|
### `external-jwt` — un jeton émis par un serveur d'autorisation TIERS
|
|
@@ -310,17 +310,17 @@ Clé API personnelle en `Authorization: Bearer nf_…` (préfixe `apiKeys.prefix
|
|
|
310
310
|
Contrairement au JWT (auto-porté), un PAT est un **bearer opaque** dont la vérité vit **côté
|
|
311
311
|
serveur** (`ITokenStore`) → **révocable immédiatement**. Défenses :
|
|
312
312
|
|
|
313
|
-
- **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`)
|
|
314
314
|
→ une valeur malformée n'atteint jamais la base (**anti-DoS**).
|
|
315
|
-
- **Lookup par hash SHA-256** (`findByHash`, `ApiKeyAuthenticator.ts
|
|
315
|
+
- **Lookup par hash SHA-256** (`findByHash`, `ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`) — le secret n'existe
|
|
316
316
|
**nulle part au repos**.
|
|
317
|
-
- **Révocation** (`revokedAt`) + **expiration** (`expiresAt`) (`ApiKeyAuthenticator.ts
|
|
318
|
-
**ban en masse** du porteur (`invalidBefore` vs `createdAt`, `ApiKeyAuthenticator.ts
|
|
319
|
-
- **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`).
|
|
320
320
|
- **`lastUsedAt` throttlé** : aucune écriture sur le hot path tant que la fenêtre
|
|
321
|
-
`apiKeys.lastUsedThrottleS` n'est pas dépassée (`ApiKeyAuthenticator.ts
|
|
321
|
+
`apiKeys.lastUsedThrottleS` n'est pas dépassée (`ApiKeyAuthenticator.ts#IApiKeyAuthenticatorRuntime.lastUsedThrottleS`).
|
|
322
322
|
|
|
323
|
-
Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts
|
|
323
|
+
Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
|
|
324
324
|
|
|
325
325
|
### `firewall-realtime` — la promotion, en WebSocket, de l'identité déjà posée
|
|
326
326
|
|
|
@@ -329,27 +329,27 @@ Le token promu porte `scopes`, `apiKeyId`, `tenantId` (`ApiKeyAuthenticator.ts:1
|
|
|
329
329
|
> a résolue — y compris un agent authentifié par jeton porteur, sans cookie ni session. Son nom
|
|
330
330
|
> d'origine (`SessionRealtimeAuthenticator`) décrivait le premier mode branché, pas son rôle ; la
|
|
331
331
|
> confusion a coûté un durcissement pensé pour la session appliqué à toutes les identités
|
|
332
|
-
> (`FirewallRealtimeAuthenticator.ts
|
|
332
|
+
> (`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator`).
|
|
333
333
|
|
|
334
334
|
Sur un handshake WS (une requête upgrade HTTP qui traverse **le même pipeline**), `startSession`
|
|
335
335
|
puis `handleSecurity` ont **déjà** tourné : session chargée, identité re-résolue, `IUser` posé dans
|
|
336
336
|
l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
|
|
337
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
337
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.supports`).
|
|
338
338
|
|
|
339
339
|
- **Perf : il ne relit pas la base.** `authenticate()` réutilise l'identité de l'ALS
|
|
340
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
340
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`) au lieu de refaire deux lectures base par connexion —
|
|
341
341
|
un coût évitable sur le différenciateur temps réel.
|
|
342
|
-
- **Câblé automatiquement** par `Firewall.#wireRealtime()` (`firewall.ts
|
|
343
|
-
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`).
|
|
344
344
|
- **Le mode de preuve suit le jeton du firewall**, il n'est pas deviné : absent (zone historique),
|
|
345
|
-
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`).
|
|
346
346
|
- **Filet Zero Trust** : il câble un revalidateur appelé avant chaque action data plane
|
|
347
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
347
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`) — la socket peut survivre à l'identité qui l'a
|
|
348
348
|
ouverte (logout, changement de compte, `jti` denylisté). En mode session,
|
|
349
349
|
`buildSessionRevalidator()` re-lit `storage.read(id)` et compare l'identifiant ; toute erreur de
|
|
350
|
-
lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts
|
|
350
|
+
lecture invalide, fail-closed (`FirewallRealtimeAuthenticator.ts#SessionLike.storage`). En mode jeton porteur, la
|
|
351
351
|
preuve est autre : `exp`, `jti` denylisté, `invalidBefore`
|
|
352
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
352
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.#buildBearerRevalidator`).
|
|
353
353
|
|
|
354
354
|
> [!NOTE]
|
|
355
355
|
> **Asymétrie de révocation HTTP↔WS (assumée)** : le jeton realtime est figé au handshake (les
|
|
@@ -360,7 +360,7 @@ l'ALS. `FirewallRealtimeAuthenticator.supports()` ne fait que le constater
|
|
|
360
360
|
## ⚙️ Composer une zone — ordre, mode, cohabitation
|
|
361
361
|
|
|
362
362
|
La liste `area.authenticators` se lit **dans l'ordre**, déroulée par `Firewall.#authenticate()`
|
|
363
|
-
(`firewall.ts
|
|
363
|
+
(`firewall.ts#Firewall.#authenticate`) selon le `mode` de la zone (`first` par défaut, `config.ts#areaSchema`).
|
|
364
364
|
|
|
365
365
|
### Situation 1 — humains ET machines sur la même API (`first`)
|
|
366
366
|
|
|
@@ -369,12 +369,12 @@ Deux preuves différentes, mêmes routes — c'est la config du Démarrage rapid
|
|
|
369
369
|
de lecture :
|
|
370
370
|
|
|
371
371
|
- un maillon dont `supports()` est faux est simplement **sauté** en mode `first`
|
|
372
|
-
(`firewall.ts
|
|
372
|
+
(`firewall.ts#Firewall.#rebuildSecurityHeaders`) ;
|
|
373
373
|
- un credential **présenté mais invalide échoue immédiatement** — l'échec d'`authenticate()`
|
|
374
|
-
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é
|
|
375
375
|
API révoquée donne un 401 direct, même si un autre maillon aurait pu réussir.
|
|
376
376
|
- aucune preuve présentée sur toute la chaîne → `handleSecurity()` lève l'`AuthenticationError`
|
|
377
|
-
Zero Trust (`firewall.ts
|
|
377
|
+
Zero Trust (`firewall.ts#Firewall.handleSecurity`).
|
|
378
378
|
|
|
379
379
|
### Situation 2 — le piège de l'ordre (`anonymous` toujours EN DERNIER)
|
|
380
380
|
|
|
@@ -393,15 +393,15 @@ authenticators: ["anonymous", "session"], // ❌ anonymous accepte TOUT le mon
|
|
|
393
393
|
### Situation 3 — empiler les preuves (`all`)
|
|
394
394
|
|
|
395
395
|
En mode `all`, **chaque** maillon est obligatoire : `supports()` faux = 401 immédiat
|
|
396
|
-
(`firewall.ts
|
|
396
|
+
(`firewall.ts#Firewall.applySecurityHeaders`) et le **dernier** token de la chaîne porte l'identité (`firewall.ts#Firewall.unregisterCspOrigins`) —
|
|
397
397
|
utile pour exiger une preuve de canal (mTLS) **et** une identité, ou un « sudo mode » session +
|
|
398
398
|
re-saisie du mot de passe. Scénario complet côté zones : [firewall](./firewall.md).
|
|
399
399
|
|
|
400
400
|
### Cohabitation JWT + clé API dans une même zone
|
|
401
401
|
|
|
402
402
|
Les deux sont des `Bearer`, mais Nodefony les **discrimine sur la forme** — un JWT a la structure
|
|
403
|
-
compacte `a.b.c` (`COMPACT_JWS`, `JwtAuthenticator.ts
|
|
404
|
-
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_
|
|
405
405
|
format → aucun conflit, aucune double vérification.
|
|
406
406
|
|
|
407
407
|
## Le fil rouge : le message d'échec uniforme
|
|
@@ -413,7 +413,7 @@ paresse : c'est une **défense anti-énumération / anti-oracle**.
|
|
|
413
413
|
Distinguer « compte inconnu » de « mot de passe faux », ou « token expiré » de « signature
|
|
414
414
|
invalide », donnerait à un attaquant une sonde. La cause fine part **toujours** en log d'audit ; le
|
|
415
415
|
client n'obtient qu'un 401 + son challenge — posé par le firewall, premier maillon de la zone qui
|
|
416
|
-
en déclare un (`Firewall.#setChallenge()`, `firewall.ts
|
|
416
|
+
en déclare un (`Firewall.#setChallenge()`, `firewall.ts#Firewall.#setChallenge`).
|
|
417
417
|
|
|
418
418
|
## Mot de passe en clair — refusé en production
|
|
419
419
|
|
|
@@ -430,14 +430,14 @@ de passe — cf la doc du plan d'administration de `@nodefony/framework`) :
|
|
|
430
430
|
- Le refus précède le throttle et le verifier : aucun hash calculé, aucun échec compté. Il est
|
|
431
431
|
audité (`reason: "insecure_transport"`).
|
|
432
432
|
- Le **premier** refus du process part en WARNING, les suivants non : un pod mal configuré
|
|
433
|
-
refuserait chaque connexion (`#warned`, `CredentialTransportPolicy.ts
|
|
433
|
+
refuserait chaque connexion (`#warned`, `CredentialTransportPolicy.ts#CredentialTransportPolicy.#warned`).
|
|
434
434
|
- **La décision se prend sur le scheme EFFECTIF** (`context.scheme`), pas sur le transport du
|
|
435
435
|
pod. Derrière un proxy qui termine TLS — le cas de la plupart des déploiements — il suffit de
|
|
436
436
|
déclarer ce proxy de confiance : `trustProxy` dans la config de `@nodefony/http`, pour que
|
|
437
437
|
`X-Forwarded-Proto: https` soit lu.
|
|
438
438
|
- Une seule décision, posée au conteneur par le firewall au boot (`credentialTransport`,
|
|
439
|
-
`firewall.ts
|
|
440
|
-
`security.allowInsecureCredentials: true` (`CredentialTransportPolicy.ts
|
|
439
|
+
`firewall.ts#Firewall.#provisionSharedServices`) : production ⇒ refus, sauf échappement **écrit**
|
|
440
|
+
`security.allowInsecureCredentials: true` (`CredentialTransportPolicy.ts#shouldEnforceCredentialTransport`). Hors
|
|
441
441
|
production, rien n'est refusé.
|
|
442
442
|
|
|
443
443
|
Pourquoi un 403 et pas un 401 : aucune preuve n'a été jugée. Le client ne corrigera rien dans
|
|
@@ -456,38 +456,38 @@ registerAuthenticatorFactory("ldap", ({ container, config }) => {
|
|
|
456
456
|
|
|
457
457
|
À faire au chargement du module (avant le boot). Trois règles, calquées sur les builtins :
|
|
458
458
|
|
|
459
|
-
- implémenter le contrat `IAuthenticator` (`IAuthenticator.ts
|
|
459
|
+
- implémenter le contrat `IAuthenticator` (`IAuthenticator.ts#IAuthenticator`) — `challenge()` seulement si
|
|
460
460
|
un en-tête `WWW-Authenticate` a du sens pour la stratégie ;
|
|
461
461
|
- renvoyer le **message uniforme** en cas d'échec (la cause fine part en audit) ;
|
|
462
462
|
- laisser les résolutions de services **lazy** dans l'instance — la fabrique ne fait que
|
|
463
|
-
construire (`authenticatorRegistry.ts
|
|
463
|
+
construire (`authenticatorRegistry.ts#IAuthenticatorFactoryContext`).
|
|
464
464
|
|
|
465
465
|
## 📜 Normes appliquées
|
|
466
466
|
|
|
467
467
|
<!-- prettier-ignore -->
|
|
468
468
|
| Domaine | Norme | Ancrage |
|
|
469
469
|
| --- | --- | --- |
|
|
470
|
-
| Challenge d'auth (401) | RFC 9110 §11 | `Firewall.#setChallenge()` (`firewall.ts
|
|
471
|
-
| Bearer | RFC 6750 | `readBearerHeader()` (`runtime/bearer.ts
|
|
472
|
-
| JWT (BCP) | RFC 7519, 8725 | `jwtVerify` durci : allowlist + claims (`JwtAuthenticator.ts
|
|
473
|
-
| HTTP Basic | RFC 7617 | `UserPasswordAuthenticator` (`UserPasswordAuthenticator.ts
|
|
474
|
-
| Backoff de login | NIST SP 800-63B | `#throttler.check()` avant le verifier (`UserPasswordAuthenticator.ts
|
|
475
|
-
| Rate limit (429) | RFC 6585 | `Retry-After` posé par le firewall (`firewall.ts
|
|
476
|
-
| 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`) |
|
|
477
477
|
|
|
478
478
|
## ⚡ Performance & mémoire
|
|
479
479
|
|
|
480
480
|
- `supports()` est un test **bon marché** (en-tête + regex) — et n'est payé que sur zone protégée
|
|
481
481
|
(le chemin chaud/froid vit dans le [firewall](./firewall.md)).
|
|
482
482
|
- Résolutions **lazy** : le verifier `#resolveVerifier` est mémoïsé au premier login
|
|
483
|
-
(`UserPasswordAuthenticator.ts
|
|
484
|
-
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`).
|
|
485
485
|
- `anonymous` : singleton gelé `anonymousUser`, zéro allocation d'utilisateur
|
|
486
|
-
(`AnonymousToken.ts
|
|
486
|
+
(`AnonymousToken.ts#AnonymousToken`).
|
|
487
487
|
- `apikey` : écriture `lastUsedAt` **coalescée** — pas une écriture par requête
|
|
488
|
-
(`ApiKeyAuthenticator.ts
|
|
488
|
+
(`ApiKeyAuthenticator.ts#ApiKeyAuthenticator.authenticate`).
|
|
489
489
|
- `firewall-realtime` : **zéro lecture base** au handshake — réutilise l'ALS
|
|
490
|
-
(`FirewallRealtimeAuthenticator.ts
|
|
490
|
+
(`FirewallRealtimeAuthenticator.ts#FirewallRealtimeAuthenticator.authenticate`).
|
|
491
491
|
- Le throttle NIST **avant** le hash : un 429 ne coûte aucun argon2.
|
|
492
492
|
|
|
493
493
|
## ⚠️ Pièges (symptôme → cause → correction)
|
|
@@ -495,13 +495,13 @@ registerAuthenticatorFactory("ldap", ({ container, config }) => {
|
|
|
495
495
|
<!-- prettier-ignore -->
|
|
496
496
|
| Symptôme | Cause (dans le code) | Correction |
|
|
497
497
|
| --- | --- | --- |
|
|
498
|
-
| 401 systématique sur une zone protégée | Aucune preuve + `anonymous` non listé — Zero Trust (`firewall.ts
|
|
499
|
-
| 401 générique + log ERROR « service `users` » | Câblage : pas de `UserService` au container (`authenticatorRegistry.ts
|
|
500
|
-
| JWT rejeté alors qu'il « semble » valide | `aud`/`iss`/`typ` non conformes, ou `alg` ≠ EdDSA (`JwtAuthenticator.ts
|
|
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
|
|
502
|
-
| Révocation WS pas immédiate | Identité figée au handshake — asymétrie assumée du `FirewallRealtimeAuthenticator` (`FirewallRealtimeAuthenticator.ts
|
|
503
|
-
| Brute-force pas ralenti | `loginThrottler` absent du container — `rateLimit.enabled` off (`firewall.ts
|
|
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
|
|
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é |
|
|
505
505
|
|
|
506
506
|
## 🧪 Tests & couverture
|
|
507
507
|
|