@nodefony/security 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +182 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +151 -0
- package/dist/nodefony/command/security-secrets.js +158 -0
- package/dist/nodefony/command/security-token.js +335 -0
- package/dist/nodefony/command/security-user-add.js +131 -0
- package/dist/nodefony/command/security-user-delete.js +102 -0
- package/dist/nodefony/command/security-user-list.js +77 -0
- package/dist/nodefony/config/config.js +366 -0
- package/dist/nodefony/config/defineModuleConfig.js +35 -0
- package/dist/nodefony/contracts/IAccessVoter.js +13 -0
- package/dist/nodefony/contracts/IApiKey.js +1 -0
- package/dist/nodefony/contracts/IAuditEvent.js +1 -0
- package/dist/nodefony/contracts/IAuditStore.js +1 -0
- package/dist/nodefony/contracts/IAuthenticator.js +1 -0
- package/dist/nodefony/contracts/IAuthorizationService.js +1 -0
- package/dist/nodefony/contracts/IFirewall.js +1 -0
- package/dist/nodefony/contracts/IFirewallDescription.js +1 -0
- package/dist/nodefony/contracts/IJwtKeystore.js +1 -0
- package/dist/nodefony/contracts/IOAuthProvider.js +1 -0
- package/dist/nodefony/contracts/ISecuredArea.js +1 -0
- package/dist/nodefony/contracts/IToken.js +1 -0
- package/dist/nodefony/contracts/ITokenStore.js +1 -0
- package/dist/nodefony/contracts/ITotpSecret.js +1 -0
- package/dist/nodefony/contracts/ITotpSecretStore.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredential.js +1 -0
- package/dist/nodefony/contracts/IWebAuthnCredentialStore.js +1 -0
- package/dist/nodefony/contracts/IWebhookEndpoint.js +1 -0
- package/dist/nodefony/contracts/IWebhookStore.js +1 -0
- package/dist/nodefony/contracts/index.js +2 -0
- package/dist/nodefony/errors/AccessDeniedError.js +14 -0
- package/dist/nodefony/errors/ApiKeyError.js +21 -0
- package/dist/nodefony/errors/AuthenticationError.js +14 -0
- package/dist/nodefony/errors/CsrfError.js +23 -0
- package/dist/nodefony/errors/InvalidTargetError.js +39 -0
- package/dist/nodefony/errors/SsrfError.js +17 -0
- package/dist/nodefony/errors/ThrottledError.js +21 -0
- package/dist/nodefony/errors/UnverifiableTokenError.js +42 -0
- package/dist/nodefony/errors/WebAuthnError.js +21 -0
- package/dist/nodefony/errors/index.js +9 -0
- package/dist/nodefony/service/accessTokenVerifier.js +77 -0
- package/dist/nodefony/service/apiKeys.js +310 -0
- package/dist/nodefony/service/auditService.js +145 -0
- package/dist/nodefony/service/authFlow.js +332 -0
- package/dist/nodefony/service/authorization.js +95 -0
- package/dist/nodefony/service/cors.js +81 -0
- package/dist/nodefony/service/csrf.js +97 -0
- package/dist/nodefony/service/firewall.js +699 -0
- package/dist/nodefony/service/oauth2.js +153 -0
- package/dist/nodefony/service/securityHeaders.js +80 -0
- package/dist/nodefony/service/tokenService.js +486 -0
- package/dist/nodefony/service/totp.js +209 -0
- package/dist/nodefony/service/webAuthn.js +343 -0
- package/dist/nodefony/service/webhooks.js +539 -0
- package/dist/nodefony/src/RoleHierarchyWalker.js +77 -0
- package/dist/nodefony/src/SecuredArea.js +51 -0
- package/dist/nodefony/src/admin/SecurityAdminApi.js +495 -0
- package/dist/nodefony/src/admin/WebhookAdminApi.js +378 -0
- package/dist/nodefony/src/admin/adminAudit.js +37 -0
- package/dist/nodefony/src/admin/userRevocationCascade.js +40 -0
- package/dist/nodefony/src/apikey/apiKeyFormat.js +107 -0
- package/dist/nodefony/src/audit/MemoryAuditStore.js +121 -0
- package/dist/nodefony/src/audit/auditBridge.js +82 -0
- package/dist/nodefony/src/audit/auditFilters.js +60 -0
- package/dist/nodefony/src/audit/auditStoreRegistry.js +25 -0
- package/dist/nodefony/src/audit/readAuditContext.js +24 -0
- package/dist/nodefony/src/audit/recordAudit.js +16 -0
- package/dist/nodefony/src/authenticator/AnonymousAuthenticator.js +36 -0
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +164 -0
- package/dist/nodefony/src/authenticator/ExternalJwtAuthenticator.js +224 -0
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +174 -0
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +176 -0
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +92 -0
- package/dist/nodefony/src/authenticator/UserPasswordAuthenticator.js +95 -0
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +63 -0
- package/dist/nodefony/src/authenticator/bearer.js +2 -0
- package/dist/nodefony/src/authenticator/externalSubject.js +36 -0
- package/dist/nodefony/src/authenticator/peekIssuer.js +56 -0
- package/dist/nodefony/src/crypto/secretCipher.js +79 -0
- package/dist/nodefony/src/csp.js +54 -0
- package/dist/nodefony/src/csrfToken.js +65 -0
- package/dist/nodefony/src/net/ssrfGuard.js +130 -0
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +37 -0
- package/dist/nodefony/src/oauth/providers/github.js +65 -0
- package/dist/nodefony/src/oauth/providers/oidc.js +48 -0
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +94 -0
- package/dist/nodefony/src/realtime/frameAuthorizer.js +279 -0
- package/dist/nodefony/src/realtime/realtimeContracts.js +1 -0
- package/dist/nodefony/src/sessionIdentity.js +35 -0
- package/dist/nodefony/src/throttle/LoginThrottler.js +97 -0
- package/dist/nodefony/src/token/AnonymousToken.js +40 -0
- package/dist/nodefony/src/token/JwtKeystore.js +160 -0
- package/dist/nodefony/src/token/MemoryTokenStore.js +236 -0
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +231 -0
- package/dist/nodefony/src/token/UserToken.js +67 -0
- package/dist/nodefony/src/token/jwtRuntime.js +19 -0
- package/dist/nodefony/src/token/secretFile.js +134 -0
- package/dist/nodefony/src/token/tokenCriteria.js +35 -0
- package/dist/nodefony/src/token/tokenFilters.js +72 -0
- package/dist/nodefony/src/token/tokenSort.js +40 -0
- package/dist/nodefony/src/token/tokenStatus.js +35 -0
- package/dist/nodefony/src/token/tokenStoreRegistry.js +25 -0
- package/dist/nodefony/src/totp/MemoryTotpSecretStore.js +97 -0
- package/dist/nodefony/src/totp/totpCipher.js +30 -0
- package/dist/nodefony/src/totp/totpCrypto.js +226 -0
- package/dist/nodefony/src/totp/totpOperations.js +129 -0
- package/dist/nodefony/src/totp/totpSecretStoreRegistry.js +18 -0
- package/dist/nodefony/src/voter/RoleVoter.js +32 -0
- package/dist/nodefony/src/voter/ScopeVoter.js +52 -0
- package/dist/nodefony/src/voter/voterRegistry.js +20 -0
- package/dist/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.js +121 -0
- package/dist/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.js +18 -0
- package/dist/nodefony/src/webhook/MemoryWebhookStore.js +87 -0
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +208 -0
- package/dist/nodefony/src/webhook/webhookCipher.js +27 -0
- package/dist/nodefony/src/webhook/webhookDelivery.js +102 -0
- package/dist/nodefony/src/webhook/webhookFilters.js +56 -0
- package/dist/nodefony/src/webhook/webhookSignature.js +51 -0
- package/dist/nodefony/src/webhook/webhookSort.js +48 -0
- package/dist/nodefony/src/webhook/webhookStoreRegistry.js +18 -0
- package/dist/types/index.d.ts +157 -0
- package/dist/types/nodefony/command/security-secrets.d.ts +24 -0
- package/dist/types/nodefony/command/security-token.d.ts +44 -0
- package/dist/types/nodefony/command/security-user-add.d.ts +28 -0
- package/dist/types/nodefony/command/security-user-delete.d.ts +25 -0
- package/dist/types/nodefony/command/security-user-list.d.ts +28 -0
- package/dist/types/nodefony/config/config.d.ts +295 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/contracts/IAccessVoter.d.ts +23 -0
- package/dist/types/nodefony/contracts/IApiKey.d.ts +75 -0
- package/dist/types/nodefony/contracts/IAuditEvent.d.ts +94 -0
- package/dist/types/nodefony/contracts/IAuditStore.d.ts +80 -0
- package/dist/types/nodefony/contracts/IAuthenticator.d.ts +66 -0
- package/dist/types/nodefony/contracts/IAuthorizationService.d.ts +28 -0
- package/dist/types/nodefony/contracts/IFirewall.d.ts +64 -0
- package/dist/types/nodefony/contracts/IFirewallDescription.d.ts +120 -0
- package/dist/types/nodefony/contracts/IJwtKeystore.d.ts +40 -0
- package/dist/types/nodefony/contracts/IOAuthProvider.d.ts +51 -0
- package/dist/types/nodefony/contracts/ISecuredArea.d.ts +57 -0
- package/dist/types/nodefony/contracts/IToken.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +240 -0
- package/dist/types/nodefony/contracts/ITotpSecret.d.ts +41 -0
- package/dist/types/nodefony/contracts/ITotpSecretStore.d.ts +88 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredential.d.ts +56 -0
- package/dist/types/nodefony/contracts/IWebAuthnCredentialStore.d.ts +118 -0
- package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +82 -0
- package/dist/types/nodefony/contracts/IWebhookStore.d.ts +85 -0
- package/dist/types/nodefony/contracts/index.d.ts +9 -0
- package/dist/types/nodefony/errors/AccessDeniedError.d.ts +10 -0
- package/dist/types/nodefony/errors/ApiKeyError.d.ts +17 -0
- package/dist/types/nodefony/errors/AuthenticationError.d.ts +10 -0
- package/dist/types/nodefony/errors/CsrfError.d.ts +19 -0
- package/dist/types/nodefony/errors/InvalidTargetError.d.ts +34 -0
- package/dist/types/nodefony/errors/SsrfError.d.ts +13 -0
- package/dist/types/nodefony/errors/ThrottledError.d.ts +16 -0
- package/dist/types/nodefony/errors/UnverifiableTokenError.d.ts +37 -0
- package/dist/types/nodefony/errors/WebAuthnError.d.ts +17 -0
- package/dist/types/nodefony/errors/index.d.ts +8 -0
- package/dist/types/nodefony/service/accessTokenVerifier.d.ts +29 -0
- package/dist/types/nodefony/service/apiKeys.d.ts +103 -0
- package/dist/types/nodefony/service/auditService.d.ts +30 -0
- package/dist/types/nodefony/service/authFlow.d.ts +123 -0
- package/dist/types/nodefony/service/authorization.d.ts +33 -0
- package/dist/types/nodefony/service/cors.d.ts +48 -0
- package/dist/types/nodefony/service/csrf.d.ts +57 -0
- package/dist/types/nodefony/service/firewall.d.ts +148 -0
- package/dist/types/nodefony/service/oauth2.d.ts +66 -0
- package/dist/types/nodefony/service/securityHeaders.d.ts +66 -0
- package/dist/types/nodefony/service/tokenService.d.ts +103 -0
- package/dist/types/nodefony/service/totp.d.ts +58 -0
- package/dist/types/nodefony/service/webAuthn.d.ts +123 -0
- package/dist/types/nodefony/service/webhooks.d.ts +160 -0
- package/dist/types/nodefony/src/RoleHierarchyWalker.d.ts +21 -0
- package/dist/types/nodefony/src/SecuredArea.d.ts +31 -0
- package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +82 -0
- package/dist/types/nodefony/src/admin/WebhookAdminApi.d.ts +30 -0
- package/dist/types/nodefony/src/admin/adminAudit.d.ts +27 -0
- package/dist/types/nodefony/src/admin/userRevocationCascade.d.ts +31 -0
- package/dist/types/nodefony/src/apikey/apiKeyFormat.d.ts +43 -0
- package/dist/types/nodefony/src/audit/MemoryAuditStore.d.ts +33 -0
- package/dist/types/nodefony/src/audit/auditBridge.d.ts +49 -0
- package/dist/types/nodefony/src/audit/auditFilters.d.ts +56 -0
- package/dist/types/nodefony/src/audit/auditStoreRegistry.d.ts +37 -0
- package/dist/types/nodefony/src/audit/readAuditContext.d.ts +17 -0
- package/dist/types/nodefony/src/audit/recordAudit.d.ts +13 -0
- package/dist/types/nodefony/src/authenticator/AnonymousAuthenticator.d.ts +26 -0
- package/dist/types/nodefony/src/authenticator/ApiKeyAuthenticator.d.ts +74 -0
- package/dist/types/nodefony/src/authenticator/ExternalJwtAuthenticator.d.ts +132 -0
- package/dist/types/nodefony/src/authenticator/FirewallRealtimeAuthenticator.d.ts +78 -0
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +69 -0
- package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +70 -0
- package/dist/types/nodefony/src/authenticator/UserPasswordAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/src/authenticator/authenticatorRegistry.d.ts +39 -0
- package/dist/types/nodefony/src/authenticator/bearer.d.ts +22 -0
- package/dist/types/nodefony/src/authenticator/externalSubject.d.ts +27 -0
- package/dist/types/nodefony/src/authenticator/peekIssuer.d.ts +31 -0
- package/dist/types/nodefony/src/crypto/secretCipher.d.ts +31 -0
- package/dist/types/nodefony/src/csp.d.ts +39 -0
- package/dist/types/nodefony/src/csrfToken.d.ts +36 -0
- package/dist/types/nodefony/src/net/ssrfGuard.d.ts +43 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +45 -0
- package/dist/types/nodefony/src/oauth/providers/github.d.ts +9 -0
- package/dist/types/nodefony/src/oauth/providers/oidc.d.ts +35 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +62 -0
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +171 -0
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +139 -0
- package/dist/types/nodefony/src/sessionIdentity.d.ts +20 -0
- package/dist/types/nodefony/src/throttle/LoginThrottler.d.ts +68 -0
- package/dist/types/nodefony/src/token/AnonymousToken.d.ts +23 -0
- package/dist/types/nodefony/src/token/JwtKeystore.d.ts +43 -0
- package/dist/types/nodefony/src/token/MemoryTokenStore.d.ts +66 -0
- package/dist/types/nodefony/src/token/RemoteJwtVerifier.d.ts +149 -0
- package/dist/types/nodefony/src/token/UserToken.d.ts +41 -0
- package/dist/types/nodefony/src/token/jwtRuntime.d.ts +28 -0
- package/dist/types/nodefony/src/token/secretFile.d.ts +70 -0
- package/dist/types/nodefony/src/token/tokenCriteria.d.ts +20 -0
- package/dist/types/nodefony/src/token/tokenFilters.d.ts +76 -0
- package/dist/types/nodefony/src/token/tokenSort.d.ts +33 -0
- package/dist/types/nodefony/src/token/tokenStatus.d.ts +38 -0
- package/dist/types/nodefony/src/token/tokenStoreRegistry.d.ts +38 -0
- package/dist/types/nodefony/src/totp/MemoryTotpSecretStore.d.ts +43 -0
- package/dist/types/nodefony/src/totp/totpCipher.d.ts +9 -0
- package/dist/types/nodefony/src/totp/totpCrypto.d.ts +164 -0
- package/dist/types/nodefony/src/totp/totpOperations.d.ts +73 -0
- package/dist/types/nodefony/src/totp/totpSecretStoreRegistry.d.ts +27 -0
- package/dist/types/nodefony/src/voter/RoleVoter.d.ts +25 -0
- package/dist/types/nodefony/src/voter/ScopeVoter.d.ts +30 -0
- package/dist/types/nodefony/src/voter/voterRegistry.d.ts +33 -0
- package/dist/types/nodefony/src/webauthn/MemoryWebAuthnCredentialStore.d.ts +39 -0
- package/dist/types/nodefony/src/webauthn/webAuthnCredentialStoreRegistry.d.ts +26 -0
- package/dist/types/nodefony/src/webhook/MemoryWebhookStore.d.ts +37 -0
- package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +69 -0
- package/dist/types/nodefony/src/webhook/webhookCipher.d.ts +8 -0
- package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +28 -0
- package/dist/types/nodefony/src/webhook/webhookFilters.d.ts +64 -0
- package/dist/types/nodefony/src/webhook/webhookSignature.d.ts +20 -0
- package/dist/types/nodefony/src/webhook/webhookSort.d.ts +39 -0
- package/dist/types/nodefony/src/webhook/webhookStoreRegistry.d.ts +31 -0
- package/docs/api-keys.md +691 -0
- package/docs/audit.md +751 -0
- package/docs/authenticators.md +487 -0
- package/docs/authorization.md +497 -0
- package/docs/cors.md +497 -0
- package/docs/csrf.md +392 -0
- package/docs/external-jwt.md +181 -0
- package/docs/firewall.md +546 -0
- package/docs/headers.md +616 -0
- package/docs/index.md +207 -0
- package/docs/lexique.md +190 -0
- package/docs/oauth2.md +575 -0
- package/docs/obtenir-un-jeton.md +225 -0
- package/docs/tokens.md +520 -0
- package/docs/totp.md +804 -0
- package/docs/webauthn.md +733 -0
- package/docs/webhooks.md +1016 -0
- package/package.json +83 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/**
|
|
3
|
+
* Erreur de **gestion** d'un credential WebAuthn (enrôlement) — porte un `code`
|
|
4
|
+
* HTTP que l'adaptateur framework mappe par duck-typing (il n'importe jamais les
|
|
5
|
+
* classes de `@nodefony/security`).
|
|
6
|
+
*
|
|
7
|
+
* - `409` — plafond `passkeys.maxPerUser` atteint.
|
|
8
|
+
*
|
|
9
|
+
* Distincte de la **cérémonie** elle-même (défi/signature/origine invalides →
|
|
10
|
+
* `AuthenticationError` 401, message uniforme anti-énumération) : ici la
|
|
11
|
+
* cérémonie a réussi cryptographiquement, c'est la politique du serveur qui
|
|
12
|
+
* refuse d'enregistrer un credential de plus.
|
|
13
|
+
*/
|
|
14
|
+
export declare class WebAuthnError extends nodefonyError {
|
|
15
|
+
constructor(message: string, code: 409);
|
|
16
|
+
}
|
|
17
|
+
export default WebAuthnError;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { AuthenticationError } from "./AuthenticationError.js";
|
|
2
|
+
export { AccessDeniedError } from "./AccessDeniedError.js";
|
|
3
|
+
export { ThrottledError } from "./ThrottledError.js";
|
|
4
|
+
export { UnverifiableTokenError } from "./UnverifiableTokenError.js";
|
|
5
|
+
export { InvalidTargetError } from "./InvalidTargetError.js";
|
|
6
|
+
export { CsrfError } from "./CsrfError.js";
|
|
7
|
+
export { SsrfError } from "./SsrfError.js";
|
|
8
|
+
export { WebAuthnError } from "./WebAuthnError.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Service, Module } from "nodefony";
|
|
2
|
+
import { RemoteJwtVerifier } from "../src/token/RemoteJwtVerifier.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pose (ou non) le vérificateur de jetons d'accès TIERS dans le conteneur.
|
|
5
|
+
*
|
|
6
|
+
* Ce service est une **décision de câblage**, pas de la cryptographie : toute la
|
|
7
|
+
* mécanique vit dans {@link RemoteJwtVerifier}, et la doctrine de refus dans le
|
|
8
|
+
* cœur (`nodefony/src/oauth/`). Ici, on lit la configuration et on tranche une
|
|
9
|
+
* seule question — cette application accepte-t-elle des jetons émis ailleurs ?
|
|
10
|
+
*
|
|
11
|
+
* **Aucun émetteur déclaré = rien n'est posé**, et c'est le comportement voulu :
|
|
12
|
+
* une porte protégée qui ne trouve pas de vérificateur refuse de servir en le
|
|
13
|
+
* disant (503 + CRITIC), là où un vérificateur présent mais vide refuserait
|
|
14
|
+
* chaque jeton un par un — même résultat pour l'appelant, diagnostic beaucoup
|
|
15
|
+
* plus difficile pour l'exploitant.
|
|
16
|
+
*
|
|
17
|
+
* Le coût est nul quand la capacité n'est pas utilisée : rien n'est instancié,
|
|
18
|
+
* aucune requête n'est faite au démarrage. La découverte des clés n'a lieu qu'au
|
|
19
|
+
* PREMIER jeton réellement présenté, et une seule fois par émetteur.
|
|
20
|
+
*/
|
|
21
|
+
declare class AccessTokenVerifierService extends Service {
|
|
22
|
+
#private;
|
|
23
|
+
module: Module;
|
|
24
|
+
constructor(module: Module);
|
|
25
|
+
/** Le vérificateur, ou `null` si aucun émetteur n'est déclaré. */
|
|
26
|
+
get verifier(): RemoteJwtVerifier | null;
|
|
27
|
+
}
|
|
28
|
+
export default AccessTokenVerifierService;
|
|
29
|
+
export { AccessTokenVerifierService };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Service, Module, type IPage } from "nodefony";
|
|
2
|
+
import { type ITokenCounts } from "../src/token/tokenFilters.js";
|
|
3
|
+
import type { ITokenListQuery } from "../contracts/ITokenStore.js";
|
|
4
|
+
import type { IApiKeyView, IApiKeyCreated, IApiKeyCapabilities, ICreateApiKeyOptions } from "../contracts/IApiKey.js";
|
|
5
|
+
/**
|
|
6
|
+
* Gestion des **clés API personnelles (PAT, P6.12)** — émission, listing et
|
|
7
|
+
* révocation, au-dessus du `ITokenStore` **partagé** (posé au container par le
|
|
8
|
+
* `TokenService`, qui en possède aussi le `gc`). Un PAT et un refresh token
|
|
9
|
+
* cohabitent dans la même table (`kind`) ; ce service ne traite que `kind:"pat"`.
|
|
10
|
+
*
|
|
11
|
+
* **Sécurité** : le secret (256 bits aléatoires) n'est rendu en clair qu'à la
|
|
12
|
+
* création (`IApiKeyCreated.token`, RFC « shown once ») ; seul son `sha256` est
|
|
13
|
+
* persisté. Création/révocation s'appliquent **toujours à un porteur donné**
|
|
14
|
+
* (jamais à autrui) — l'identité est résolue côté endpoint (session BFF). La
|
|
15
|
+
* vérification d'une clé présentée vit, elle, dans `ApiKeyAuthenticator`.
|
|
16
|
+
*
|
|
17
|
+
* Le store est résolu **paresseusement** du container (`tokenStore`) au premier
|
|
18
|
+
* usage : indépendant de l'ordre de boot des services.
|
|
19
|
+
*/
|
|
20
|
+
declare class ApiKeyService extends Service {
|
|
21
|
+
#private;
|
|
22
|
+
module: Module;
|
|
23
|
+
constructor(module: Module);
|
|
24
|
+
/** `true` si les clés API sont activées en config. */
|
|
25
|
+
isEnabled(): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Champs de tri que le backend **actuellement branché** sait honorer, en
|
|
28
|
+
* vocabulaire public. Le data plane admin les passe en allowlist au traducteur
|
|
29
|
+
* de requête de page : hors de cette liste, un `?order=` est refusé en 400.
|
|
30
|
+
*
|
|
31
|
+
* La liste vient du store, jamais d'une constante recopiée ici : c'est ce qui
|
|
32
|
+
* fait qu'un backend à capacité réduite (Redis, dont le `SCAN` n'a pas d'ordre
|
|
33
|
+
* global) refuse le tri **sans qu'aucune règle supplémentaire ne soit écrite**.
|
|
34
|
+
* Store absent ou indisponible → aucune capacité annoncée, donc aucun tri promis.
|
|
35
|
+
*
|
|
36
|
+
* @returns les champs triables, ou un tableau vide.
|
|
37
|
+
*/
|
|
38
|
+
sortableFields(): readonly string[];
|
|
39
|
+
/**
|
|
40
|
+
* Émet une nouvelle clé API pour un porteur — renvoie sa vue publique **+ le
|
|
41
|
+
* token en clair** (affiché une seule fois).
|
|
42
|
+
*
|
|
43
|
+
* @throws ApiKeyError 400 — nom vide/trop long, scope hors catalogue, expiry invalide.
|
|
44
|
+
* @throws ApiKeyError 409 — plafond `maxPerSubject` atteint.
|
|
45
|
+
* @throws ApiKeyError 503 — store indisponible.
|
|
46
|
+
*/
|
|
47
|
+
createForSubject(subjectId: string, subjectType: "user" | "service", opts: ICreateApiKeyOptions): Promise<IApiKeyCreated>;
|
|
48
|
+
/** Liste les clés (PAT) d'un porteur — vue publique, sans secret, récentes d'abord. */
|
|
49
|
+
listForSubject(subjectId: string): Promise<IApiKeyView[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Liste **paginée** des clés (PAT) du système, tous porteurs confondus — vue
|
|
52
|
+
* d'ADMINISTRATION (gouvernance / réponse à incident), publique et sans secret.
|
|
53
|
+
* Réservé au data plane admin (RBAC `ROLE_NODEFONY_ADMIN`) : l'identité du porteur
|
|
54
|
+
* (`subjectId`) est exposée pour la supervision.
|
|
55
|
+
*
|
|
56
|
+
* Pagination **native au store** (jamais un `listAll()` matérialisé en RAM) : `kind`
|
|
57
|
+
* est forcé à `"pat"` ; les autres filtres (`subjectId`/`revoked`) + la fenêtre
|
|
58
|
+
* (`limit`/`offset`/`cursor`) viennent de `query`. Tri `createdAt` DESC par défaut.
|
|
59
|
+
*
|
|
60
|
+
* @param query - filtres + fenêtre de page ({@link ITokenListQuery}, `kind` ignoré).
|
|
61
|
+
* @returns une page de vues publiques ({@link IApiKeyView}, sans secret).
|
|
62
|
+
*/
|
|
63
|
+
listPagePat(query: ITokenListQuery): Promise<IPage<IApiKeyView>>;
|
|
64
|
+
/**
|
|
65
|
+
* Les compteurs de tête de la console — posés sur la collection ENTIÈRE, pas
|
|
66
|
+
* sur la page affichée.
|
|
67
|
+
*
|
|
68
|
+
* Les trois états partitionnent, mais chacun est **compté** : une partition
|
|
69
|
+
* est une propriété du domaine d'aujourd'hui, pas une garantie du code, et un
|
|
70
|
+
* quatrième état la briserait en silence si l'un se déduisait des autres.
|
|
71
|
+
*
|
|
72
|
+
* `kind` reste forcé à `"pat"` comme pour la liste : ces cartes surplombent
|
|
73
|
+
* un tableau de clés d'API, pas de jetons de rafraîchissement.
|
|
74
|
+
*
|
|
75
|
+
* @param query - filtres à appliquer avant comptage (sans fenêtre).
|
|
76
|
+
*/
|
|
77
|
+
countKeyFacets(query?: Partial<ITokenListQuery>): Promise<ITokenCounts>;
|
|
78
|
+
/**
|
|
79
|
+
* Révoque **n'importe quelle** clé (PAT) — action d'ADMINISTRATION (réponse à
|
|
80
|
+
* incident : clé compromise), SANS contrainte de porteur (≠ `revokeForSubject`).
|
|
81
|
+
* Audité avec l'acteur admin ET le porteur cible. Idempotent.
|
|
82
|
+
*
|
|
83
|
+
* @param id - identifiant public de la clé.
|
|
84
|
+
* @param actorId - identité de l'admin qui révoque (tracée pour l'audit).
|
|
85
|
+
* @returns la vue publique mise à jour, ou `null` si introuvable / pas un PAT.
|
|
86
|
+
*/
|
|
87
|
+
revokeAnyPat(id: string, actorId: string): Promise<IApiKeyView | null>;
|
|
88
|
+
/**
|
|
89
|
+
* Capacités/contraintes d'émission (plafond, scopes, préfixe, durée par défaut)
|
|
90
|
+
* — pour un formulaire de création honnête côté console. Aucune valeur sensible.
|
|
91
|
+
*/
|
|
92
|
+
describeCapabilities(): IApiKeyCapabilities;
|
|
93
|
+
/**
|
|
94
|
+
* Révoque une clé du porteur. **Anti-énumération** : une clé inexistante OU
|
|
95
|
+
* appartenant à autrui renvoie `false` (« introuvable pour ce porteur ») —
|
|
96
|
+
* jamais un 403 qui révélerait son existence. Idempotent (déjà révoquée → `true`).
|
|
97
|
+
*
|
|
98
|
+
* @returns `true` si la clé du porteur a été trouvée (et révoquée), sinon `false`.
|
|
99
|
+
*/
|
|
100
|
+
revokeForSubject(subjectId: string, id: string): Promise<boolean>;
|
|
101
|
+
}
|
|
102
|
+
export default ApiKeyService;
|
|
103
|
+
export { ApiKeyService };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Service, Module, type IPage } from "nodefony";
|
|
2
|
+
import type { IAuditEvent, IAuditEventDraft } from "../contracts/IAuditEvent.js";
|
|
3
|
+
import type { IAuditListQuery, IAuditSink } from "../contracts/IAuditStore.js";
|
|
4
|
+
/**
|
|
5
|
+
* Journal d'audit de sécurité (P6.14) — collecte les **événements** de sécurité
|
|
6
|
+
* (login, refus d'accès, jeton émis/révoqué, défense CSRF/CORS, verrou WS) émis
|
|
7
|
+
* EXPLICITEMENT par le firewall, les authenticators et les controllers. Distinct
|
|
8
|
+
* du log de trafic (`JsonAuditLogger`, P3.1, 1 PDU/requête) : ici on trace les
|
|
9
|
+
* **transitions d'état** de sécurité, jamais le hot-path par requête.
|
|
10
|
+
*
|
|
11
|
+
* Propriétaire du {@link IAuditStore} (référence mémoire append-only) : le pose au
|
|
12
|
+
* container (`auditStore`, consommé par le data plane P6.15) et arme le `gc` de
|
|
13
|
+
* rétention (timer `unref`). Implémente {@link IAuditSink} — `record` est
|
|
14
|
+
* **fire-and-forget** (jamais bloquant) et **no-op à coût nul** si désactivé.
|
|
15
|
+
*
|
|
16
|
+
* Slot : store **pluggable** (ORM/Loki, multi-pod) = lot futur, comme
|
|
17
|
+
* `tokenStoreRegistry`. Le socle n'embarque que la référence mémoire.
|
|
18
|
+
*/
|
|
19
|
+
declare class AuditService extends Service implements IAuditSink {
|
|
20
|
+
#private;
|
|
21
|
+
module: Module;
|
|
22
|
+
constructor(module: Module);
|
|
23
|
+
record(draft: IAuditEventDraft): void;
|
|
24
|
+
subscribe(listener: (event: IAuditEvent) => void): () => void;
|
|
25
|
+
/** Lit une page du journal (délègue au store) ; vide si l'audit est inactif. */
|
|
26
|
+
listPage(query: IAuditListQuery): Promise<IPage<IAuditEvent>>;
|
|
27
|
+
/** `true` si l'audit est actif (config `audit.enabled`). */
|
|
28
|
+
isEnabled(): boolean;
|
|
29
|
+
}
|
|
30
|
+
export default AuditService;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Service, Module } from "nodefony";
|
|
2
|
+
import type { ContextType, ISession } from "@nodefony/http";
|
|
3
|
+
/**
|
|
4
|
+
* Projection PUBLIQUE de l'utilisateur — ce qui sort en JSON vers le client.
|
|
5
|
+
* Jamais l'entité brute : le hash (`IPasswordAuthenticatedUser.password`) ne
|
|
6
|
+
* doit traverser ni la sérialisation ni un log.
|
|
7
|
+
*/
|
|
8
|
+
export interface ISafeUser {
|
|
9
|
+
id: string;
|
|
10
|
+
username: string;
|
|
11
|
+
roles: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Issue d'un `login` : soit l'identité est établie (session ouverte), soit un
|
|
15
|
+
* **second facteur** est requis (2FA) — le mot de passe seul n'a PAS authentifié.
|
|
16
|
+
*/
|
|
17
|
+
export type ILoginOutcome = {
|
|
18
|
+
status: "authenticated";
|
|
19
|
+
user: ISafeUser;
|
|
20
|
+
} | {
|
|
21
|
+
status: "mfa_required";
|
|
22
|
+
methods: ["totp"];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Flux de session BFF — login/logout/me côté serveur (P6 J3).
|
|
26
|
+
*
|
|
27
|
+
* C'est le GUICHET : le credential est présenté UNE fois (`login`), vérifié par
|
|
28
|
+
* le {@link IPasswordVerifier} (hash, leurre anti-timing, re-hash migration),
|
|
29
|
+
* puis remplacé par un cookie de session opaque `HttpOnly` — le mot de passe ne
|
|
30
|
+
* recircule jamais, le navigateur ne stocke aucun token lisible par JS.
|
|
31
|
+
*
|
|
32
|
+
* Anti session-fixation (OWASP) : l'ID de session est TOUJOURS régénéré au
|
|
33
|
+
* login — un ID pré-posé par un attaquant (cookie forcé avant le guichet) ne
|
|
34
|
+
* survit pas à l'authentification ; l'ancienne entrée storage est détruite.
|
|
35
|
+
*
|
|
36
|
+
* Throttling NIST SP 800-63B : le MÊME `LoginThrottler` que la porte Basic
|
|
37
|
+
* (instance partagée via le container, posée par le firewall au boot) — un
|
|
38
|
+
* attaquant ne contourne pas le backoff en changeant de porte.
|
|
39
|
+
*
|
|
40
|
+
* Les handlers HTTP (`SessionAuthController`, `@nodefony/framework`) sont des
|
|
41
|
+
* adaptateurs minces au-dessus de ce service — la logique reste testable sans
|
|
42
|
+
* transport.
|
|
43
|
+
*/
|
|
44
|
+
declare class AuthFlow extends Service {
|
|
45
|
+
#private;
|
|
46
|
+
module: Module;
|
|
47
|
+
constructor(module: Module);
|
|
48
|
+
/**
|
|
49
|
+
* Authentifie le couple identifiant/mot de passe et OUVRE la session BFF.
|
|
50
|
+
*
|
|
51
|
+
* Ordre NIST : throttle AVANT le verifier (un identifiant bloqué ne coûte
|
|
52
|
+
* aucun hash argon2 — le backoff protège aussi le serveur du DoS), échec
|
|
53
|
+
* compté, succès remis à zéro.
|
|
54
|
+
*
|
|
55
|
+
* @param context - contexte HTTP courant (porte la session/le cookie).
|
|
56
|
+
* @param identifier - identifiant saisi (body JSON, non typé à la frontière).
|
|
57
|
+
* @param password - mot de passe saisi.
|
|
58
|
+
* @returns `authenticated` (identité établie) ou `mfa_required` (2ᵉ facteur requis).
|
|
59
|
+
* @throws ThrottledError (429 + `Retry-After`) — backoff actif.
|
|
60
|
+
* @throws AuthenticationError (401, message uniforme) — credential absent ou
|
|
61
|
+
* invalide.
|
|
62
|
+
*/
|
|
63
|
+
login(context: ContextType, identifier: unknown, password: unknown): Promise<ILoginOutcome>;
|
|
64
|
+
/**
|
|
65
|
+
* Ouvre la session BFF pour un utilisateur **déjà authentifié par un autre
|
|
66
|
+
* facteur** (passkey/WebAuthn, OAuth, magic link…) — aucun mot de passe à
|
|
67
|
+
* vérifier ici, la preuve a été apportée en amont par l'appelant.
|
|
68
|
+
*
|
|
69
|
+
* Même anti-fixation que {@link login} (ID régénéré, ancienne entrée
|
|
70
|
+
* détruite). L'identité est re-résolue + revalidée (compte actif/non
|
|
71
|
+
* verrouillé) via la source unique `resolveSessionIdentity` — un compte banni
|
|
72
|
+
* entre la preuve et l'ouverture de session est rejeté.
|
|
73
|
+
*
|
|
74
|
+
* @param identifier - identifiant de l'utilisateur prouvé (ex. `sub` du credential).
|
|
75
|
+
* @throws AuthenticationError (401, message uniforme) — identifiant absent, ou
|
|
76
|
+
* compte disparu/inactif/verrouillé.
|
|
77
|
+
*/
|
|
78
|
+
establishSessionFor(context: ContextType, identifier: unknown, reason?: string): Promise<ISafeUser>;
|
|
79
|
+
/**
|
|
80
|
+
* Valide le **second facteur** (code TOTP ou code de récupération) après un
|
|
81
|
+
* `login` ayant renvoyé `mfa_required`, puis OUVRE la session BFF. Le défi
|
|
82
|
+
* PENDING déposé en session par `login` est lu, vérifié, puis invalidé (usage
|
|
83
|
+
* unique) ; l'identité n'est établie qu'ICI. **Throttlé** sur l'identité en
|
|
84
|
+
* attente (anti brute-force du code à 6 chiffres).
|
|
85
|
+
*
|
|
86
|
+
* @param context - contexte HTTP (porte la session PENDING).
|
|
87
|
+
* @param code - code présenté (TOTP ou code de récupération).
|
|
88
|
+
* @returns la projection publique de l'utilisateur authentifié.
|
|
89
|
+
* @throws ThrottledError (429) — trop de tentatives.
|
|
90
|
+
* @throws AuthenticationError (401, uniforme) — aucun défi en cours, code absent
|
|
91
|
+
* ou invalide (la session N'est PAS ouverte ; le défi reste pour un retry).
|
|
92
|
+
*/
|
|
93
|
+
completeMfaLogin(context: ContextType, code: unknown): Promise<ISafeUser>;
|
|
94
|
+
/**
|
|
95
|
+
* Garantit une session pour la requête courante — la démarre (+ cookie) si
|
|
96
|
+
* elle n'existe pas encore. Sert aux cérémonies **pré-authentification**
|
|
97
|
+
* (login WebAuthn) : elles doivent porter un challenge côté serveur AVANT que
|
|
98
|
+
* l'utilisateur soit connecté. La session anonyme ainsi créée devient la
|
|
99
|
+
* session authentifiée au login — {@link establishSessionFor} régénère l'ID
|
|
100
|
+
* (anti-fixation préservée), donc un challenge déposé ici n'ouvre aucune brèche.
|
|
101
|
+
*
|
|
102
|
+
* @returns la session (existante ou neuve), ou `null` si le service de session
|
|
103
|
+
* est indisponible.
|
|
104
|
+
*/
|
|
105
|
+
ensureSession(context: ContextType): Promise<ISession | null>;
|
|
106
|
+
/**
|
|
107
|
+
* Détruit la session courante (storage + cookie). Idempotent : sans session
|
|
108
|
+
* active, ne fait rien.
|
|
109
|
+
*
|
|
110
|
+
* @returns `true` si une session a réellement été détruite.
|
|
111
|
+
*/
|
|
112
|
+
logout(context: ContextType): Promise<boolean>;
|
|
113
|
+
/**
|
|
114
|
+
* Identité portée par la session courante, re-résolue auprès du provider
|
|
115
|
+
* (mêmes contrôles que le `SessionAuthenticator` — source unique).
|
|
116
|
+
*
|
|
117
|
+
* @returns la projection publique, ou `null` (pas de session, session
|
|
118
|
+
* orpheline, compte verrouillé/désactivé) — le handler répond 401.
|
|
119
|
+
*/
|
|
120
|
+
me(context: ContextType): Promise<ISafeUser | null>;
|
|
121
|
+
}
|
|
122
|
+
export default AuthFlow;
|
|
123
|
+
export { AuthFlow };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Service, Module, Severity, Msgid, Message, Pdu } from "nodefony";
|
|
2
|
+
import type { IAuthorizationService } from "../contracts/IAuthorizationService.js";
|
|
3
|
+
import type { IToken } from "../contracts/IToken.js";
|
|
4
|
+
/**
|
|
5
|
+
* Service d'autorisation Nodefony (niveau C, P6 J6) — décide d'un accès via un
|
|
6
|
+
* jury de {@link IAccessVoter}.
|
|
7
|
+
*
|
|
8
|
+
* Stratégie **affirmative + DENY veto** : un seul `DENY` bloque (veto) ; sinon un
|
|
9
|
+
* `GRANT` suffit ; **silence total** (tous `ABSTAIN`, ou aucun voter compétent)
|
|
10
|
+
* → `DENY` (**Zero Trust** : fermé par défaut). Tout refus est audité (WARNING) ;
|
|
11
|
+
* les accès accordés restent silencieux (pas de spam — l'audit d'octroi explicite
|
|
12
|
+
* viendra avec `@AuditLog`, P6.14).
|
|
13
|
+
*
|
|
14
|
+
* Les voters sont découverts au boot via le `voterRegistry` (built-in `role` +
|
|
15
|
+
* ceux des apps/plugins) — aucun nom en dur ici. Consommé par les décorateurs
|
|
16
|
+
* (`@IsGranted`, J7) et le verrou de frame WS (« 1 garde = N transports »).
|
|
17
|
+
*
|
|
18
|
+
* Perf : aucune allocation par appel (`decide` itère les voters et teste
|
|
19
|
+
* `supports()` en place) ; les voters sont instanciés UNE fois au boot.
|
|
20
|
+
*/
|
|
21
|
+
declare class Authorization extends Service implements IAuthorizationService {
|
|
22
|
+
#private;
|
|
23
|
+
module: Module;
|
|
24
|
+
constructor(module: Module);
|
|
25
|
+
/**
|
|
26
|
+
* Le token a-t-il le droit `attribute` sur `subject` ? Affirmative + DENY veto,
|
|
27
|
+
* défaut `DENY` (Zero Trust). Voir {@link IAuthorizationService.decide}.
|
|
28
|
+
*/
|
|
29
|
+
decide(token: IToken, attribute: string, subject?: unknown): Promise<boolean>;
|
|
30
|
+
log(pci: unknown, severity?: Severity, msgid?: Msgid, msg?: Message): Pdu;
|
|
31
|
+
}
|
|
32
|
+
export default Authorization;
|
|
33
|
+
export { Authorization };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sous-ensemble de la config `cors` consommé par la politique (cf defineSecurityConfig).
|
|
3
|
+
*/
|
|
4
|
+
export interface ICorsOptions {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
origins: readonly string[];
|
|
7
|
+
credentials: boolean;
|
|
8
|
+
methods: readonly string[];
|
|
9
|
+
allowedHeaders: readonly string[];
|
|
10
|
+
exposedHeaders: readonly string[];
|
|
11
|
+
maxAgeS: number;
|
|
12
|
+
}
|
|
13
|
+
/** En-têtes de réponse CORS à poser (nom canonique → valeur). */
|
|
14
|
+
export type CorsHeaders = Record<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* Politique CORS de Nodefony — Same-Origin Policy assouplie côté serveur
|
|
17
|
+
* (Fetch Standard / W3C CORS protocol). Logique PURE et synchrone : décide les
|
|
18
|
+
* en-têtes `Access-Control-*` à poser pour une origine donnée. Instanciée une
|
|
19
|
+
* fois au boot par le firewall, testable sans serveur.
|
|
20
|
+
*
|
|
21
|
+
* Invariants de sécurité (OWASP) :
|
|
22
|
+
* - **Jamais `*` + credentials** : interdit au boot (refine Zod). Ici, `*` n'est
|
|
23
|
+
* émis QUE si `credentials=false` ; avec credentials, l'origine est reflétée.
|
|
24
|
+
* - **Reflet d'origine ⇒ `Vary: Origin`** : signalé via {@link reflectsOrigin}
|
|
25
|
+
* pour que l'appelant pose l'en-tête `Vary` (correction de cache).
|
|
26
|
+
* - **Origine non whitelistée ⇒ aucun en-tête** : la réponse n'est pas partageable
|
|
27
|
+
* (le navigateur bloque), zéro information divulguée.
|
|
28
|
+
*
|
|
29
|
+
* @see Fetch Standard (CORS protocol) · OWASP CORS.
|
|
30
|
+
*/
|
|
31
|
+
export declare class Cors {
|
|
32
|
+
#private;
|
|
33
|
+
constructor(options: ICorsOptions);
|
|
34
|
+
/** `true` si la valeur Allow-Origin reflète l'origine (⇒ l'appelant doit poser `Vary: Origin`). */
|
|
35
|
+
reflectsOrigin(allowOrigin: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* En-têtes d'une réponse au **preflight** `OPTIONS` (méthodes/headers autorisés
|
|
38
|
+
* + cache + credentials), ou `null` si l'origine n'est pas autorisée (réponse
|
|
39
|
+
* 204 nue → le navigateur bloque).
|
|
40
|
+
*/
|
|
41
|
+
preflightHeaders(origin: string): CorsHeaders | null;
|
|
42
|
+
/**
|
|
43
|
+
* En-têtes d'une réponse à une **requête réelle** cross-origin (Allow-Origin +
|
|
44
|
+
* credentials + headers exposés au JS), ou `null` si l'origine n'est pas autorisée.
|
|
45
|
+
*/
|
|
46
|
+
actualHeaders(origin: string): CorsHeaders | null;
|
|
47
|
+
}
|
|
48
|
+
export default Cors;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Sous-ensemble de la config `csrf` consommé par la défense (cf defineSecurityConfig). */
|
|
2
|
+
export interface ICsrfOptions {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
fetchMetadata: boolean;
|
|
5
|
+
checkOrigin: boolean;
|
|
6
|
+
strictSameSite: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** En-têtes bruts d'une requête, extraits par le firewall (clés HTTP en lowercase). */
|
|
9
|
+
export interface ICsrfRequest {
|
|
10
|
+
/** Méthode HTTP (`context.method`). */
|
|
11
|
+
method: string | null | undefined;
|
|
12
|
+
/** `Sec-Fetch-Site` — provenance tamponnée par le navigateur (défense primaire). */
|
|
13
|
+
secFetchSite: string | undefined;
|
|
14
|
+
/** `Origin` — origine du document initiateur (fallback). */
|
|
15
|
+
origin: string | undefined;
|
|
16
|
+
/** `Referer` — utilisé seulement si `Origin` est absent (fallback). */
|
|
17
|
+
referer: string | undefined;
|
|
18
|
+
/** Hôte cible (`context.domain` / en-tête `Host`) — pour le test same-host du fallback. */
|
|
19
|
+
host: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Défense CSRF par défaut de Nodefony — **Fetch Metadata d'abord** (modèle Go 1.25
|
|
23
|
+
* `CrossOriginProtection` / OWASP 2025), repli `Origin`/`Referer` pour les vieux
|
|
24
|
+
* navigateurs. Logique PURE et synchrone (aucun I/O, aucune alloc sur le hot-path
|
|
25
|
+
* GET) → testable sans serveur, instanciée une seule fois au boot par le firewall.
|
|
26
|
+
*
|
|
27
|
+
* Chaîne de décision sur une requête state-changing :
|
|
28
|
+
*
|
|
29
|
+
* 1. **Origine de confiance** (alias multi-domaine `csrf.trustedOrigins` ∪ whitelist
|
|
30
|
+
* CORS) → laisser passer même en cross-site : un alias légitime de l'app, ou ce
|
|
31
|
+
* que CORS autorise déjà, n'est pas du CSRF (cohérence CSRF ↔ CORS).
|
|
32
|
+
* 2. **Fetch Metadata** (`Sec-Fetch-Site`, infalsifiable) : `same-origin`/`none`
|
|
33
|
+
* → OK ; `same-site` → OK sauf `strictSameSite` ; `cross-site` → **403** ;
|
|
34
|
+
* valeur inconnue → on délègue au repli (forward-compat, W3C « SHOULD ignore »).
|
|
35
|
+
* 3. **Repli `Origin`/`Referer`** : aucune des deux → client non-navigateur, hors
|
|
36
|
+
* vecteur CSRF → OK ; sinon same-host requis, mismatch → **403**.
|
|
37
|
+
*
|
|
38
|
+
* @see RFC 9110 §9.2.1 (méthodes sûres) · W3C Fetch Metadata · RFC 6265bis §8.8.1.
|
|
39
|
+
*/
|
|
40
|
+
export declare class Csrf {
|
|
41
|
+
#private;
|
|
42
|
+
/**
|
|
43
|
+
* `true` si la méthode mute l'état (hors {@link SAFE_METHODS}). Permet à
|
|
44
|
+
* l'appelant (firewall) de court-circuiter le hot-path GET sans lire d'en-tête.
|
|
45
|
+
*/
|
|
46
|
+
static isStateChanging(method: string | null | undefined): boolean;
|
|
47
|
+
constructor(options: ICsrfOptions, allowedOrigins?: readonly string[]);
|
|
48
|
+
/**
|
|
49
|
+
* Valide la provenance d'une requête. No-op (retour immédiat) sur une méthode
|
|
50
|
+
* sûre — coût nul sur le GET dominant. Lève {@link CsrfError} (403) sinon.
|
|
51
|
+
*
|
|
52
|
+
* @throws CsrfError - mutation `cross-site` (Fetch Metadata) ou `Origin`/`Referer`
|
|
53
|
+
* étranger aux origines de l'app (repli).
|
|
54
|
+
*/
|
|
55
|
+
enforce(req: ICsrfRequest): void;
|
|
56
|
+
}
|
|
57
|
+
export default Csrf;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Service, Module, Severity, Msgid, Message, Pdu } from "nodefony";
|
|
2
|
+
import type { IProtectedResourceInput } from "nodefony";
|
|
3
|
+
import type { ContextType } from "@nodefony/http";
|
|
4
|
+
import { SecuredArea } from "../src/SecuredArea.js";
|
|
5
|
+
import { RoleHierarchyWalker } from "../src/RoleHierarchyWalker.js";
|
|
6
|
+
import { type CspFragment } from "../src/csp.js";
|
|
7
|
+
import type { IFirewall } from "../contracts/IFirewall.js";
|
|
8
|
+
import type { IAuthenticator } from "../contracts/IAuthenticator.js";
|
|
9
|
+
import type { ISecuredArea } from "../contracts/ISecuredArea.js";
|
|
10
|
+
import type { IFirewallDescription, IRoleHierarchyDescription } from "../contracts/IFirewallDescription.js";
|
|
11
|
+
/**
|
|
12
|
+
* Orchestrateur de sécurité Nodefony — refonte 2026 (P6).
|
|
13
|
+
*
|
|
14
|
+
* `isSecure()` (hot-path, court-circuit si aucune zone) ne fait QUE matcher la
|
|
15
|
+
* zone et poser `context.security`. `handleSecurity()` (lazy, seulement sur une
|
|
16
|
+
* zone protégée) exécute la chaîne d'authentication selon le `mode` de la zone
|
|
17
|
+
* (`first` : le premier qui reconnaît la requête authentifie ; `all` : tous
|
|
18
|
+
* doivent passer, le dernier porte l'identité) → propage l'utilisateur dans
|
|
19
|
+
* l'ALS → applique le **Zero Trust** (zone protégée sans preuve acceptée → 401,
|
|
20
|
+
* sauf anonymat explicite via l'authenticator `anonymous`).
|
|
21
|
+
*
|
|
22
|
+
* **Fail-closed** : config invalide au boot (Zod, nom d'authenticator inconnu)
|
|
23
|
+
* → le firewall capture TOUT le trafic et répond 401 (jamais une app servie
|
|
24
|
+
* sans sa sécurité). Erreur interne pendant l'authentification (source
|
|
25
|
+
* d'identité down, câblage manquant) → log ERROR serveur + 401 générique
|
|
26
|
+
* (aucun détail ne fuite au client).
|
|
27
|
+
*
|
|
28
|
+
* Conformité : tout 401 porte un challenge `WWW-Authenticate` (RFC 7235) fourni
|
|
29
|
+
* par le premier authenticator de la zone qui en déclare un.
|
|
30
|
+
*
|
|
31
|
+
* CORS, CSRF et autorisation par décorateurs viennent se brancher en S4/S5.
|
|
32
|
+
* Toutes les structures sont **lazy** (perf : une app sans zone = zéro alloc).
|
|
33
|
+
*/
|
|
34
|
+
declare class Firewall extends Service implements IFirewall {
|
|
35
|
+
#private;
|
|
36
|
+
module: Module;
|
|
37
|
+
constructor(module: Module);
|
|
38
|
+
/** Hiérarchie de rôles résolue (niveau A de l'autorisation, P6.8). */
|
|
39
|
+
get roleHierarchy(): RoleHierarchyWalker;
|
|
40
|
+
/**
|
|
41
|
+
* `true` si l'un des rôles de l'utilisateur couvre `required` (hiérarchie
|
|
42
|
+
* comprise). Surface lue par le verrou de frame WS ({@link buildFrameAuthorizer})
|
|
43
|
+
* pour le RBAC par canal — délègue au {@link RoleHierarchyWalker}.
|
|
44
|
+
*/
|
|
45
|
+
hasRole(userRoles: readonly string[], required: string): boolean;
|
|
46
|
+
registerAuthenticator(authenticator: IAuthenticator): void;
|
|
47
|
+
getArea(name: string): ISecuredArea | undefined;
|
|
48
|
+
describe(): IFirewallDescription;
|
|
49
|
+
/**
|
|
50
|
+
* Hiérarchie de rôles déclarée + résolution transitive (data plane Studio).
|
|
51
|
+
* Brut = ce que l'app a écrit ; `inherits` = aplati précalculé par le walker.
|
|
52
|
+
*/
|
|
53
|
+
describeRoleHierarchy(): IRoleHierarchyDescription;
|
|
54
|
+
/**
|
|
55
|
+
* Ce que l'application PROTÈGE, à publier en RFC 9728 — une entrée par
|
|
56
|
+
* ressource déclarée par une zone.
|
|
57
|
+
*
|
|
58
|
+
* ⭐ **Même donnée que le défi, donc impossible qu'ils divergent.** Le `401`
|
|
59
|
+
* d'une zone porte `resource_metadata="…/.well-known/oauth-protected-resource
|
|
60
|
+
* /<chemin de `area.resource`>"` ; ce que rend cette méthode est la source de
|
|
61
|
+
* ce que `@nodefony/framework` monte à cette URL. Une seconde déclaration —
|
|
62
|
+
* une clé « ressources publiées » à côté des zones — se serait périmée au
|
|
63
|
+
* premier renommage, et le symptôme aurait été un `404` que rien n'explique.
|
|
64
|
+
*
|
|
65
|
+
* 🔴 **Les serveurs d'autorisation sont les émetteurs de confiance, pas une
|
|
66
|
+
* liste à part.** `authorization_servers` répond à « qui peut délivrer un
|
|
67
|
+
* jeton pour cette ressource ? » — c'est exactement l'allowlist
|
|
68
|
+
* `resourceServer.issuers`, la seule que le vérificateur consulte. Publier
|
|
69
|
+
* autre chose reviendrait à envoyer le client demander un jeton à un émetteur
|
|
70
|
+
* dont on refuse ensuite la signature.
|
|
71
|
+
*
|
|
72
|
+
* Aucun émetteur de confiance ⇒ **rien à publier** : un document sans serveur
|
|
73
|
+
* d'autorisation apprendrait au client qu'un jeton est nécessaire sans jamais
|
|
74
|
+
* lui dire où l'obtenir (et la RFC 9728 comme la spécification MCP l'excluent).
|
|
75
|
+
*
|
|
76
|
+
* Appelée UNE fois, au montage des routes (`onKernelReady`) — hors hot path,
|
|
77
|
+
* d'où l'allocation directe plutôt qu'un cache à invalider.
|
|
78
|
+
*
|
|
79
|
+
* @returns les ressources protégées déclarées, éventuellement vide
|
|
80
|
+
*/
|
|
81
|
+
publishedProtectedResources(): readonly IProtectedResourceInput[];
|
|
82
|
+
/**
|
|
83
|
+
* Match de zone par pathname (+ host) SANS contexte — source UNIQUE consultée
|
|
84
|
+
* par `isSecure` (HTTP) ET le verrou WebSocket (la frame `api.request` n'a
|
|
85
|
+
* qu'un path). Hot-path : patterns pré-compilés + pathname fourni → 0 alloc.
|
|
86
|
+
*/
|
|
87
|
+
matchPath(pathname: string, host?: string): SecuredArea | null;
|
|
88
|
+
/** Match rapide de zone — pose `context.security`. `true` si zone capturée. */
|
|
89
|
+
isSecure(context: ContextType): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Pipeline complet de la zone : chaîne d'authenticators (selon `mode`) → ALS
|
|
92
|
+
* → Zero Trust. Rejette (401, challenge RFC 7235 posé) ou résout.
|
|
93
|
+
*/
|
|
94
|
+
handleSecurity(context: ContextType): Promise<ContextType>;
|
|
95
|
+
/**
|
|
96
|
+
* Défense CSRF (P6 J5/étape 2) — branchée dans le pipeline HTTP de `@nodefony/http`
|
|
97
|
+
* pour TOUTE requête (zone ou non), APRÈS le resolve (les marqueurs `@CsrfProtect`/
|
|
98
|
+
* `@CsrfExempt` de la route sont disponibles). Trois rôles :
|
|
99
|
+
*
|
|
100
|
+
* - **Émission** : sur une requête SÛRE vers une route `@CsrfProtect`, minte le
|
|
101
|
+
* synchronizer token (`context.csrfToken`) — HttpContext pose ensuite le cookie
|
|
102
|
+
* lisible `csrf-token`. Sinon, hot-path GET = retour immédiat (aucun en-tête lu).
|
|
103
|
+
* - **Étape 1 (globale)** : sur une mutation, défense Fetch Metadata / Origin
|
|
104
|
+
* (rejet cross-site même sur route publique). Skippée si `@CsrfExempt` (webhook,
|
|
105
|
+
* auth par signature/clé) ou `bypassFirewall` (callbacks OAuth).
|
|
106
|
+
* - **Étape 2 (opt-in)** : sur une mutation `@CsrfProtect`, exige EN PLUS le
|
|
107
|
+
* synchronizer token (en-tête `x-csrf-token` ≡ cookie + HMAC valide).
|
|
108
|
+
*
|
|
109
|
+
* @throws CsrfError (403) — provenance tierce, ou synchronizer token absent/invalide.
|
|
110
|
+
*/
|
|
111
|
+
enforceCsrf(context: ContextType): void;
|
|
112
|
+
/**
|
|
113
|
+
* Politique CORS (P6 J5) — appelée par `HttpKernel.handleHttp()`
|
|
114
|
+
* (`http-kernel.ts:1169`), en TÊTE du pipeline : avant le routing, avant le parse
|
|
115
|
+
* du corps, donc bien avant `handleFrontController` et le firewall. Un preflight
|
|
116
|
+
* n'a pas de route déclarée — router d'abord lèverait un 405.
|
|
117
|
+
* Pose les en-têtes `Access-Control-*` et **court-circuite le
|
|
118
|
+
* preflight** `OPTIONS` en `204` (le preflight ne porte jamais de credentials,
|
|
119
|
+
* Fetch Standard → il ne doit ni router ni s'authentifier). No-op hors requête
|
|
120
|
+
* cross-origin (pas d'`Origin`), CORS désactivé, ou réponse non-HTTP (WS).
|
|
121
|
+
*
|
|
122
|
+
* @returns `204` si la requête est un preflight (l'appelant court-circuite la
|
|
123
|
+
* réponse), sinon `undefined` (la requête réelle suit le pipeline normal).
|
|
124
|
+
*/
|
|
125
|
+
handleCors(context: ContextType): number | undefined;
|
|
126
|
+
/**
|
|
127
|
+
* En-têtes de sécurité APPLICATIFS (P6 J5) — CSP, Referrer-Policy, isolation
|
|
128
|
+
* cross-origin (COOP/COEP/CORP), Origin-Agent-Cluster, Permissions-Policy.
|
|
129
|
+
* Posés sur toute réponse du pipeline (branché dans `handleHttp`). Complète le
|
|
130
|
+
* socle transport de `@nodefony/http` (nosniff/frame/HSTS, posé à l'entrée brute)
|
|
131
|
+
* SANS le ré-émettre. No-op si désactivé ou réponse non-HTTP (WS).
|
|
132
|
+
*
|
|
133
|
+
* En-têtes constants = table figée pré-calculée au boot (0 alloc/concat). Le CSP
|
|
134
|
+
* nonce/req (étape B) ajoute 1 `join` + 1 `setHeader` UNIQUEMENT si activé.
|
|
135
|
+
*/
|
|
136
|
+
applySecurityHeaders(context: ContextType): void;
|
|
137
|
+
/**
|
|
138
|
+
* Déclare des directives CSP additionnelles pour `moduleName` (cf `IFirewall`).
|
|
139
|
+
* No-op si les en-têtes applicatifs sont désactivés (pas de CSP à étendre).
|
|
140
|
+
* Recompose `#securityHeaders` (merge + re-split nonce) — hors hot-path.
|
|
141
|
+
*/
|
|
142
|
+
registerCspOrigins(moduleName: string, fragment: CspFragment): void;
|
|
143
|
+
/** Retire les directives CSP de `moduleName` et recompose si nécessaire. */
|
|
144
|
+
unregisterCspOrigins(moduleName: string): void;
|
|
145
|
+
log(pci: unknown, severity?: Severity, msgid?: Msgid, msg?: Message): Pdu;
|
|
146
|
+
}
|
|
147
|
+
export default Firewall;
|
|
148
|
+
export { Firewall };
|