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