@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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indique si une IP (littérale) est **non publique** (donc interdite comme cible
|
|
3
|
+
* sortante). Une IP syntaxiquement invalide est considérée bloquée (fail-closed).
|
|
4
|
+
*
|
|
5
|
+
* `node:net` `BlockList` rabat **nativement** les IPv6 **IPv4-mapped** — toutes
|
|
6
|
+
* notations confondues (`::ffff:127.0.0.1`, `::ffff:7f00:1`,
|
|
7
|
+
* `0:0:0:0:0:ffff:127.0.0.1`…) — sur les règles IPv4 : `::ffff:169.254.169.254`
|
|
8
|
+
* est bloqué sans traitement spécial (vérifié red-team `webhookSsrf.attack`).
|
|
9
|
+
* ⚠️ NE PAS ajouter `::ffff:0:0/96` à la liste : Node ferait alors matcher TOUTE
|
|
10
|
+
* adresse IPv4 (faux positif massif → 100 % des webhooks rejetés).
|
|
11
|
+
*
|
|
12
|
+
* @param ip - adresse IPv4 ou IPv6 (sans crochets).
|
|
13
|
+
*/
|
|
14
|
+
export declare function isBlockedAddress(ip: string): boolean;
|
|
15
|
+
/** Résolveur DNS injectable (testabilité) — renvoie toutes les IP d'un hôte. */
|
|
16
|
+
export type DnsResolver = (hostname: string) => Promise<string[]>;
|
|
17
|
+
/** Options de validation d'une URL sortante. */
|
|
18
|
+
export interface IAssertPublicUrlOptions {
|
|
19
|
+
/** Autorise les cibles non publiques (dev/test only). Défaut : `false`. */
|
|
20
|
+
readonly allowPrivate?: boolean;
|
|
21
|
+
/** Autorise `http:` en plus de `https:`. Défaut : `false` (https only). */
|
|
22
|
+
readonly allowHttp?: boolean;
|
|
23
|
+
/** Résolveur DNS (injectable pour les tests). Défaut : `node:dns`. */
|
|
24
|
+
readonly resolver?: DnsResolver;
|
|
25
|
+
}
|
|
26
|
+
/** Résultat d'une validation SSRF réussie. */
|
|
27
|
+
export interface IPublicUrlResult {
|
|
28
|
+
/** URL validée (normalisée). */
|
|
29
|
+
readonly url: URL;
|
|
30
|
+
/** IP résolues, toutes publiques — à **pinner** à la connexion. */
|
|
31
|
+
readonly addresses: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Valide qu'une URL sortante est sûre (anti-SSRF) : protocole autorisé, pas
|
|
35
|
+
* d'identifiants embarqués, hôte résolvable, et **toutes** les IP résolues
|
|
36
|
+
* publiques. Lève {@link SsrfError} (422) sinon.
|
|
37
|
+
*
|
|
38
|
+
* @param rawUrl - URL brute à valider.
|
|
39
|
+
* @param options - politique (https-only, deny private, résolveur).
|
|
40
|
+
* @returns l'URL normalisée + les IP résolues (pour pinning à la connexion).
|
|
41
|
+
* @throws SsrfError si l'URL est malformée ou cible une ressource interdite.
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertPublicUrl(rawUrl: string, options?: IAssertPublicUrlOptions): Promise<IPublicUrlResult>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type * as Arctic from "arctic";
|
|
2
|
+
import type { IOAuthProvider } from "../../contracts/IOAuthProvider.js";
|
|
3
|
+
/**
|
|
4
|
+
* Registre de **fabriques de fournisseurs OAuth** — résout un nom configuré
|
|
5
|
+
* (`oauth2.providers.<name>`) vers un {@link IOAuthProvider}, SANS coupler le
|
|
6
|
+
* cœur à un fournisseur en dur.
|
|
7
|
+
*
|
|
8
|
+
* Convention-frère : `tokenStoreRegistry`, `authenticatorRegistry`,
|
|
9
|
+
* `webAuthnCredentialStoreRegistry`. Les builtins (`google`, `github`) couvrent
|
|
10
|
+
* les deux archétypes (OIDC+PKCE / OAuth simple) ; une application enregistre les
|
|
11
|
+
* ~50 autres fournisseurs `arctic` (Microsoft, Apple, Discord...) ou un
|
|
12
|
+
* fournisseur maison via {@link registerOAuthProvider}, sans éditer le core.
|
|
13
|
+
*
|
|
14
|
+
* @remarks `arctic` n'est ici qu'un **type** : l'instance runtime, chargée
|
|
15
|
+
* paresseusement par `OAuth2Service` au premier login, est passée à la fabrique
|
|
16
|
+
* via {@link IOAuthProviderContext.arctic} — zéro import runtime statique.
|
|
17
|
+
*/
|
|
18
|
+
/** Contexte de construction d'un fournisseur (lib arctic chargée + secrets de config). */
|
|
19
|
+
export interface IOAuthProviderContext {
|
|
20
|
+
/** Module `arctic` chargé paresseusement (les classes de fournisseurs). */
|
|
21
|
+
readonly arctic: typeof Arctic;
|
|
22
|
+
/** Identifiant client (config, issu de l'env de l'app). */
|
|
23
|
+
readonly clientId: string;
|
|
24
|
+
/** Secret client (config) — jamais loggé. */
|
|
25
|
+
readonly clientSecret: string;
|
|
26
|
+
/** URL de callback exacte (RFC 9700). */
|
|
27
|
+
readonly redirectUri: string;
|
|
28
|
+
/**
|
|
29
|
+
* Émetteur/realm des fournisseurs OIDC self-hosted (Keycloak : URL du realm,
|
|
30
|
+
* ex. `https://kc.example/realms/app`) — `undefined` pour les fournisseurs à
|
|
31
|
+
* endpoints fixes (Google, GitHub).
|
|
32
|
+
*/
|
|
33
|
+
readonly issuer?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Fabrique d'un fournisseur OAuth pour un nom donné. */
|
|
36
|
+
export type OAuthProviderFactory = (ctx: IOAuthProviderContext) => IOAuthProvider;
|
|
37
|
+
/**
|
|
38
|
+
* Enregistre (ou remplace) la fabrique d'un fournisseur OAuth. Appelée par les
|
|
39
|
+
* builtins au chargement, et par une application pour ses fournisseurs.
|
|
40
|
+
*/
|
|
41
|
+
export declare function registerOAuthProvider(name: string, factory: OAuthProviderFactory): void;
|
|
42
|
+
/** Fabrique d'un fournisseur par nom, ou `undefined` si inconnu. */
|
|
43
|
+
export declare function getOAuthProviderFactory(name: string): OAuthProviderFactory | undefined;
|
|
44
|
+
/** Noms enregistrés (validation boot, introspection Studio, tests). */
|
|
45
|
+
export declare function listOAuthProviders(): string[];
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { IOAuthProvider } from "../../../contracts/IOAuthProvider.js";
|
|
2
|
+
import type { IOAuthProviderContext } from "../oauthProviderRegistry.js";
|
|
3
|
+
/**
|
|
4
|
+
* Fournisseur **GitHub** (OAuth 2.0 simple, NON-OIDC). Pas de PKCE, pas d'ID
|
|
5
|
+
* token : le profil est lu via l'API REST (`/user`), et l'email — souvent privé —
|
|
6
|
+
* via `/user/emails` (scope `user:email`). GitHub n'émet pas de paramètre `iss`
|
|
7
|
+
* (`expectedIssuer = null`) : la défense anti-CSRF repose sur le `state`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createGithubProvider(ctx: IOAuthProviderContext): IOAuthProvider;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { OAuth2Tokens } from "arctic";
|
|
2
|
+
import type { IOAuthProvider } from "../../../contracts/IOAuthProvider.js";
|
|
3
|
+
/**
|
|
4
|
+
* Client `arctic` minimal d'un fournisseur **OIDC avec PKCE** — surface
|
|
5
|
+
* structurelle commune à `Google`, `MicrosoftEntraId`, `Auth0`, `Okta`,
|
|
6
|
+
* `KeyCloak`... (une instance arctic de ces classes est assignable telle quelle).
|
|
7
|
+
*/
|
|
8
|
+
export interface IOidcPkceClient {
|
|
9
|
+
createAuthorizationURL(state: string, codeVerifier: string, scopes: string[]): URL;
|
|
10
|
+
validateAuthorizationCode(code: string, codeVerifier: string): Promise<OAuth2Tokens>;
|
|
11
|
+
}
|
|
12
|
+
/** Paramètres d'un fournisseur OIDC générique. */
|
|
13
|
+
export interface IOidcProviderOptions {
|
|
14
|
+
/** Nom du fournisseur (`"google"`, `"microsoft"`...) — porté dans le profil. */
|
|
15
|
+
readonly name: string;
|
|
16
|
+
/** Instance `arctic` (déjà construite avec les secrets). */
|
|
17
|
+
readonly client: IOidcPkceClient;
|
|
18
|
+
/** Émetteur attendu (claim `iss`, anti-mix-up RFC 9207). */
|
|
19
|
+
readonly issuer: string;
|
|
20
|
+
/** `arctic.decodeIdToken` (injecté — arctic est chargé paresseusement). */
|
|
21
|
+
readonly decodeIdToken: (idToken: string) => object;
|
|
22
|
+
/** Scopes par défaut si la config n'en précise aucun. */
|
|
23
|
+
readonly defaultScopes?: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fabrique un {@link IOAuthProvider} **générique OIDC** — couvre TOUT fournisseur
|
|
27
|
+
* OpenID Connect sans code spécifique : le profil se lit toujours pareil (claims
|
|
28
|
+
* standard `sub`/`email`/`email_verified`/`name` de l'ID token). Ajouter un
|
|
29
|
+
* fournisseur OIDC = une entrée de quelques lignes (nom + classe arctic + issuer),
|
|
30
|
+
* pas un fichier.
|
|
31
|
+
*
|
|
32
|
+
* PKCE S256 systématique (RFC 7636) ; le profil vient de l'ID token signé obtenu
|
|
33
|
+
* du token endpoint via TLS (décodage suffisant en code flow, RFC 8725).
|
|
34
|
+
*/
|
|
35
|
+
export declare function createOidcProvider(opts: IOidcProviderOptions): IOAuthProvider;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { IUser } from "@nodefony/user";
|
|
2
|
+
import type { IRealtimeToken } from "./realtimeContracts.js";
|
|
3
|
+
/**
|
|
4
|
+
* Adaptateur `IUser` → `IRealtimeToken` (jeton realtime).
|
|
5
|
+
*
|
|
6
|
+
* Construit UNIQUEMENT pour un utilisateur **authentifié** (le
|
|
7
|
+
* {@link FirewallRealtimeAuthenticator} ne l'instancie qu'après avoir vérifié
|
|
8
|
+
* l'identité résolue par le firewall) → `isAuthenticated()` est toujours `true`.
|
|
9
|
+
* Un visiteur non authentifié reste sur `ANONYMOUS_REALTIME_TOKEN` (posé par le
|
|
10
|
+
* hub realtime), jamais ici.
|
|
11
|
+
*
|
|
12
|
+
* Les rôles ET les scopes sont des **copies** (pas de fuite de la structure
|
|
13
|
+
* interne de l'utilisateur ni du jeton d'origine). Les deux axes voyagent :
|
|
14
|
+
* les rôles disent qui l'on est, les scopes ce qu'une clé déléguée a le droit
|
|
15
|
+
* de faire — et un agent qui perd ses scopes en passant sur la socket perd la
|
|
16
|
+
* garantie même du mode machine.
|
|
17
|
+
*/
|
|
18
|
+
export declare class UserRealtimeToken implements IRealtimeToken {
|
|
19
|
+
#private;
|
|
20
|
+
/**
|
|
21
|
+
* Mode d'authentification RÉEL de l'identité (`session`, `jwt`, `apikey`…),
|
|
22
|
+
* repris du jeton que le firewall a posé dans l'ALS.
|
|
23
|
+
*
|
|
24
|
+
* Il était jadis figé à `"session"`, ce qui faisait passer un agent authentifié
|
|
25
|
+
* par JWT pour un utilisateur à cookie — un mensonge sur l'identité, et la
|
|
26
|
+
* racine du bug qui révoquait les sockets machine à machine.
|
|
27
|
+
*/
|
|
28
|
+
readonly type: string;
|
|
29
|
+
/**
|
|
30
|
+
* @param user - identité authentifiée résolue par le firewall.
|
|
31
|
+
* @param revalidate - preuve de vie de l'identité, adaptée à son mode.
|
|
32
|
+
* @param type - mode d'authentification réel (défaut `"session"`).
|
|
33
|
+
* @param scopes - scopes délégués du jeton d'origine (défaut aucun).
|
|
34
|
+
*/
|
|
35
|
+
constructor(user: IUser, revalidate?: ((nowMs?: number) => Promise<boolean>) | null, type?: string, scopes?: readonly string[]);
|
|
36
|
+
/**
|
|
37
|
+
* Zero Trust : l'identité qui a ouvert cette socket est-elle TOUJOURS vivante ?
|
|
38
|
+
*
|
|
39
|
+
* Selon le mode : une session BFF encore ouverte et toujours celle de ce compte
|
|
40
|
+
* (détecte la déconnexion et le changement de compte sur navigateur partagé),
|
|
41
|
+
* ou un jeton porteur non expiré et non révoqué. `true` si aucune re-validation
|
|
42
|
+
* n'a pu être câblée (best-effort, jamais un faux refus).
|
|
43
|
+
*
|
|
44
|
+
* @param nowMs - horloge injectable (epoch ms) ; par défaut l'heure courante.
|
|
45
|
+
* Sert aux bornes temporelles d'un jeton, et rend le comportement testable
|
|
46
|
+
* sans dépendre de l'horloge de la machine.
|
|
47
|
+
*/
|
|
48
|
+
isValid(nowMs?: number): Promise<boolean>;
|
|
49
|
+
getUserIdentifier(): string;
|
|
50
|
+
isAuthenticated(): boolean;
|
|
51
|
+
getRoles(): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Scopes délégués — copie défensive.
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ Ils doivent être RÉELS : `ScopeVoter` ne consulte cette liste que pour un
|
|
56
|
+
* jeton machine. Une liste vide sur un jeton `jwt`/`apikey` ne veut pas dire
|
|
57
|
+
* « tous les droits », elle veut dire « aucun scope » — donc refus.
|
|
58
|
+
*/
|
|
59
|
+
getScopes(): string[];
|
|
60
|
+
getAttribute<T = unknown>(key: string): T | undefined;
|
|
61
|
+
}
|
|
62
|
+
export default UserRealtimeToken;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { FrameAuthorizer, IChannelPolicy, IRealtimeToken } from "./realtimeContracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Surface MINIMALE du firewall consommée par le verrou de frame : matcher une
|
|
4
|
+
* zone par pathname ET vérifier un rôle (hiérarchie comprise). `Firewall` la
|
|
5
|
+
* satisfait structurellement (`matchPath` + `hasRole` délégué au
|
|
6
|
+
* `RoleHierarchyWalker`) — typage local pour éviter un cycle d'import
|
|
7
|
+
* `firewall.ts` ↔ `frameAuthorizer.ts`.
|
|
8
|
+
*/
|
|
9
|
+
export interface IFrameAuthorizerFirewall {
|
|
10
|
+
matchPath(pathname: string, host?: string): {
|
|
11
|
+
readonly security: boolean;
|
|
12
|
+
} | null;
|
|
13
|
+
/** `true` si l'un des rôles de l'utilisateur couvre `required` (via hiérarchie). */
|
|
14
|
+
hasRole(userRoles: readonly string[], required: string): boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Source des politiques de canal DÉCLARÉES côté métier (`@RealtimeChannel`) —
|
|
18
|
+
* miroir partiel du `realtimeService`. `resolveChannelPolicy` est optionnel : un
|
|
19
|
+
* hub d'une version antérieure ne l'expose pas (traité comme « pas de politique
|
|
20
|
+
* métier »).
|
|
21
|
+
*/
|
|
22
|
+
export interface IChannelPolicyResolver {
|
|
23
|
+
resolveChannelPolicy?(channel: string): IChannelPolicy | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Règle de canal **système** (plateforme) — un préfixe de namespace réservé et
|
|
27
|
+
* la politique qui s'y applique. Plancher NON contournable par une déclaration
|
|
28
|
+
* métier (un controller user ne doit pas exposer `syslog:`). Surchargeable par
|
|
29
|
+
* la config (`defineSecurityConfig().realtimeChannels`).
|
|
30
|
+
*/
|
|
31
|
+
export interface ISystemChannelRule {
|
|
32
|
+
readonly prefix: string;
|
|
33
|
+
readonly policy: IChannelPolicy;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Rapporteur de refus de frame (journal d'audit P6.14) — invoqué UNIQUEMENT
|
|
37
|
+
* quand le verrou refuse une frame (cold-path : frames refusées rares). Le
|
|
38
|
+
* chemin autorisé (`return true`) ne l'appelle JAMAIS → 0 allocation sur le
|
|
39
|
+
* hot-path WS. Le firewall fournit l'implémentation (closure sur son container) ;
|
|
40
|
+
* absent par défaut (verrou pur, testable sans audit).
|
|
41
|
+
*
|
|
42
|
+
* @param surface - cible refusée : pont API souverain (`api.request`) ou canal
|
|
43
|
+
* (subscribe/inbound).
|
|
44
|
+
* @param target - pathname (api.request) ou nom de canal refusé.
|
|
45
|
+
* @param reason - raison machine stable (`zone_protected` | `channel_policy`).
|
|
46
|
+
* @param token - jeton WS de l'acteur (lu pour `getUserIdentifier()`).
|
|
47
|
+
*/
|
|
48
|
+
export type FrameDenyReporter = (surface: "api.request" | "channel", target: string, reason: string, token: IRealtimeToken) => void;
|
|
49
|
+
/**
|
|
50
|
+
* Politique système par défaut des canaux d'**introspection serveur** : réservés
|
|
51
|
+
* aux administrateurs. DURCISSEMENT Zero Trust (P6) : avant, « authentifié
|
|
52
|
+
* suffisait » (tout `ROLE_USER` lisait `nodefony:syslog`) ; désormais `ROLE_ADMIN`.
|
|
53
|
+
* Surchargeable finement par `realtimeChannels` (ex. `ROLE_SECURITY_AUDITOR`).
|
|
54
|
+
*/
|
|
55
|
+
export declare const SYSTEM_CHANNEL_POLICY: IChannelPolicy;
|
|
56
|
+
/**
|
|
57
|
+
* Namespace d'introspection serveur (observabilité) — s'y abonner expose l'état
|
|
58
|
+
* interne du pod : journaux (`nodefony:syslog`), base (`nodefony:orm:*`), métriques
|
|
59
|
+
* et supervision (`nodefony:dashboard`, `nodefony:supervision@<pid>`), sonde de la
|
|
60
|
+
* socket (`nodefony:socket`), contrôle du pod (`nodefony:kernel:gc` force un GC
|
|
61
|
+
* bloquant). Liste extensible via la config. Convention transverse Nodefony :
|
|
62
|
+
* `<module>:health` / `<module>:stats` (gérée à part dans {@link matchSystemPolicy}).
|
|
63
|
+
*
|
|
64
|
+
* ⚠️ Couplage ASSUMÉ : security connaît le namespace système de la plateforme
|
|
65
|
+
* (c'est son rôle de définir la politique) — mais il ne le REDÉCLARE pas : la
|
|
66
|
+
* constante vient du cœur, comme côté hub.
|
|
67
|
+
*/
|
|
68
|
+
export declare const DEFAULT_SYSTEM_PREFIXES: readonly ["nodefony:"];
|
|
69
|
+
/**
|
|
70
|
+
* Plancher des canaux de **sécurité** (`nodefony:audit`, P6.14 lot 4) : réservé au
|
|
71
|
+
* super-admin Nodefony (`ROLE_NODEFONY_ADMIN`) — un cran AU-DESSUS du plancher
|
|
72
|
+
* d'observabilité générique (`ROLE_ADMIN`). Le journal d'audit du pod ne se lit
|
|
73
|
+
* pas avec un simple rôle admin applicatif. Cohérent avec le data plane HTTP de
|
|
74
|
+
* l'audit (`SecurityAdminApi`, lot 3, même rôle).
|
|
75
|
+
*
|
|
76
|
+
* Multi-tenant (futur) : `nodefony:audit` reste un canal **plateforme** (pod),
|
|
77
|
+
* jamais exposé à un user tenant ; l'événement portera le `tenantId` (via l'ALS)
|
|
78
|
+
* pour permettre un filtrage par tenant quand le chantier multi-tenant arrivera.
|
|
79
|
+
*/
|
|
80
|
+
export declare const SECURITY_CHANNEL_POLICY: IChannelPolicy;
|
|
81
|
+
/**
|
|
82
|
+
* Politique du canal **MONTANT** des journaux du navigateur
|
|
83
|
+
* ({@link PLATFORM_INBOUND.syslogUplink}) : authentifié, sans rôle particulier.
|
|
84
|
+
*
|
|
85
|
+
* Pourquoi il échappe à {@link SYSTEM_CHANNEL_POLICY} alors qu'il porte la même marque :
|
|
86
|
+
* ce plancher-là protège la **lecture** de l'état interne du pod — s'abonner à
|
|
87
|
+
* `nodefony:syslog` fait sortir les journaux du serveur. Le canal montant ne rend rien ;
|
|
88
|
+
* il ACCEPTE. Lui demander `ROLE_ADMIN` ne le rendrait pas plus sûr, cela le rendrait
|
|
89
|
+
* inutile : on ne recueillerait que les erreurs survenues chez les administrateurs, quand
|
|
90
|
+
* tout l'intérêt est de voir celles que subissent les utilisateurs.
|
|
91
|
+
*
|
|
92
|
+
* Les dangers propres à une surface d'écriture — noyer le journal, y fabriquer des
|
|
93
|
+
* pistes — se traitent là où ils se posent : origine forcée par le serveur, débit et
|
|
94
|
+
* taille bornés par connexion, sévérité plafonnée (`createSyslogUplinkHandler`).
|
|
95
|
+
*
|
|
96
|
+
* Le plancher irréductible reste respecté, et non contourné : `authenticated` est exigé,
|
|
97
|
+
* donc une connexion ANONYME ne pousse rien. Limite assumée, à connaître avant de
|
|
98
|
+
* chercher un journal qui n'existe pas : **les erreurs d'un visiteur non connecté ne
|
|
99
|
+
* remontent pas** — celles de la page de connexion, notamment.
|
|
100
|
+
*/
|
|
101
|
+
export declare const UPLINK_CHANNEL_POLICY: IChannelPolicy;
|
|
102
|
+
/**
|
|
103
|
+
* F2 (revue 0.6) — PLANCHER IRRÉDUCTIBLE du namespace réservé plateforme. Il
|
|
104
|
+
* couvre tout ce qui expose l'état interne du pod (logs, audit, métriques,
|
|
105
|
+
* requêtes, supervision) : une règle de config `realtimeChannels` (placée AVANT
|
|
106
|
+
* les défauts, 1ᵉʳ match gagne) pourrait sinon l'OUVRIR à l'anonyme
|
|
107
|
+
* (`{ authenticated:false }` ou policy vide). Le plancher garantit qu'un canal de
|
|
108
|
+
* ce namespace exige TOUJOURS au moins `authenticated` — la config peut RESSERRER
|
|
109
|
+
* (rôle/scope) ou re-cibler le rôle, jamais DESCENDRE sous authenticated. Le canal
|
|
110
|
+
* d'audit en fait partie (son défaut ROLE_NODEFONY_ADMIN est déjà au-dessus, mais
|
|
111
|
+
* le plancher le blinde contre une surcharge de config). Défense structurelle,
|
|
112
|
+
* fail-closed (cf F1 fail-loud).
|
|
113
|
+
*/
|
|
114
|
+
export declare const RESERVED_FLOOR_PREFIXES: readonly ["nodefony:"];
|
|
115
|
+
/**
|
|
116
|
+
* Règles système par défaut. Le canal d'audit est placé EN TÊTE (1ᵉʳ match gagne)
|
|
117
|
+
* avec son plancher super-admin propre ; le reste du namespace plateforme hérite de
|
|
118
|
+
* {@link SYSTEM_CHANNEL_POLICY}. Le firewall y préfixe les règles issues de la
|
|
119
|
+
* config (qui gagnent par ordre).
|
|
120
|
+
*/
|
|
121
|
+
export declare const DEFAULT_SYSTEM_RULES: readonly ISystemChannelRule[];
|
|
122
|
+
/**
|
|
123
|
+
* Construit les règles système à partir d'une liste de namespaces réservés.
|
|
124
|
+
*
|
|
125
|
+
* Sépare la LISTE (quels namespaces sont réservés — propriété du hub realtime,
|
|
126
|
+
* qui sert ces canaux) de la POLITIQUE (quels droits — propriété de la sécurité).
|
|
127
|
+
* Le firewall appelle donc cette fabrique avec la liste que le hub lui donne, et
|
|
128
|
+
* ne redéclare rien : un namespace ajouté côté realtime hérite automatiquement
|
|
129
|
+
* d'une politique, au lieu de rester ouvert sans que personne ne le remarque.
|
|
130
|
+
*
|
|
131
|
+
* Le canal d'audit est placé EN TÊTE (premier match gagnant) : son plancher est
|
|
132
|
+
* plus haut que celui du reste de l'observabilité. Il n'est pas un namespace mais
|
|
133
|
+
* un canal précis — sa règle n'est donc posée que si la liste reçue le COUVRE : si
|
|
134
|
+
* le hub cessait un jour de réserver ce territoire, la sécurité cesserait avec lui
|
|
135
|
+
* de prétendre l'arbitrer, au lieu de garder une règle orpheline.
|
|
136
|
+
*
|
|
137
|
+
* @param prefixes - namespaces réservés (ordre indifférent).
|
|
138
|
+
* @returns les règles, canal d'audit d'abord.
|
|
139
|
+
*/
|
|
140
|
+
export declare function buildSystemRules(prefixes: readonly string[]): readonly ISystemChannelRule[];
|
|
141
|
+
/**
|
|
142
|
+
* Construit le verrou de frame WS branché sur le hub realtime par le firewall au
|
|
143
|
+
* boot (`RealtimeService.setFrameAuthorizer`). SYNC, 0 lecture base : lit le
|
|
144
|
+
* token déjà résolu au handshake et matche la cible de la frame contre la zone
|
|
145
|
+
* (api.request) ou la politique du canal (subscribe/inbound).
|
|
146
|
+
*
|
|
147
|
+
* Trois surfaces gardées :
|
|
148
|
+
* - `api.request {path}` (pont API souverain) : re-match de zone HTTP → zone
|
|
149
|
+
* protégée + anonyme = refus (autorisation de ZONE, identique à `GET {path}` ;
|
|
150
|
+
* la méthode/action est gardée en aval par le router + `@IsGranted`).
|
|
151
|
+
* - `subscribe {channel}` : politique de canal (système plancher + déclaration
|
|
152
|
+
* métier `@RealtimeChannel` → rôles/scopes).
|
|
153
|
+
* - inbound (`method` = canal full-duplex déclaré avec policy) : même politique
|
|
154
|
+
* que `subscribe` — un client ne pousse pas sur un canal protégé sans droit.
|
|
155
|
+
*
|
|
156
|
+
* Toute autre frame (`ping`, `unsubscribe`, action explicitement ouverte) passe — le verrou
|
|
157
|
+
* cible les surfaces qui atteignent le data plane / l'observabilité / un canal
|
|
158
|
+
* protégé.
|
|
159
|
+
*
|
|
160
|
+
* @param firewall - matcher de zone + checker de rôle (le `Firewall`).
|
|
161
|
+
* @param options - `channelResolver` (politiques métier déclarées, via le
|
|
162
|
+
* service realtime) + `systemRules` (défauts + config) +
|
|
163
|
+
* `onDeny` (rapporteur d'audit, invoqué sur refus seulement).
|
|
164
|
+
* @returns un {@link FrameAuthorizer} sync (`true` = frame autorisée).
|
|
165
|
+
*/
|
|
166
|
+
export declare function buildFrameAuthorizer(firewall: IFrameAuthorizerFirewall, options?: {
|
|
167
|
+
readonly channelResolver?: IChannelPolicyResolver | null;
|
|
168
|
+
readonly systemRules?: readonly ISystemChannelRule[];
|
|
169
|
+
readonly onDeny?: FrameDenyReporter;
|
|
170
|
+
}): FrameAuthorizer;
|
|
171
|
+
export default buildFrameAuthorizer;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Miroirs STRUCTURELS des contrats `@nodefony/realtime` — **zéro import** du
|
|
3
|
+
* module realtime (ni runtime ni type-only).
|
|
4
|
+
*
|
|
5
|
+
* POURQUOI : `@nodefony/security` et `@nodefony/realtime` **ne se dépendent pas**
|
|
6
|
+
* (deux `package.json` disjoints, vérifié). Le câblage du verrou WS se fait par
|
|
7
|
+
* **nom de service** (`container.get("realtimeService")`), pas par import — donc
|
|
8
|
+
* security décrit localement la SURFACE qu'il consomme. Le typage structurel de
|
|
9
|
+
* TypeScript fait le pont : un objet security (authenticator, token) dont la forme
|
|
10
|
+
* coïncide est accepté à l'exécution par realtime (duck typing). Convention-frère :
|
|
11
|
+
* `ISessionAuthFlow` (framework décrit la surface http) et le seam realtime↔security
|
|
12
|
+
* côté realtime (`IRealtimeToken` y est déjà décrit comme « sous-ensemble de IToken »).
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ Ces interfaces DOIVENT rester alignées sur celles de
|
|
15
|
+
* `@nodefony/realtime/nodefony/interfaces/*` — toute dérive de forme casserait le
|
|
16
|
+
* pont silencieusement (test de compat dans le banc realtime du verrou).
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Jeton realtime — identité minimale d'une connexion WS (miroir de
|
|
20
|
+
* `IRealtimeToken`). Posé au handshake, lu en O(1) par le verrou de frame.
|
|
21
|
+
*/
|
|
22
|
+
export interface IRealtimeToken {
|
|
23
|
+
readonly type: string;
|
|
24
|
+
getUserIdentifier(): string;
|
|
25
|
+
isAuthenticated(): boolean;
|
|
26
|
+
getRoles(): string[];
|
|
27
|
+
getScopes(): string[];
|
|
28
|
+
getAttribute<T = unknown>(key: string): T | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Re-validation Zero Trust à l'usage (miroir de `IRealtimeToken` realtime) —
|
|
31
|
+
* `false` = identité périmée (session morte / compte changé). Optionnel
|
|
32
|
+
* (absent = valide). Appelé par le pont `api.request` avant l'action data plane.
|
|
33
|
+
*/
|
|
34
|
+
isValid?(nowMs?: number): boolean | Promise<boolean>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Données de handshake WS (miroir de `IRealtimeHandshake`) — DTO neutre passé à
|
|
38
|
+
* un authenticator. Cookies déjà parsés en `Map<name, value>`.
|
|
39
|
+
*/
|
|
40
|
+
export interface IRealtimeHandshake {
|
|
41
|
+
readonly headers: Readonly<Record<string, string | string[] | undefined>>;
|
|
42
|
+
readonly cookies: ReadonlyMap<string, string>;
|
|
43
|
+
readonly url: string;
|
|
44
|
+
readonly remoteAddress: string;
|
|
45
|
+
readonly origin?: string;
|
|
46
|
+
readonly protocols: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Stratégie d'auth au handshake WS (miroir de `IRealtimeAuthenticator`,
|
|
50
|
+
* pattern Symfony `supports/authenticate`).
|
|
51
|
+
*/
|
|
52
|
+
export interface IRealtimeAuthenticator {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
supports(handshake: IRealtimeHandshake): boolean;
|
|
55
|
+
authenticate(handshake: IRealtimeHandshake): Promise<IRealtimeToken>;
|
|
56
|
+
onSuccess?(handshake: IRealtimeHandshake, token: IRealtimeToken): void;
|
|
57
|
+
onFailure?(handshake: IRealtimeHandshake, error: Error): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Sélecteur de zone WS (miroir de `IRealtimeAuthenticatorMatcher`) — pattern
|
|
61
|
+
* d'URL (string|RegExp) + vhost optionnel. Parité avec `ISecuredArea.host`.
|
|
62
|
+
*/
|
|
63
|
+
export interface IRealtimeAuthenticatorMatcher {
|
|
64
|
+
readonly pattern: string | RegExp;
|
|
65
|
+
readonly host?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Verrou de frame (miroir de `FrameAuthorizer`) — SYNC : `true` = frame
|
|
69
|
+
* autorisée. Lit le token déjà résolu au handshake (0 lecture base par frame).
|
|
70
|
+
*/
|
|
71
|
+
export type FrameAuthorizer = (frame: unknown, token: IRealtimeToken) => boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Politique d'autorisation d'un **canal** (miroir de `ChannelPolicy` realtime) —
|
|
74
|
+
* exigences à satisfaire pour `subscribe`/inbound. Toutes les contraintes posées
|
|
75
|
+
* sont cumulatives (ET) ; un champ absent = pas de contrainte sur cet axe. Une
|
|
76
|
+
* politique entièrement vide = canal libre (équivaut à `null`).
|
|
77
|
+
*
|
|
78
|
+
* Résolue 1× au `subscribe` (cold path : un client s'abonne rarement), comparée
|
|
79
|
+
* au token déjà chargé au handshake → 0 lecture base. Deux origines : déclaration
|
|
80
|
+
* **métier** (`@RealtimeChannel(name, opts)`, portée par le hub realtime) et
|
|
81
|
+
* **plateforme** (`defineSecurityConfig().realtimeChannels` + namespaces système
|
|
82
|
+
* réservés, portée par security — cf {@link buildFrameAuthorizer}).
|
|
83
|
+
*/
|
|
84
|
+
export interface IChannelPolicy {
|
|
85
|
+
/** Exige une connexion authentifiée (token non anonyme). */
|
|
86
|
+
readonly authenticated?: boolean;
|
|
87
|
+
/** Un de ces rôles suffit (évalué AVEC la hiérarchie de rôles du firewall). */
|
|
88
|
+
readonly roles?: readonly string[];
|
|
89
|
+
/** Un de ces scopes suffit (axe API : JWT/clé API ; session BFF n'en porte pas). */
|
|
90
|
+
readonly scopes?: readonly string[];
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Surface consommée du `realtimeService` (miroir partiel de `RealtimeService`) —
|
|
94
|
+
* seulement les seams que security câble/lit au boot et au dispatch. Résolu par
|
|
95
|
+
* nom via le container (`container.get<IRealtimeService>("realtimeService")`).
|
|
96
|
+
*/
|
|
97
|
+
export interface IRealtimeService {
|
|
98
|
+
/** Seam #2/#3 — enregistre un authenticator pour les handshakes WS matchés. */
|
|
99
|
+
useAuthenticator(matcher: IRealtimeAuthenticatorMatcher, authenticator: IRealtimeAuthenticator): void;
|
|
100
|
+
/**
|
|
101
|
+
* Seam #1 — pose le verrou de frame (hot-path `beforeDispatch`).
|
|
102
|
+
*
|
|
103
|
+
* `options.silentProbe` porte la MÊME décision sans rapporter de refus : le
|
|
104
|
+
* `realtime:welcome` interroge N canaux à chaque connexion pour n'annoncer que
|
|
105
|
+
* ce qui est obtenable, et passer par le verrou y écrirait N refus au journal
|
|
106
|
+
* d'audit pour des demandes que personne n'a faites. Optionnel côté appelé :
|
|
107
|
+
* un module realtime d'une version antérieure l'ignore, et l'annonce reste
|
|
108
|
+
* alors ce qu'elle était — jamais une erreur.
|
|
109
|
+
*/
|
|
110
|
+
setFrameAuthorizer(authorizer: FrameAuthorizer | null, options?: {
|
|
111
|
+
readonly silentProbe?: FrameAuthorizer | null;
|
|
112
|
+
}): void;
|
|
113
|
+
/**
|
|
114
|
+
* Seam #1b — politique de canal **déclarée côté métier** (`@RealtimeChannel`),
|
|
115
|
+
* agrégée par le hub. `null`/absent = aucune politique métier pour ce canal
|
|
116
|
+
* (security retombe alors sur sa politique plateforme/système). Optionnel : un
|
|
117
|
+
* hub d'une version antérieure ne l'expose pas → traité comme `undefined`.
|
|
118
|
+
*/
|
|
119
|
+
resolveChannelPolicy?(channel: string): IChannelPolicy | null;
|
|
120
|
+
/**
|
|
121
|
+
* Namespaces de canaux **réservés à la plateforme**, tels que le hub les connaît
|
|
122
|
+
* (`syslog:`, `orm:`, `kernel:`…). Le firewall y accroche ses politiques au lieu
|
|
123
|
+
* d'en tenir un second inventaire : deux listes auraient divergé au premier
|
|
124
|
+
* namespace ajouté côté realtime, et le namespace neuf serait resté **sans
|
|
125
|
+
* politique** — un trou silencieux. Le hub possède la liste, la sécurité possède
|
|
126
|
+
* les rôles.
|
|
127
|
+
*
|
|
128
|
+
* Optionnel : un hub d'une version antérieure ne l'expose pas → repli sur la
|
|
129
|
+
* liste locale ({@link RESERVED_FLOOR_PREFIXES}).
|
|
130
|
+
*/
|
|
131
|
+
reservedSystemPrefixes?(): readonly string[];
|
|
132
|
+
/**
|
|
133
|
+
* Seam canal SYSTÈME (P6.14 lot 4) — enregistre la factory d'un canal plateforme
|
|
134
|
+
* (`nodefony:audit`) sur le hub, sans qu'aucun controller ne le connaisse. Lazy :
|
|
135
|
+
* le provider est créé au 1ᵉʳ abonné, `dispose` au dernier. Optionnel : un hub
|
|
136
|
+
* d'une version antérieure ne l'expose pas → security s'abstient (canal absent).
|
|
137
|
+
*/
|
|
138
|
+
registerSystemChannel?(channel: string, factory: (channel: string, publish: (channel: string, payload: unknown) => void) => (() => void) | null): void;
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IUser, IUserProvider } from "@nodefony/user";
|
|
2
|
+
/**
|
|
3
|
+
* Résout l'identité portée par une session (l'identifiant stocké dans le blob)
|
|
4
|
+
* en utilisateur VIVANT — re-fetch systématique auprès du provider.
|
|
5
|
+
*
|
|
6
|
+
* C'est LE choix structurant de la session BFF : la session ne stocke que
|
|
7
|
+
* l'identifiant, jamais l'utilisateur sérialisé. Rôles toujours frais, et un
|
|
8
|
+
* compte verrouillé/désactivé entre deux requêtes est rejeté immédiatement
|
|
9
|
+
* (révocation effective — l'argument décisif face au JWT côté web).
|
|
10
|
+
*
|
|
11
|
+
* Partagé par le `SessionAuthenticator` (requêtes en zone) et `AuthFlow.me()`
|
|
12
|
+
* (hors zone) : une seule source de vérité des contrôles d'état du compte.
|
|
13
|
+
*
|
|
14
|
+
* @param provider - source d'identité (`UserService` via le container).
|
|
15
|
+
* @param identifier - identifiant fonctionnel stocké en session.
|
|
16
|
+
* @returns l'utilisateur actif.
|
|
17
|
+
* @throws AuthenticationError (401, message uniforme) — identifiant inconnu,
|
|
18
|
+
* compte verrouillé ou désactivé.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveSessionIdentity(provider: IUserProvider, identifier: string): Promise<IUser>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Politique de backoff du throttling de login (NIST SP 800-63B §5.2.2).
|
|
3
|
+
*
|
|
4
|
+
* JAMAIS de verrouillage dur : un lockout au N-ième échec offrirait à
|
|
5
|
+
* l'attaquant un déni de service gratuit sur le compte de sa victime (5 mauvais
|
|
6
|
+
* essais suffiraient à l'exclure). Le délai PROGRESSIF rend le brute-force
|
|
7
|
+
* impraticable (le coût croît exponentiellement) sans donner ce levier — le
|
|
8
|
+
* titulaire légitime attend au pire `capDelayS`, jamais un déblocage admin.
|
|
9
|
+
*/
|
|
10
|
+
export interface ILoginThrottleOptions {
|
|
11
|
+
/** Échecs consécutifs tolérés sans délai (défaut 3 — fautes de frappe). */
|
|
12
|
+
freeAttempts?: number;
|
|
13
|
+
/** Délai (s) après `freeAttempts` — double à chaque échec suivant (défaut 1). */
|
|
14
|
+
baseDelayS?: number;
|
|
15
|
+
/** Plafond du délai (s) — défaut 900 (15 min). */
|
|
16
|
+
capDelayS?: number;
|
|
17
|
+
/** Borne du nombre d'identifiants suivis (anti-fuite mémoire, défaut 10 000). */
|
|
18
|
+
maxTracked?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Limiteur de tentatives de login **en mémoire, par identifiant saisi**.
|
|
22
|
+
*
|
|
23
|
+
* La clé est l'identifiant TEL QUE SAISI (existant ou non en base) : un
|
|
24
|
+
* identifiant martelé est ralenti qu'il corresponde à un compte ou pas — aucun
|
|
25
|
+
* oracle d'énumération. Par-processus (V1) : en cluster, chaque worker porte son
|
|
26
|
+
* compteur (l'attaquant gagne ×N workers — acceptable car le backoff est
|
|
27
|
+
* exponentiel ; un backend partagé type Redis se branchera derrière la même
|
|
28
|
+
* interface sans toucher l'authenticator).
|
|
29
|
+
*
|
|
30
|
+
* Coût borné (règle perf Nodefony) : `Map` allouée LAZY au premier échec (une
|
|
31
|
+
* app sans échec de login ne paie rien), AUCUN timer — l'expiration est évaluée
|
|
32
|
+
* à la lecture et les entrées mortes sont purgées par balayage opportuniste
|
|
33
|
+
* quand la borne `maxTracked` est atteinte (puis éviction FIFO en dernier
|
|
34
|
+
* recours : ~10 000 × ~100 B ≈ 1 MB au pire).
|
|
35
|
+
*/
|
|
36
|
+
export declare class LoginThrottler {
|
|
37
|
+
#private;
|
|
38
|
+
readonly freeAttempts: number;
|
|
39
|
+
readonly baseDelayS: number;
|
|
40
|
+
readonly capDelayS: number;
|
|
41
|
+
readonly maxTracked: number;
|
|
42
|
+
constructor(options?: ILoginThrottleOptions, now?: () => number);
|
|
43
|
+
/**
|
|
44
|
+
* L'identifiant peut-il tenter un login maintenant ?
|
|
45
|
+
*
|
|
46
|
+
* @param identifier - identifiant tel que saisi.
|
|
47
|
+
* @returns `0` si autorisé, sinon les secondes restantes (→ `Retry-After`,
|
|
48
|
+
* arrondies à l'entier supérieur).
|
|
49
|
+
*/
|
|
50
|
+
check(identifier: string): number;
|
|
51
|
+
/**
|
|
52
|
+
* Enregistre un échec : au-delà de `freeAttempts`, arme un délai exponentiel
|
|
53
|
+
* `baseDelayS × 2^(échecs - freeAttempts - 1)`, plafonné à `capDelayS`.
|
|
54
|
+
*
|
|
55
|
+
* @param identifier - identifiant tel que saisi.
|
|
56
|
+
*/
|
|
57
|
+
recordFailure(identifier: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Login réussi : oublie l'identifiant (le délai repart de zéro — NIST :
|
|
60
|
+
* l'utilisateur légitime ne traîne pas la dette d'un attaquant passé).
|
|
61
|
+
*
|
|
62
|
+
* @param identifier - identifiant tel que saisi.
|
|
63
|
+
*/
|
|
64
|
+
recordSuccess(identifier: string): void;
|
|
65
|
+
/** Nombre d'identifiants actuellement suivis (introspection / tests). */
|
|
66
|
+
get trackedCount(): number;
|
|
67
|
+
}
|
|
68
|
+
export default LoginThrottler;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IUser } from "@nodefony/user";
|
|
2
|
+
import type { IToken } from "../../contracts/IToken.js";
|
|
3
|
+
/**
|
|
4
|
+
* Token du visiteur non authentifié — Zero Trust : un visiteur EST un utilisateur
|
|
5
|
+
* anonyme (jamais `null`).
|
|
6
|
+
*
|
|
7
|
+
* Porte le singleton gelé `anonymousUser` → **zéro allocation d'utilisateur** par
|
|
8
|
+
* requête non authentifiée (hot path). Les attributs sont lazy (`null` tant qu'on
|
|
9
|
+
* n'en pose pas).
|
|
10
|
+
*/
|
|
11
|
+
export declare class AnonymousToken implements IToken {
|
|
12
|
+
#private;
|
|
13
|
+
readonly type = "anonymous";
|
|
14
|
+
getUser(): IUser;
|
|
15
|
+
getUserIdentifier(): string;
|
|
16
|
+
isAuthenticated(): boolean;
|
|
17
|
+
getRoles(): string[];
|
|
18
|
+
getCredentials(): unknown;
|
|
19
|
+
getScopes(): string[];
|
|
20
|
+
getAttribute<T = unknown>(key: string): T | undefined;
|
|
21
|
+
setAttribute(key: string, value: unknown): void;
|
|
22
|
+
}
|
|
23
|
+
export default AnonymousToken;
|