@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,132 @@
|
|
|
1
|
+
import { type Container } from "nodefony";
|
|
2
|
+
import type { ContextType } from "@nodefony/http";
|
|
3
|
+
import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
|
|
4
|
+
import type { ISecuredArea } from "../../contracts/ISecuredArea.js";
|
|
5
|
+
import type { IToken } from "../../contracts/IToken.js";
|
|
6
|
+
import { type ExternalSubjectMapping } from "./externalSubject.js";
|
|
7
|
+
/** Comment le sujet d'un jeton devient un utilisateur de cette application. */
|
|
8
|
+
export type ExternalSubjectPolicy = "require" | "ephemeral";
|
|
9
|
+
/** Un émetteur reconnu, et la façon dont ses sujets entrent chez nous. */
|
|
10
|
+
export interface IExternalIssuerBinding {
|
|
11
|
+
/** Émetteur de confiance, sous sa forme canonique. */
|
|
12
|
+
issuer: string;
|
|
13
|
+
/** Comment le `sub` de CET émetteur devient un identifiant local. */
|
|
14
|
+
subjectMapping: ExternalSubjectMapping;
|
|
15
|
+
}
|
|
16
|
+
/** Ce que la fabrique doit fournir à l'authenticator. */
|
|
17
|
+
export interface IExternalJwtAuthenticatorOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Émetteurs de confiance, et leur politique de sujet.
|
|
20
|
+
*
|
|
21
|
+
* La liste sert à deux choses, qu'il ne faut pas confondre : reconnaître les
|
|
22
|
+
* jetons qui relèvent de cet authenticator (aiguillage — elle n'accorde
|
|
23
|
+
* rien, le vérificateur refait le contrôle sur sa propre liste), et savoir
|
|
24
|
+
* dans quel espace de noms lire le sujet de chacun.
|
|
25
|
+
*/
|
|
26
|
+
issuers: readonly IExternalIssuerBinding[];
|
|
27
|
+
/** Politique de rattachement du sujet à un utilisateur local. */
|
|
28
|
+
subjectPolicy: ExternalSubjectPolicy;
|
|
29
|
+
/** Rôles accordés en mode `ephemeral`. */
|
|
30
|
+
ephemeralRoles: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Authentification par **jeton d'accès émis par un serveur d'autorisation
|
|
34
|
+
* TIERS** (Keycloak, Auth0, Entra, ou l'émetteur d'une flotte d'agents).
|
|
35
|
+
*
|
|
36
|
+
* C'est le chaînon qui relie deux pièces déjà en place : le vérificateur de
|
|
37
|
+
* jetons distants, qui sait lire un jeton dont on ne possède pas la clé, et le
|
|
38
|
+
* pare-feu, qui raisonne en utilisateurs et en rôles. Le vérificateur s'arrête
|
|
39
|
+
* à un sujet et des scopes — délibérément, car établir une identité
|
|
40
|
+
* applicative est une décision de l'application, pas du protocole. C'est cette
|
|
41
|
+
* décision-là que porte cette classe, et rien d'autre.
|
|
42
|
+
*
|
|
43
|
+
* ## Cohabitation avec les jetons maison
|
|
44
|
+
*
|
|
45
|
+
* `JwtAuthenticator` et celui-ci reconnaissent la même forme de credential.
|
|
46
|
+
* Chacun ne prend donc que les jetons dont l'émetteur revendiqué est le sien
|
|
47
|
+
* ({@link peekIssuer}) — lecture non vérifiée qui ne sert qu'à AIGUILLER. Sans
|
|
48
|
+
* cela, en mode `first`, le premier listé capturerait les deux familles et
|
|
49
|
+
* refuserait la moitié des jetons : l'ordre de la configuration deviendrait
|
|
50
|
+
* une décision de sécurité, dont l'erreur ne se verrait qu'en production.
|
|
51
|
+
*
|
|
52
|
+
* ## Ce qui vaut garantie
|
|
53
|
+
*
|
|
54
|
+
* - **L'audience vient de la ZONE**, jamais du jeton, et elle est obligatoire :
|
|
55
|
+
* sans elle l'authenticator refuse de démarrer ({@link validateArea}).
|
|
56
|
+
* - **Un refus est un 401 uniforme ; une PANNE est un 503** — un émetteur
|
|
57
|
+
* injoignable n'est pas un jeton invalide, et le dire autrement enverrait le
|
|
58
|
+
* client renouveler en boucle un jeton parfaitement bon.
|
|
59
|
+
* - **Le sujet est revérifié localement** en mode `require` : un compte
|
|
60
|
+
* supprimé, désactivé ou verrouillé ferme l'accès sans attendre l'expiration
|
|
61
|
+
* du jeton, que l'application ne contrôle pas.
|
|
62
|
+
*
|
|
63
|
+
* - **Le sujet n'entre jamais nu dans l'espace de noms local** : un `sub` n'est
|
|
64
|
+
* unique que chez son émetteur, et le rattachement passe donc par
|
|
65
|
+
* {@link localIdentifierFor}, piloté par le `subjectMapping` de CET émetteur.
|
|
66
|
+
* - **Le refus est apprenable** : le défi porte le pointeur `resource_metadata`
|
|
67
|
+
* (RFC 9728), qui dit au client où aller chercher de quoi obtenir un jeton.
|
|
68
|
+
*/
|
|
69
|
+
export declare class ExternalJwtAuthenticator implements IAuthenticator {
|
|
70
|
+
#private;
|
|
71
|
+
readonly name = "external-jwt";
|
|
72
|
+
/**
|
|
73
|
+
* @param container - container DI (résolution lazy du vérificateur et de `users`)
|
|
74
|
+
* @param options - émetteurs reconnus et politique de rattachement
|
|
75
|
+
*/
|
|
76
|
+
constructor(container: Container, options: IExternalJwtAuthenticatorOptions);
|
|
77
|
+
/**
|
|
78
|
+
* La requête porte-t-elle un jeton se réclamant d'un émetteur de confiance ?
|
|
79
|
+
*
|
|
80
|
+
* Le contrôle est délibérément le MÊME que celui du vérificateur, et non un
|
|
81
|
+
* simple « c'est un JWT » : un jeton maison ne doit pas être capturé ici, et
|
|
82
|
+
* un jeton d'un émetteur inconnu n'a pas à provoquer le moindre travail.
|
|
83
|
+
*/
|
|
84
|
+
supports(context: ContextType): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Extrait le jeton brut ET l'audience de la zone.
|
|
87
|
+
*
|
|
88
|
+
* L'audience transite par le token parce que `authenticate()` ne reçoit pas
|
|
89
|
+
* le contexte : c'est ici, et seulement ici, qu'on sait quelle ressource est
|
|
90
|
+
* visée.
|
|
91
|
+
*/
|
|
92
|
+
createToken(context: ContextType): Promise<IToken>;
|
|
93
|
+
/**
|
|
94
|
+
* Vérifie le jeton auprès de son émetteur, puis rattache le sujet.
|
|
95
|
+
*
|
|
96
|
+
* @throws AuthenticationError (401) — jeton refusé, ou sujet sans compte
|
|
97
|
+
* local utilisable
|
|
98
|
+
* @throws UnverifiableTokenError (503) — rien ne peut vérifier ce jeton, ou
|
|
99
|
+
* l'émetteur est injoignable : on ne sait pas, et on le dit
|
|
100
|
+
*/
|
|
101
|
+
authenticate(token: IToken): Promise<IToken>;
|
|
102
|
+
/** Slot audit — le firewall enregistre déjà succès et échec par zone. */
|
|
103
|
+
onSuccess(_context: ContextType, _token: IToken): Promise<void>;
|
|
104
|
+
/** Slot audit — le 401 et le défi sont posés par le firewall. */
|
|
105
|
+
onFailure(_context: ContextType, _error: Error): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Défi RFC 6750 + pointeur RFC 9728, posé par le firewall sur les 401.
|
|
108
|
+
*
|
|
109
|
+
* ⭐ **C'est cet en-tête qui rend l'autorisation apprenable.** Un `Bearer` nu
|
|
110
|
+
* est un mur : le client sait qu'il lui faut un jeton, mais pas où le
|
|
111
|
+
* demander. `resource_metadata` nomme le document qui le lui dira — c'est le
|
|
112
|
+
* seul mécanisme normalisé pour ça, et celui qu'un client MCP conforme suit.
|
|
113
|
+
*
|
|
114
|
+
* Aucun `error` n'est joint : le firewall pose ce défi sur TOUT 401 de la
|
|
115
|
+
* zone, sans savoir si la requête portait un jeton. Or la RFC 6750 §3
|
|
116
|
+
* demande de ne PAS mettre de code d'erreur quand elle n'en portait aucun —
|
|
117
|
+
* un `invalid_token` ferait renouveler en boucle un jeton qui n'existe pas.
|
|
118
|
+
*
|
|
119
|
+
* @param area - zone refusante ; sans elle (ou sans ressource déclarée) le
|
|
120
|
+
* défi retombe sur `Bearer` nu, faute de ressource à nommer
|
|
121
|
+
*/
|
|
122
|
+
challenge(area?: ISecuredArea): string;
|
|
123
|
+
/**
|
|
124
|
+
* Refuse une zone sans ressource — au boot, pas à la première requête.
|
|
125
|
+
*
|
|
126
|
+
* Sans audience, la vérification accepterait un jeton émis pour un autre
|
|
127
|
+
* service : le seul verrou qui lie un jeton à CE service disparaîtrait, et
|
|
128
|
+
* l'application n'en saurait rien.
|
|
129
|
+
*/
|
|
130
|
+
validateArea(area: ISecuredArea): void;
|
|
131
|
+
}
|
|
132
|
+
export default ExternalJwtAuthenticator;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { IRealtimeAuthenticator, IRealtimeHandshake, IRealtimeToken } from "../realtime/realtimeContracts.js";
|
|
2
|
+
/**
|
|
3
|
+
* Authenticator realtime des identités résolues par le **firewall** — équivalent
|
|
4
|
+
* WS de tout ce que le pipeline HTTP sait authentifier.
|
|
5
|
+
*
|
|
6
|
+
* ── Pourquoi il NE re-lit PAS la base ──────────────────────────────────────
|
|
7
|
+
* Un handshake WebSocket est une requête upgrade HTTP qui traverse le MÊME
|
|
8
|
+
* pipeline : `startSession` (reprise L1 du cookie) **puis** `firewall.handleSecurity`
|
|
9
|
+
* tournent AVANT que le `RealtimeController` ne fasse son handshake. Sur une zone
|
|
10
|
+
* data plane, le firewall a donc DÉJÀ : (1) authentifié (session, JWT, clé API…),
|
|
11
|
+
* (2) re-résolu l'identité via le provider `users` (rôles frais), (3) posé
|
|
12
|
+
* l'`IUser` **et le jeton** dans l'ALS et appliqué le Zero Trust (un anonyme est
|
|
13
|
+
* fermé AVANT d'arriver ici). Re-décoder le credential ici referait des lectures
|
|
14
|
+
* base **redondantes** par connexion — sur le différenciateur temps réel, un coût
|
|
15
|
+
* évitable. → on **réutilise** l'identité déjà en ALS.
|
|
16
|
+
*
|
|
17
|
+
* Le `RealtimeController.onHandshake` s'exécute dans la même bulle ALS que le
|
|
18
|
+
* firewall (un seul `RequestContext.run` enveloppe handshake + frames) → la
|
|
19
|
+
* lecture est sûre et synchrone.
|
|
20
|
+
*
|
|
21
|
+
* ── Il n'est PAS l'authenticator « de la session » ──────────────────────────
|
|
22
|
+
* Son nom d'origine (`SessionRealtimeAuthenticator`) décrivait le premier mode
|
|
23
|
+
* branché, pas son rôle : il promeut **toute** identité que le firewall a posée,
|
|
24
|
+
* y compris un agent authentifié par jeton porteur, sans cookie ni session. La
|
|
25
|
+
* confusion a coûté cher — un durcissement pensé pour la session a été appliqué
|
|
26
|
+
* à toutes les identités, et une connexion JWT parfaitement valide se faisait
|
|
27
|
+
* révoquer au motif qu'elle n'avait pas de session. D'où le nom actuel : il dit
|
|
28
|
+
* d'où vient l'identité (le firewall), pas comment elle a été prouvée.
|
|
29
|
+
*
|
|
30
|
+
* ── Révocation : un invariant, deux preuves ────────────────────────────────
|
|
31
|
+
* L'invariant est unique — **une socket ne survit pas à l'identité qui l'a
|
|
32
|
+
* ouverte** — mais la preuve dépend du mode, parce que ce sont deux mécanismes
|
|
33
|
+
* de révocation différents :
|
|
34
|
+
*
|
|
35
|
+
* | Mode | Ce qui rend l'identité morte |
|
|
36
|
+
* | -------------------------- | ------------------------------------------------ |
|
|
37
|
+
* | session BFF (`session`) | session détruite, expirée, ou passée à un autre |
|
|
38
|
+
* | jeton porteur (JWT, clé…) | `exp` atteint · `jti` denylisté · `invalidBefore` |
|
|
39
|
+
*
|
|
40
|
+
* Le jeton est figé au handshake (les frames lisent un cache O(1), jamais la
|
|
41
|
+
* base) ; la re-validation tourne sur le tick du hub (`REVOCATION_REVALIDATE_MS`)
|
|
42
|
+
* et devant chaque `api.request`. Une révocation prend donc effet en une fenêtre,
|
|
43
|
+
* pas à la frame suivante — c'est l'état de l'art (Socket.IO/Phoenix figent aussi
|
|
44
|
+
* l'identité au handshake).
|
|
45
|
+
*/
|
|
46
|
+
export declare class FirewallRealtimeAuthenticator implements IRealtimeAuthenticator {
|
|
47
|
+
#private;
|
|
48
|
+
readonly name = "firewall-realtime";
|
|
49
|
+
/**
|
|
50
|
+
* @param resolveStore - fournit le store de révocation des jetons (le firewall
|
|
51
|
+
* passe une closure sur son container). Omis → mode dégradé documenté :
|
|
52
|
+
* seules les bornes portées par le jeton lui-même sont vérifiables.
|
|
53
|
+
*/
|
|
54
|
+
constructor(resolveStore?: (() => IRealtimeRevocationStore | null) | null);
|
|
55
|
+
/** Une identité authentifiée a-t-elle été résolue (par le firewall) au handshake ? */
|
|
56
|
+
supports(_handshake: IRealtimeHandshake): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Promeut l'identité déjà résolue (ALS) en jeton realtime — 0 lecture base.
|
|
59
|
+
*
|
|
60
|
+
* @throws AuthenticationError — aucune identité authentifiée en ALS (ne devrait
|
|
61
|
+
* pas arriver sur une zone data plane : le firewall ferme l'anonyme en amont ;
|
|
62
|
+
* filet défensif fail-closed → le hub ferme la socket en 4001).
|
|
63
|
+
*/
|
|
64
|
+
authenticate(_handshake: IRealtimeHandshake): Promise<IRealtimeToken>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Surface MINIMALE du store de jetons consommée ici : les deux lectures qui
|
|
68
|
+
* disent si un jeton a été révoqué avant son terme. Typée localement plutôt
|
|
69
|
+
* qu'importée d'`ITokenStore` — ce module n'a besoin ni du reste du contrat ni
|
|
70
|
+
* du couplage, et un test peut fournir un double en deux lignes.
|
|
71
|
+
*/
|
|
72
|
+
export interface IRealtimeRevocationStore {
|
|
73
|
+
/** `true` si ce `jti` a été mis sur la denylist et n'est pas encore expiré. */
|
|
74
|
+
isJtiDenied(jti: string): Promise<boolean>;
|
|
75
|
+
/** Seuil de révocation en masse du porteur (epoch ms), ou `null`. */
|
|
76
|
+
getInvalidBefore(subjectId: string): Promise<number | null>;
|
|
77
|
+
}
|
|
78
|
+
export default FirewallRealtimeAuthenticator;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Container } from "nodefony";
|
|
2
|
+
import type { ContextType } from "@nodefony/http";
|
|
3
|
+
import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
|
|
4
|
+
import type { IToken } from "../../contracts/IToken.js";
|
|
5
|
+
import type { IJwtRuntime } from "../token/jwtRuntime.js";
|
|
6
|
+
/**
|
|
7
|
+
* Authentification par **JWT Bearer** (RFC 6750) — réservée API service↔service /
|
|
8
|
+
* agents (le web utilise la session BFF). Vérifie un access token EdDSA signé par
|
|
9
|
+
* le {@link IJwtKeystore} du serveur.
|
|
10
|
+
*
|
|
11
|
+
* Défenses **dures** (RFC 8725 JWT BCP, prouvées en test) :
|
|
12
|
+
* - **allowlist d'algorithmes** côté serveur (`["EdDSA"]`) — l'algo n'est JAMAIS
|
|
13
|
+
* choisi d'après l'en-tête du token (§3.1) ; `alg=none` jamais accepté par jose.
|
|
14
|
+
* - **clé par `kid` depuis le keyset LOCAL** (`createLocalJWKSet`) — jamais
|
|
15
|
+
* `jku`/`jwk` de l'en-tête (injection de clé / SSRF, §3.5).
|
|
16
|
+
* - **`aud` (§3.9) + `iss` (§3.8) obligatoires** + `typ:"at+jwt"` (§3.11, sépare
|
|
17
|
+
* access et refresh) + `exp`/`nbf` (jose).
|
|
18
|
+
* - **révocation** : denylist `jti` + seuil `invalidBefore` par porteur (le JWT
|
|
19
|
+
* est auto-porté et non révocable sans état serveur).
|
|
20
|
+
* - **sujet revérifié** (§3.10) : `loadUserByIdentifier(sub)` → compte disparu,
|
|
21
|
+
* inactif ou verrouillé = rejet.
|
|
22
|
+
*
|
|
23
|
+
* Dépendances (keystore, store, userProvider) résolues **paresseusement** du
|
|
24
|
+
* container au premier usage (cold path) ; jose importé **lazy** (dep lourde).
|
|
25
|
+
*/
|
|
26
|
+
export declare class JwtAuthenticator implements IAuthenticator {
|
|
27
|
+
#private;
|
|
28
|
+
readonly name = "jwt";
|
|
29
|
+
/**
|
|
30
|
+
* @param container - container DI (résolution lazy de `jwtKeystore`/`tokenStore`/`users`).
|
|
31
|
+
* @param runtime - paramètres JWT effectifs (iss/aud/ttl) partagés avec l'émetteur.
|
|
32
|
+
*/
|
|
33
|
+
constructor(container: Container, runtime: IJwtRuntime);
|
|
34
|
+
/**
|
|
35
|
+
* La requête porte-t-elle un `Authorization: Bearer <jws>` émis par NOUS ?
|
|
36
|
+
*
|
|
37
|
+
* L'émetteur revendiqué est lu sans être vérifié ({@link peekIssuer}) et sert
|
|
38
|
+
* uniquement à AIGUILLER : `ExternalJwtAuthenticator` reconnaît la même forme
|
|
39
|
+
* de credential pour les jetons d'un serveur d'autorisation tiers. Sans ce
|
|
40
|
+
* discriminant, en mode `first`, le premier des deux listés dans la zone
|
|
41
|
+
* capturerait les deux familles et refuserait la moitié des jetons — l'ordre
|
|
42
|
+
* de la configuration deviendrait une décision de sécurité, dont l'erreur ne
|
|
43
|
+
* se verrait qu'en production.
|
|
44
|
+
*
|
|
45
|
+
* Un jeton dont l'émetteur est illisible reste pris en charge ici : c'est un
|
|
46
|
+
* jeton maison malformé, que la vérification refusera en le disant, plutôt
|
|
47
|
+
* qu'un credential qui disparaîtrait sans laisser de trace.
|
|
48
|
+
*/
|
|
49
|
+
supports(context: ContextType): boolean;
|
|
50
|
+
/** Extrait le token brut (non vérifié) → porté par un `UserToken` type `"jwt"`. */
|
|
51
|
+
createToken(context: ContextType): Promise<IToken>;
|
|
52
|
+
/**
|
|
53
|
+
* Vérifie la signature + les claims du JWT, applique la révocation et résout le
|
|
54
|
+
* sujet — ou lève un 401 au message uniforme.
|
|
55
|
+
*
|
|
56
|
+
* @throws AuthenticationError (401) — token absent/invalide/expiré/révoqué, ou
|
|
57
|
+
* sujet disparu/banni.
|
|
58
|
+
* @throws Error (câblage : keystore/store/users absents) — logguée ERROR par le
|
|
59
|
+
* firewall puis 401 fail-closed (rien ne fuite au client).
|
|
60
|
+
*/
|
|
61
|
+
authenticate(token: IToken): Promise<IToken>;
|
|
62
|
+
/** Slot audit (J4b). */
|
|
63
|
+
onSuccess(_context: ContextType, _token: IToken): Promise<void>;
|
|
64
|
+
/** Slot audit (J4b) — le 401 + challenge sont posés par le firewall. */
|
|
65
|
+
onFailure(_context: ContextType, _error: Error): Promise<void>;
|
|
66
|
+
/** Challenge RFC 6750/7235 posé par le firewall sur les 401 de la zone. */
|
|
67
|
+
challenge(): string;
|
|
68
|
+
}
|
|
69
|
+
export default JwtAuthenticator;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ContextType } from "@nodefony/http";
|
|
2
|
+
import type { IUserProvider } from "@nodefony/user";
|
|
3
|
+
import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
|
|
4
|
+
import type { ISecuredArea } from "../../contracts/ISecuredArea.js";
|
|
5
|
+
import type { IToken } from "../../contracts/IToken.js";
|
|
6
|
+
/**
|
|
7
|
+
* Authentification par **session serveur** (cookie opaque, modèle BFF) — la
|
|
8
|
+
* preuve des requêtes qui SUIVENT le login (`AuthFlow.login`, qui a déjà posé
|
|
9
|
+
* l'identifiant dans le blob et régénéré l'ID anti-fixation).
|
|
10
|
+
*
|
|
11
|
+
* `supports()` exige une session REPRISE porteuse d'un utilisateur : le
|
|
12
|
+
* pipeline http démarre la session AVANT le firewall (point d'activation
|
|
13
|
+
* unique, lazy — cookie entrant ou intent de route), cet authenticator ne
|
|
14
|
+
* démarre jamais rien lui-même. L'identité est re-résolue à CHAQUE requête
|
|
15
|
+
* via {@link resolveSessionIdentity} (rôles frais, révocation immédiate).
|
|
16
|
+
*
|
|
17
|
+
* Pas de `challenge()` : une session absente/expirée donne un 401 nu — le
|
|
18
|
+
* client web redirige vers son écran de login, jamais de popup Basic. Si la
|
|
19
|
+
* zone liste aussi `userpassword`, le firewall pose SON challenge (RFC 7235).
|
|
20
|
+
*/
|
|
21
|
+
export declare class SessionAuthenticator implements IAuthenticator {
|
|
22
|
+
#private;
|
|
23
|
+
readonly name = "session";
|
|
24
|
+
/**
|
|
25
|
+
* @param resolveProvider - résolution lazy de la source d'identité
|
|
26
|
+
* (typiquement `container.get("users")`) — appelée à la première requête.
|
|
27
|
+
*/
|
|
28
|
+
constructor(resolveProvider: () => IUserProvider);
|
|
29
|
+
/**
|
|
30
|
+
* Refuse une zone déclarée SANS REGISTRE — au boot, pas à la première requête.
|
|
31
|
+
*
|
|
32
|
+
* `stateless: true` annonce que l'identité tient tout entière dans la preuve
|
|
33
|
+
* portée par chaque requête, et que la session est ignorée « même si un
|
|
34
|
+
* cookie est présent ». Lister `session` dans une telle zone dit exactement
|
|
35
|
+
* l'inverse : {@link supports} y rendrait vrai dès qu'un cookie ramène une
|
|
36
|
+
* session porteuse d'un utilisateur, et la zone authentifierait par le
|
|
37
|
+
* registre qu'elle déclare ne pas tenir.
|
|
38
|
+
*
|
|
39
|
+
* Cette contradiction ne se voyait NULLE PART : l'application démarrait, la
|
|
40
|
+
* console d'administration affichait « aucun registre serveur », et le
|
|
41
|
+
* cookie authentifiait quand même. Elle se refuse donc au démarrage — le
|
|
42
|
+
* firewall en fait une erreur de configuration fail-closed, plutôt qu'une
|
|
43
|
+
* requête sur deux qui se comporte autrement que ce qui est écrit.
|
|
44
|
+
*
|
|
45
|
+
* @param area - la zone qui liste cet authenticator.
|
|
46
|
+
* @throws Error si la zone est `stateless` — le message la NOMME.
|
|
47
|
+
*/
|
|
48
|
+
validateArea(area: ISecuredArea): void;
|
|
49
|
+
/** La requête porte-t-elle une session reprise avec un utilisateur ? */
|
|
50
|
+
supports(context: ContextType): boolean;
|
|
51
|
+
/** Extrait l'identifiant du blob de session (jamais de secret en jeu). */
|
|
52
|
+
createToken(context: ContextType): Promise<IToken>;
|
|
53
|
+
/**
|
|
54
|
+
* Re-résout l'identifiant de session en utilisateur vivant et promeut le
|
|
55
|
+
* token. Les contrôles d'état (existe, actif, non verrouillé) vivent dans
|
|
56
|
+
* {@link resolveSessionIdentity} — partagés avec `AuthFlow.me()`.
|
|
57
|
+
*
|
|
58
|
+
* @throws AuthenticationError (401, message uniforme) — session orpheline,
|
|
59
|
+
* compte verrouillé ou désactivé.
|
|
60
|
+
*/
|
|
61
|
+
authenticate(token: IToken): Promise<IToken>;
|
|
62
|
+
/**
|
|
63
|
+
* Pose l'identifiant sur le contexte : la persistance de session du pipeline
|
|
64
|
+
* (`saveSession`) lie le blob au principal courant (string attendu).
|
|
65
|
+
*/
|
|
66
|
+
onSuccess(context: ContextType, token: IToken): Promise<void>;
|
|
67
|
+
/** Slot audit (P6.14). Le 401 est posé par le firewall. */
|
|
68
|
+
onFailure(_context: ContextType, _error: Error): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
export default SessionAuthenticator;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ContextType } from "@nodefony/http";
|
|
2
|
+
import type { IPasswordVerifier } from "@nodefony/user";
|
|
3
|
+
import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
|
|
4
|
+
import type { IToken } from "../../contracts/IToken.js";
|
|
5
|
+
import type { LoginThrottler } from "../throttle/LoginThrottler.js";
|
|
6
|
+
/**
|
|
7
|
+
* Authentification par identifiant + mot de passe — schéma **HTTP Basic**
|
|
8
|
+
* (RFC 7617) : `Authorization: Basic base64(identifiant:motdepasse)`, charset
|
|
9
|
+
* UTF-8, split au PREMIER `:` (le mot de passe peut en contenir).
|
|
10
|
+
*
|
|
11
|
+
* La vérification est déléguée au {@link IPasswordVerifier} (`UserService` par
|
|
12
|
+
* défaut) : hash, comparaison, leurre anti-timing et re-hash transparent restent
|
|
13
|
+
* derrière la frontière user — cet authenticator ne voit que le verdict.
|
|
14
|
+
*
|
|
15
|
+
* Le verifier est résolu **paresseusement** au premier login (cold path) : le
|
|
16
|
+
* boot ne paie rien et l'ordre de chargement des modules est indifférent.
|
|
17
|
+
*
|
|
18
|
+
* @remarks Le login par formulaire (body JSON) n'est PAS ici : il arrive avec la
|
|
19
|
+
* session BFF (`AuthController`, J3) qui appelle le verifier directement.
|
|
20
|
+
*/
|
|
21
|
+
export declare class UserPasswordAuthenticator implements IAuthenticator {
|
|
22
|
+
#private;
|
|
23
|
+
readonly name = "userpassword";
|
|
24
|
+
/**
|
|
25
|
+
* @param resolveVerifier - résolution lazy de la source de vérification
|
|
26
|
+
* (typiquement `container.get("users")`) — appelée au premier login.
|
|
27
|
+
* @param throttler - limiteur de tentatives (backoff NIST), `null` = désactivé.
|
|
28
|
+
*/
|
|
29
|
+
constructor(resolveVerifier: () => IPasswordVerifier, throttler?: LoginThrottler | null);
|
|
30
|
+
/** La requête porte-t-elle un en-tête `Authorization: Basic ...` ? */
|
|
31
|
+
supports(context: ContextType): boolean;
|
|
32
|
+
/** Décode l'enveloppe Basic — un contenu malformé donne un credential vide (échec uniforme). */
|
|
33
|
+
createToken(context: ContextType): Promise<IToken>;
|
|
34
|
+
/**
|
|
35
|
+
* Vérifie le credential via le verifier ou lève un 401 au message uniforme.
|
|
36
|
+
* Au succès le token est promu : utilisateur posé, credential effacé.
|
|
37
|
+
*
|
|
38
|
+
* Throttling NIST (si activé) : l'identifiant SAISI est vérifié AVANT le
|
|
39
|
+
* verifier (un identifiant bloqué ne coûte aucun hash → le throttle protège
|
|
40
|
+
* aussi le serveur du DoS argon2), échec compté, succès remis à zéro.
|
|
41
|
+
*
|
|
42
|
+
* @throws ThrottledError (429 + `Retry-After`) — backoff encore actif.
|
|
43
|
+
* @throws AuthenticationError (401) — credential absent ou invalide.
|
|
44
|
+
*/
|
|
45
|
+
authenticate(token: IToken): Promise<IToken>;
|
|
46
|
+
/** Slot J3 (session BFF au login) — rien à poser pour du Basic pur. */
|
|
47
|
+
onSuccess(_context: ContextType, _token: IToken): Promise<void>;
|
|
48
|
+
/** Slot J3+ (audit events). Le throttling vit dans `authenticate` (clé = identifiant) ; le 401 + challenge sont posés par le firewall. */
|
|
49
|
+
onFailure(_context: ContextType, _error: Error): Promise<void>;
|
|
50
|
+
/** Challenge RFC 7235 posé par le firewall sur les 401 de la zone. */
|
|
51
|
+
challenge(): string;
|
|
52
|
+
}
|
|
53
|
+
export default UserPasswordAuthenticator;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Container } from "nodefony";
|
|
2
|
+
import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
|
|
3
|
+
import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
|
|
4
|
+
/**
|
|
5
|
+
* Registre de **fabriques d'authenticators** — résout les noms listés dans
|
|
6
|
+
* `areas.<zone>.authenticators` vers des instances, SANS que le firewall
|
|
7
|
+
* connaisse le moindre nom en dur.
|
|
8
|
+
*
|
|
9
|
+
* Pourquoi : `IAuthenticator` est pluggable par contrat ; un
|
|
10
|
+
* `if (name === "jwt") …` dans le firewall trahirait cette promesse (couplage
|
|
11
|
+
* aux noms, fermé à l'extension). Les builtins s'enregistrent au chargement du
|
|
12
|
+
* module (donc toujours AVANT le boot) ; un plugin externe enregistre le sien
|
|
13
|
+
* (`registerAuthenticatorFactory("ldap", …)`) puis le référence en config —
|
|
14
|
+
* aucun changement dans le cœur. Convention-frère : `backplaneRegistry`
|
|
15
|
+
* (realtime), `ormRegistry` (orm-core).
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Contexte passé à une fabrique : tout ce dont un authenticator peut avoir
|
|
19
|
+
* besoin pour se construire. La fabrique ne fait QUE construire — résolutions
|
|
20
|
+
* de services coûteuses en lazy à l'intérieur de l'instance (cold path).
|
|
21
|
+
*/
|
|
22
|
+
export interface IAuthenticatorFactoryContext {
|
|
23
|
+
/** Container DI — résolution de services (`users`, `sessions`...). */
|
|
24
|
+
readonly container: Container;
|
|
25
|
+
/** Config sécurité validée + gelée (sections `jwt`, `passkeys`...). */
|
|
26
|
+
readonly config: ISecurityConfig;
|
|
27
|
+
}
|
|
28
|
+
/** Fabrique d'un authenticator pour un nom donné. */
|
|
29
|
+
export type AuthenticatorFactory = (ctx: IAuthenticatorFactoryContext) => IAuthenticator;
|
|
30
|
+
/**
|
|
31
|
+
* Enregistre (ou remplace) la fabrique d'un authenticator. Appelé par les
|
|
32
|
+
* builtins au chargement du module, et par les plugins pour les leurs
|
|
33
|
+
* (LDAP, SSO maison...).
|
|
34
|
+
*/
|
|
35
|
+
export declare function registerAuthenticatorFactory(name: string, factory: AuthenticatorFactory): void;
|
|
36
|
+
/** Fabrique d'un authenticator par nom, ou `undefined` si inconnu. */
|
|
37
|
+
export declare function getAuthenticatorFactory(name: string): AuthenticatorFactory | undefined;
|
|
38
|
+
/** Noms enregistrés (validation boot, introspection Studio, tests). */
|
|
39
|
+
export declare function listAuthenticatorFactories(): string[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lecture d'un en-tête `Authorization: Bearer …`.
|
|
3
|
+
*
|
|
4
|
+
* 🔴 **L'implémentation a déménagé au CŒUR** (`nodefony`), et ce fichier n'en
|
|
5
|
+
* garde que le point d'entrée. Le motif n'est pas cosmétique : deux couches qui
|
|
6
|
+
* ne se voient pas lisent le même en-tête — les authentificateurs d'ici, et le
|
|
7
|
+
* rôle *serveur de ressource* OAuth, qui vit au cœur parce qu'il ne dépend
|
|
8
|
+
* d'aucun module. Une frontière de paquets aurait imposé une copie, et une copie
|
|
9
|
+
* de cette fonction ne diverge pas bruyamment : elle diverge sur un cas limite
|
|
10
|
+
* (`Bearer` sans séparateur, espace insécable, jeton vide) que **chaque copie
|
|
11
|
+
* continue de passer dans ses propres tests**.
|
|
12
|
+
*
|
|
13
|
+
* Ce qui l'a motivée reste vrai et se relit au cœur : le motif d'origine était
|
|
14
|
+
* quadratique, et il s'exécutait avant toute authentification — donc pour un
|
|
15
|
+
* porteur qui n'avait rien prouvé.
|
|
16
|
+
*
|
|
17
|
+
* Les tests d'ici (`tests/unit/bearer.test.ts`, cas anti-ReDoS compris) valent
|
|
18
|
+
* désormais pour l'implémentation du cœur : ils l'atteignent par ce point
|
|
19
|
+
* d'entrée, ce qui est exactement le contrôle qu'on veut sur une brique
|
|
20
|
+
* partagée.
|
|
21
|
+
*/
|
|
22
|
+
export { bearerToken } from "nodefony";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment le sujet d'un émetteur donné entre dans l'espace de noms local.
|
|
3
|
+
*
|
|
4
|
+
* - `prefixed` — l'identifiant local est composé de l'émetteur ET du sujet.
|
|
5
|
+
* - `subject` — le sujet est pris tel quel (l'espace de noms est maîtrisé).
|
|
6
|
+
*/
|
|
7
|
+
export type ExternalSubjectMapping = "prefixed" | "subject";
|
|
8
|
+
/**
|
|
9
|
+
* Compose l'identifiant local qui désigne le sujet d'un émetteur externe.
|
|
10
|
+
*
|
|
11
|
+
* 🔴 **Un `sub` seul ne désigne personne.** OpenID Connect Core §2 ne garantit
|
|
12
|
+
* son unicité et sa non-réattribution que *dans l'espace de son émetteur*.
|
|
13
|
+
* Chercher un compte local directement par `sub` verse donc des identifiants
|
|
14
|
+
* étrangers dans l'espace local : il suffit d'un annuaire où l'utilisateur
|
|
15
|
+
* choisit son identifiant — beaucoup le permettent — pour présenter
|
|
16
|
+
* `sub: "admin"` et se voir rattacher au compte local du même nom.
|
|
17
|
+
*
|
|
18
|
+
* C'est pour cela que `prefixed` est le défaut et que `subject` se déclare :
|
|
19
|
+
* le mode sûr ne doit rien demander, le mode qui fait confiance doit être écrit.
|
|
20
|
+
*
|
|
21
|
+
* @param issuer - émetteur VÉRIFIÉ, sous sa forme canonique (jamais la valeur
|
|
22
|
+
* brute lue dans le jeton — elle est choisie par le porteur)
|
|
23
|
+
* @param subject - sujet du jeton (`sub`)
|
|
24
|
+
* @param mapping - politique déclarée pour CET émetteur
|
|
25
|
+
* @returns l'identifiant à chercher dans l'annuaire local
|
|
26
|
+
*/
|
|
27
|
+
export declare function localIdentifierFor(issuer: string, subject: string, mapping: ExternalSubjectMapping): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lecture NON VÉRIFIÉE de l'émetteur d'un JWS compact.
|
|
3
|
+
*
|
|
4
|
+
* Sert à **choisir qui doit examiner un jeton**, jamais à décider de son sort.
|
|
5
|
+
* Deux authenticators reconnaissent la même forme de credential — un
|
|
6
|
+
* `Authorization: Bearer <jws>` — l'un pour les jetons que Nodefony a émis,
|
|
7
|
+
* l'autre pour ceux d'un serveur d'autorisation tiers. Sans discriminant, le
|
|
8
|
+
* premier listé dans la zone capture les deux et refuse la moitié des jetons :
|
|
9
|
+
* l'ordre de la configuration deviendrait une décision de sécurité, et son
|
|
10
|
+
* erreur ne se verrait qu'en production.
|
|
11
|
+
*
|
|
12
|
+
* ## Ce qui rend cette lecture sûre
|
|
13
|
+
*
|
|
14
|
+
* Rien de ce qui est lu ici ne devient une clé, une URL ou un algorithme. La
|
|
15
|
+
* valeur ne sert qu'à sélectionner une entrée dans une liste **fermée**, écrite
|
|
16
|
+
* en configuration ; le jeton est ensuite vérifié entièrement par
|
|
17
|
+
* l'authenticator retenu, `iss` compris. Un attaquant qui ment sur `iss` ne
|
|
18
|
+
* gagne donc que le droit d'être refusé par un autre maillon.
|
|
19
|
+
*
|
|
20
|
+
* La taille est bornée AVANT tout travail : `JSON.parse` sur une entrée non
|
|
21
|
+
* fiable est le genre d'appel qu'on ne laisse pas grandir sans limite.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Rend le claim `iss` d'un JWS compact, sans vérifier quoi que ce soit.
|
|
25
|
+
*
|
|
26
|
+
* @param raw - le jeton brut, tel que présenté
|
|
27
|
+
* @returns l'émetteur revendiqué, ou `null` si le jeton est trop gros, mal
|
|
28
|
+
* formé, ou ne revendique pas d'émetteur exploitable
|
|
29
|
+
*/
|
|
30
|
+
export declare function peekIssuer(raw: string): string | null;
|
|
31
|
+
export default peekIssuer;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
/** Contexte de dérivation HKDF — distingue les domaines cryptographiques. */
|
|
3
|
+
export interface IKeyDerivation {
|
|
4
|
+
/** Sel HKDF (RFC 5869 §3.1) — constante par domaine. */
|
|
5
|
+
readonly salt: string | Buffer;
|
|
6
|
+
/** Info HKDF (RFC 5869 §3.2) — lie la sous-clé à son usage (séparation de domaine). */
|
|
7
|
+
readonly info: string | Buffer;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Dérive une clé AES-256 (32 octets) d'un matériel de clé applicatif via
|
|
11
|
+
* HKDF-SHA256 (RFC 5869). Accepte toute longueur/forme (passphrase, hex, base64)
|
|
12
|
+
* sans jamais l'utiliser brute comme clé AES. **Déterministe** : tous les pods
|
|
13
|
+
* d'un cluster dérivent la même clé du même secret de config + même domaine
|
|
14
|
+
* (secret lisible cross-pod). Domaines distincts (`info` différent) → clés
|
|
15
|
+
* indépendantes.
|
|
16
|
+
*
|
|
17
|
+
* @param material - matériel de clé brut (secret de config).
|
|
18
|
+
* @param derivation - sel + info propres au domaine (TOTP, webhook…).
|
|
19
|
+
* @returns clé AES-256 (32 octets).
|
|
20
|
+
*/
|
|
21
|
+
export declare function deriveKey(material: string | Buffer, derivation: IKeyDerivation): Buffer;
|
|
22
|
+
/** Génère une clé éphémère 32 octets (dev sans clé configurée — non persistée). */
|
|
23
|
+
export declare function generateEphemeralKey(): Buffer;
|
|
24
|
+
/** Chiffre un secret en clair → blob opaque versionné (IV aléatoire à chaque appel). */
|
|
25
|
+
export declare function encryptSecret(plain: Buffer, key: Buffer): string;
|
|
26
|
+
/**
|
|
27
|
+
* Déchiffre un blob produit par {@link encryptSecret}. Lève si le format/version
|
|
28
|
+
* est invalide, le blob tronqué, ou le tag GCM non valide (altération OU mauvaise
|
|
29
|
+
* clé — GCM ne distingue pas les deux, par construction).
|
|
30
|
+
*/
|
|
31
|
+
export declare function decryptSecret(blob: string, key: Buffer): Buffer;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fusion de directives Content-Security-Policy — logique PURE (aucune dépendance
|
|
3
|
+
* à Vite, au frontend ni au transport). Permet à un module de DÉCLARER ses besoins
|
|
4
|
+
* CSP (ex. `@nodefony/frontend` en dev : origines Vite + `'unsafe-eval'` pour le
|
|
5
|
+
* Fast Refresh) sans que `@nodefony/security` connaisse leur sémantique : security
|
|
6
|
+
* se contente de MERGER des fragments génériques `directive → sources`.
|
|
7
|
+
*
|
|
8
|
+
* Pourquoi un merge structuré (et pas une simple concaténation) : en CSP une
|
|
9
|
+
* directive RÉPÉTÉE est ignorée après sa 1ʳᵉ occurrence (W3C CSP3 §3) → concaténer
|
|
10
|
+
* deux `script-src` perdrait le second. Il faut fusionner les sources dans UNE
|
|
11
|
+
* directive. Le token `'nonce-{{nonce}}'` est opaque (ni `;` ni espace) → préservé
|
|
12
|
+
* tel quel par parse/serialize.
|
|
13
|
+
*/
|
|
14
|
+
/** Fragment additif d'un module : directive CSP → sources à ajouter. */
|
|
15
|
+
export type CspFragment = Record<string, readonly string[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Parse une chaîne CSP en directives ordonnées `[nom, sources[]]`. L'ordre des
|
|
18
|
+
* directives et des sources est préservé (déterminisme : header stable, tests
|
|
19
|
+
* fiables). Les séparateurs multiples / espaces superflus sont normalisés.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseCsp(csp: string): Array<[string, string[]]>;
|
|
22
|
+
/** Sérialise des directives ordonnées en chaîne CSP (`a b; c d`). */
|
|
23
|
+
export declare function serializeCsp(directives: Array<[string, string[]]>): string;
|
|
24
|
+
/**
|
|
25
|
+
* Fusionne un CSP de base avec des fragments additifs par module.
|
|
26
|
+
*
|
|
27
|
+
* - directive déjà dans la base → ses sources sont COMPLÉTÉES (dédupliquées,
|
|
28
|
+
* ordre base d'abord puis ajouts) ;
|
|
29
|
+
* - directive absente → AJOUTÉE en fin (ordre d'apparition des fragments).
|
|
30
|
+
*
|
|
31
|
+
* Pur + déterministe : recalculé uniquement quand un module (dé)enregistre ses
|
|
32
|
+
* origines (jamais par requête). Le résultat repart dans `SecurityHeaders`, qui
|
|
33
|
+
* re-split autour de `{{nonce}}` au boot → 1 `join` par requête (hot-path inchangé).
|
|
34
|
+
*
|
|
35
|
+
* @param base - CSP de configuration (peut contenir `'nonce-{{nonce}}'`).
|
|
36
|
+
* @param fragments - fragments additifs (un par module enregistré).
|
|
37
|
+
* @returns la chaîne CSP fusionnée.
|
|
38
|
+
*/
|
|
39
|
+
export declare function mergeCspFragments(base: string, fragments: Iterable<CspFragment>): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jeton synchronizer CSRF — modèle **double-submit signé** (OWASP CSRF Prevention
|
|
3
|
+
* Cheat Sheet, « Signed Double-Submit Cookie »). Le token est
|
|
4
|
+
* `nonce.HMAC-SHA256(secret, nonce)` (base64url), posé dans un cookie LISIBLE
|
|
5
|
+
* (`csrf-token`, non HttpOnly) ET rejoué par le client dans l'en-tête
|
|
6
|
+
* `x-csrf-token`. La défense `@CsrfProtect` exige les deux PRÉSENTS, ÉGAUX
|
|
7
|
+
* (double-submit) et la signature HMAC VALIDE.
|
|
8
|
+
*
|
|
9
|
+
* **Stateless** (aucune session requise) → couvre le BFF (cookie de session) ET
|
|
10
|
+
* l'API JWT sans coupler au stockage de session. Le secret HMAC empêche un script
|
|
11
|
+
* tiers de forger un token (il ne peut pas calculer la signature) ; le double
|
|
12
|
+
* submit empêche un attaquant cross-site d'en injecter un (il ne peut ni écrire
|
|
13
|
+
* l'en-tête custom — préflight CORS — ni lire le cookie de la victime — SameSite + SOP).
|
|
14
|
+
*
|
|
15
|
+
* Pure et synchrone (1 HMAC à l'émission, 1 à la vérif) — payé UNIQUEMENT sur les
|
|
16
|
+
* routes `@CsrfProtect` (la défense globale Fetch Metadata reste primaire, hot-path
|
|
17
|
+
* GET = 0).
|
|
18
|
+
*
|
|
19
|
+
* @see OWASP CSRF Prevention Cheat Sheet · RFC 9110 §15.5.4 (403).
|
|
20
|
+
*/
|
|
21
|
+
export declare class CsrfTokenManager {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(secret: string);
|
|
24
|
+
/** Émet un token signé `nonce.signature` (base64url). */
|
|
25
|
+
issue(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Vérifie une mutation `@CsrfProtect` : en-tête ET cookie présents, ÉGAUX
|
|
28
|
+
* (double-submit, comparaison à temps constant) et signature HMAC valide. Tout
|
|
29
|
+
* écart → `false` (le firewall lève alors un 403). Jamais d'exception.
|
|
30
|
+
*
|
|
31
|
+
* @param headerToken - valeur de l'en-tête `x-csrf-token` rejouée par le client.
|
|
32
|
+
* @param cookieToken - valeur du cookie `csrf-token` posé par le serveur.
|
|
33
|
+
*/
|
|
34
|
+
verify(headerToken: string | undefined, cookieToken: string | undefined): boolean;
|
|
35
|
+
}
|
|
36
|
+
export default CsrfTokenManager;
|