@nodefony/security 10.0.0-beta.5 → 10.0.0-beta.7
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/README.md +24 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.155.0 → _@oxc-project_runtime@0.158.0}/helpers/esm/decorate.js +1 -1
- package/dist/index.js +63 -61
- package/dist/nodefony/command/security-mcp-token.js +43 -5
- package/dist/nodefony/config/config.js +30 -11
- package/dist/nodefony/config/defineModuleConfig.js +52 -1
- package/dist/nodefony/contracts/IWebhookPayload.js +1 -0
- package/dist/nodefony/errors/FetchPublicError.js +18 -0
- package/dist/nodefony/errors/SsrfError.js +9 -1
- package/dist/nodefony/errors/StaleSessionError.js +20 -0
- package/dist/nodefony/errors/index.js +7 -5
- package/dist/nodefony/keycloak/keycloakAdmin.js +3 -3
- package/dist/nodefony/keycloak/scaffold.js +31 -14
- package/dist/nodefony/keycloak/security-keycloak-realm.js +1 -1
- package/dist/nodefony/service/accessTokenVerifier.js +6 -6
- package/dist/nodefony/service/apiKeys.js +8 -4
- package/dist/nodefony/service/auditService.js +48 -3
- package/dist/nodefony/service/authFlow.js +9 -6
- package/dist/nodefony/service/authorization.js +1 -1
- package/dist/nodefony/service/firewall.js +281 -44
- package/dist/nodefony/service/keycloak.js +1 -1
- package/dist/nodefony/service/oauth2.js +8 -4
- package/dist/nodefony/service/securityHeaders.js +340 -47
- package/dist/nodefony/service/tokenService.js +16 -11
- package/dist/nodefony/service/totp.js +10 -5
- package/dist/nodefony/service/webAuthn.js +46 -7
- package/dist/nodefony/service/webhooks.js +11 -4
- package/dist/nodefony/src/admin/SecurityAdminApi.js +19 -0
- package/dist/nodefony/src/audit/AuditRefusalAggregator.js +139 -0
- package/dist/nodefony/src/authenticator/ApiKeyAuthenticator.js +1 -1
- package/dist/nodefony/src/authenticator/ExternalSubjectResolver.js +31 -7
- package/dist/nodefony/src/authenticator/FirewallRealtimeAuthenticator.js +62 -20
- package/dist/nodefony/src/authenticator/JwtAuthenticator.js +29 -6
- package/dist/nodefony/src/authenticator/SessionAuthenticator.js +22 -9
- package/dist/nodefony/src/authenticator/authenticatorRegistry.js +2 -2
- package/dist/nodefony/src/csp.js +265 -10
- package/dist/nodefony/src/cspReports.js +307 -0
- package/dist/nodefony/src/net/fetchPublic.js +244 -0
- package/dist/nodefony/src/net/ssrfGuard.js +2 -2
- package/dist/nodefony/src/oauth/clientCredentials.js +1 -1
- package/dist/nodefony/src/oauth/metadata.js +1 -1
- package/dist/nodefony/src/oauth/oauthProviderRegistry.js +17 -4
- package/dist/nodefony/src/oauth/providers/github.js +1 -1
- package/dist/nodefony/src/realtime/UserRealtimeToken.js +59 -2
- package/dist/nodefony/src/realtime/frameAuthorizer.js +136 -44
- package/dist/nodefony/src/token/RemoteJwtVerifier.js +8 -2
- package/dist/nodefony/src/token/jwtRuntime.js +22 -2
- package/dist/nodefony/src/token/tokenTimeClaims.js +34 -0
- package/dist/nodefony/src/totp/totpOperations.js +1 -1
- package/dist/nodefony/src/voter/RoleVoter.js +1 -1
- package/dist/nodefony/src/webhook/WebhookDispatcher.js +71 -17
- package/dist/nodefony/src/webhook/webhookDelivery.js +37 -86
- package/dist/nodefony/testing/index.js +26 -36
- package/dist/types/index.d.ts +5 -1
- package/dist/types/nodefony/command/security-mcp-token.d.ts +12 -0
- package/dist/types/nodefony/config/config.d.ts +8 -0
- package/dist/types/nodefony/contracts/IApiKey.d.ts +8 -2
- package/dist/types/nodefony/contracts/IFirewall.d.ts +15 -2
- package/dist/types/nodefony/contracts/ITokenStore.d.ts +7 -3
- package/dist/types/nodefony/contracts/IWebhookEndpoint.d.ts +13 -3
- package/dist/types/nodefony/contracts/IWebhookPayload.d.ts +24 -0
- package/dist/types/nodefony/errors/FetchPublicError.d.ts +15 -0
- package/dist/types/nodefony/errors/SsrfError.d.ts +8 -1
- package/dist/types/nodefony/errors/StaleSessionError.d.ts +17 -0
- package/dist/types/nodefony/errors/index.d.ts +3 -0
- package/dist/types/nodefony/keycloak/scaffold.d.ts +12 -6
- package/dist/types/nodefony/service/apiKeys.d.ts +3 -0
- package/dist/types/nodefony/service/authFlow.d.ts +2 -2
- package/dist/types/nodefony/service/firewall.d.ts +28 -7
- package/dist/types/nodefony/service/securityHeaders.d.ts +82 -11
- package/dist/types/nodefony/service/webAuthn.d.ts +13 -0
- package/dist/types/nodefony/service/webhooks.d.ts +7 -1
- package/dist/types/nodefony/src/admin/SecurityAdminApi.d.ts +3 -0
- package/dist/types/nodefony/src/audit/AuditRefusalAggregator.d.ts +55 -0
- package/dist/types/nodefony/src/authenticator/ExternalSubjectResolver.d.ts +11 -0
- package/dist/types/nodefony/src/authenticator/JwtAuthenticator.d.ts +17 -3
- package/dist/types/nodefony/src/authenticator/SessionAuthenticator.d.ts +11 -4
- package/dist/types/nodefony/src/csp.d.ts +142 -0
- package/dist/types/nodefony/src/cspReports.d.ts +73 -0
- package/dist/types/nodefony/src/net/fetchPublic.d.ts +73 -0
- package/dist/types/nodefony/src/oauth/oauthProviderRegistry.d.ts +15 -0
- package/dist/types/nodefony/src/realtime/UserRealtimeToken.d.ts +35 -1
- package/dist/types/nodefony/src/realtime/frameAuthorizer.d.ts +10 -2
- package/dist/types/nodefony/src/realtime/realtimeContracts.d.ts +42 -18
- package/dist/types/nodefony/src/token/jwtRuntime.d.ts +21 -2
- package/dist/types/nodefony/src/token/tokenTimeClaims.d.ts +32 -0
- package/dist/types/nodefony/src/webhook/WebhookDispatcher.d.ts +36 -1
- package/dist/types/nodefony/src/webhook/webhookDelivery.d.ts +5 -2
- package/dist/types/nodefony/testing/index.d.ts +6 -0
- package/docs/api-keys.md +3 -1
- package/docs/audit.md +112 -28
- package/docs/authenticators.md +6 -4
- package/docs/csrf.md +3 -2
- package/docs/external-jwt.md +5 -1
- package/docs/firewall.md +3 -2
- package/docs/headers.md +427 -71
- package/docs/keycloak.md +4 -4
- package/docs/login-page.md +4 -3
- package/docs/obtenir-un-jeton.md +17 -10
- package/docs/tokens.md +1 -1
- package/docs/totp.md +2 -2
- package/docs/webauthn.md +2 -2
- package/docs/webhooks.md +63 -18
- package/package.json +17 -17
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Corps JSON d'une livraison webhook, tel que le destinataire le reçoit — le type à poser sur le `JSON.parse` du récepteur.
|
|
3
|
+
*
|
|
4
|
+
* Signé avec ses en-têtes (`webhook-id`, `webhook-timestamp`, `webhook-signature`,
|
|
5
|
+
* convention Standard Webhooks) : vérifier la signature AVANT de lire le corps.
|
|
6
|
+
*
|
|
7
|
+
* @typeParam T - la forme de `data` ; `unknown` tant que le récepteur ne l'a pas vérifiée.
|
|
8
|
+
*/
|
|
9
|
+
export interface IWebhookPayload<T = unknown> {
|
|
10
|
+
/**
|
|
11
|
+
* Identifiant du MESSAGE, égal à l'en-tête `webhook-id` et stable d'un réessai à l'autre : la clé de déduplication du récepteur.
|
|
12
|
+
*/
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/**
|
|
15
|
+
* Heure de la TENTATIVE de livraison (ISO 8601), pas celle de l'événement : un réessai porte une heure plus tardive.
|
|
16
|
+
*
|
|
17
|
+
* L'heure de l'événement vit dans `data` (`ts` pour un événement d'audit).
|
|
18
|
+
*/
|
|
19
|
+
readonly timestamp: string;
|
|
20
|
+
/** Nom de l'événement — l'action d'audit, ex. `"login.success"` ; c'est lui que filtrent les motifs d'abonnement. */
|
|
21
|
+
readonly type: string;
|
|
22
|
+
/** L'événement lui-même ; pour l'audit, un `IAuditEvent`. */
|
|
23
|
+
readonly data: T;
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { nodefonyError } from "nodefony";
|
|
2
|
+
/** Cause d'un échec de {@link FetchPublicError}. */
|
|
3
|
+
export type FetchPublicFailure = "timeout" | "too-many-redirects" | "network";
|
|
4
|
+
/**
|
|
5
|
+
* Échec d'une requête sortante vers un site tiers (`fetchPublic`) qui n'est PAS
|
|
6
|
+
* un refus de cible : délai total dépassé (`504`), trop de redirections ou
|
|
7
|
+
* échec réseau (`502`). Une cible interdite lève `SsrfError` (422), pas
|
|
8
|
+
* celle-ci — l'appelant distingue ainsi « tu n'as pas le droit d'y aller » de
|
|
9
|
+
* « le site n'a pas répondu ».
|
|
10
|
+
*/
|
|
11
|
+
export declare class FetchPublicError extends nodefonyError {
|
|
12
|
+
/** Cause de l'échec, stable et exploitable sans lire le message. */
|
|
13
|
+
readonly reason: FetchPublicFailure;
|
|
14
|
+
constructor(reason: FetchPublicFailure, message: string);
|
|
15
|
+
}
|
|
@@ -8,6 +8,13 @@ import { nodefonyError } from "nodefony";
|
|
|
8
8
|
* d'un webhook invalide).
|
|
9
9
|
*/
|
|
10
10
|
export declare class SsrfError extends nodefonyError {
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* `forbidden` : la cible est interdite (protocole, identifiants, IP non
|
|
13
|
+
* publique). `unresolvable` : l'hôte n'a aucune adresse — rien d'interne n'est
|
|
14
|
+
* visé, le site est simplement injoignable ; un appelant qui rend ces deux cas
|
|
15
|
+
* à l'utilisateur les distingue par ce champ, jamais par le texte du message.
|
|
16
|
+
*/
|
|
17
|
+
readonly reason: "forbidden" | "unresolvable";
|
|
18
|
+
constructor(message?: string | Error, reason?: "forbidden" | "unresolvable");
|
|
12
19
|
}
|
|
13
20
|
export default SsrfError;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AuthenticationError } from "./AuthenticationError.js";
|
|
2
|
+
/**
|
|
3
|
+
* Session PÉRIMÉE — elle ne désigne plus personne de valide : compte supprimé,
|
|
4
|
+
* verrouillé, désactivé, recréé sous le même identifiant, mot de passe changé
|
|
5
|
+
* depuis son ouverture, ou blob vidé. C'est un ÉTAT à purger, pas une preuve
|
|
6
|
+
* fausse présentée par un tiers : le `SessionAuthenticator` la détruit côté
|
|
7
|
+
* serveur ET côté navigateur (cookie expiré, RFC 6265 §5.3), puis le firewall
|
|
8
|
+
* poursuit comme si aucune session n'avait été présentée — une page ouverte
|
|
9
|
+
* aux anonymes s'affiche, une zone fermée redemande la connexion.
|
|
10
|
+
*
|
|
11
|
+
* Jamais levée pour une PANNE de la source d'identité : une base injoignable
|
|
12
|
+
* ne dit rien de la session, qui doit survivre à l'incident.
|
|
13
|
+
*/
|
|
14
|
+
export declare class StaleSessionError extends AuthenticationError {
|
|
15
|
+
constructor(message?: string | Error);
|
|
16
|
+
}
|
|
17
|
+
export default StaleSessionError;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export { AuthenticationError } from "./AuthenticationError.js";
|
|
2
|
+
export { StaleSessionError } from "./StaleSessionError.js";
|
|
2
3
|
export { AccessDeniedError } from "./AccessDeniedError.js";
|
|
3
4
|
export { ThrottledError } from "./ThrottledError.js";
|
|
4
5
|
export { UnverifiableTokenError } from "./UnverifiableTokenError.js";
|
|
5
6
|
export { InvalidTargetError } from "./InvalidTargetError.js";
|
|
6
7
|
export { CsrfError } from "./CsrfError.js";
|
|
7
8
|
export { SsrfError } from "./SsrfError.js";
|
|
9
|
+
export { FetchPublicError } from "./FetchPublicError.js";
|
|
10
|
+
export type { FetchPublicFailure } from "./FetchPublicError.js";
|
|
8
11
|
export { WebAuthnError } from "./WebAuthnError.js";
|
|
9
12
|
export { InsecureTransportError } from "./InsecureTransportError.js";
|
|
@@ -7,13 +7,13 @@ export declare const APP_KEYCLOAK_REALM_FILE = "docker/keycloak/import/realm.jso
|
|
|
7
7
|
export declare const APP_KEYCLOAK_THEMES_DIR = "docker/keycloak/themes";
|
|
8
8
|
export declare const APP_KEYCLOAK_THEME_DIR = "docker/keycloak/themes/nodefony";
|
|
9
9
|
/**
|
|
10
|
-
* Ressource de l'API partenaire
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* Ressource de l'API partenaire de la démo en développement : le chemin de sa
|
|
11
|
+
* zone (`/demo/api/partner`) rendu absolu contre l'émetteur de développement
|
|
12
|
+
* — le port HTTPS par défaut, comme les URL de retour du realm. Deux paquets,
|
|
13
|
+
* une valeur : le test de contribution la confronte à une application
|
|
14
|
+
* `--demo` réellement rendue.
|
|
15
15
|
*/
|
|
16
|
-
export declare const APP_PARTNER_DEV_RESOURCE = "https://localhost:5152/api/partner";
|
|
16
|
+
export declare const APP_PARTNER_DEV_RESOURCE = "https://localhost:5152/demo/api/partner";
|
|
17
17
|
/** Les types de thème que le realm sait déclarer, et la clé qui les porte. */
|
|
18
18
|
declare const THEME_TYPES: readonly ["login", "account", "email", "admin"];
|
|
19
19
|
type TThemeType = (typeof THEME_TYPES)[number];
|
|
@@ -68,6 +68,12 @@ export declare function devMachineSecret(appName: string): string;
|
|
|
68
68
|
* rôle client `admin` (`roleMapping: { admin: "ROLE_ADMIN" }`), et les
|
|
69
69
|
* audiences des zones `external-jwt` que la configuration rendue déclare.
|
|
70
70
|
*
|
|
71
|
+
* Le client « machine » (`<app>-machine`, `client_credentials`) n'existe que
|
|
72
|
+
* s'il a une audience à recevoir : une application neuve n'ouvre aucune zone à
|
|
73
|
+
* jeton tiers (#746), et un client que rien n'accepte serait un secret de plus
|
|
74
|
+
* à garder pour rien. La démo, qui apporte l'API entre programmes, le fait
|
|
75
|
+
* naître en déclarant sa ressource.
|
|
76
|
+
*
|
|
71
77
|
* @param appName - nom de l'application (= realm = identifiant de client)
|
|
72
78
|
* @param themes - types de thème à déclarer
|
|
73
79
|
* @param ids - `sub` des comptes de démonstration, FIXÉS dans le fichier :
|
|
@@ -49,6 +49,9 @@ declare class ApiKeyService extends Service {
|
|
|
49
49
|
* @param roles - rôles du porteur tels que son compte les porte ; omis =
|
|
50
50
|
* aucun rôle, donc tout scope réservé est refusé (fail-closed).
|
|
51
51
|
* @throws ApiKeyError 400 — nom vide/trop long, scope hors catalogue, expiry invalide.
|
|
52
|
+
* @throws ApiKeyError 400 — `tenantId` rempli : le périmètre d'un locataire n'est
|
|
53
|
+
* pas encore appliqué (aucun lecteur ne cloisonne sur lui) — l'accepter rendrait
|
|
54
|
+
* la clé trompeuse, et l'appliquer un jour lui retirerait des accès (rupture).
|
|
52
55
|
* @throws ApiKeyError 403 — scope réservé à des rôles que le porteur n'a pas.
|
|
53
56
|
* @throws ApiKeyError 409 — plafond `maxPerSubject` atteint.
|
|
54
57
|
* @throws ApiKeyError 403 — aucun compte sous cet identifiant (la clé est liée
|
|
@@ -132,8 +132,8 @@ declare class AuthFlow extends Service {
|
|
|
132
132
|
* (mêmes contrôles que le `SessionAuthenticator` — source unique).
|
|
133
133
|
*
|
|
134
134
|
* @returns la projection publique, ou `null` (pas de session, session
|
|
135
|
-
* orpheline, compte verrouillé/désactivé
|
|
136
|
-
* `{ user: null }`.
|
|
135
|
+
* orpheline, compte verrouillé/désactivé — la session périmée est alors
|
|
136
|
+
* détruite, cookie compris) — le handler répond `200` `{ user: null }`.
|
|
137
137
|
*/
|
|
138
138
|
me(context: ContextType): Promise<ISafeUser | null>;
|
|
139
139
|
/**
|
|
@@ -8,6 +8,12 @@ import type { IFirewall } from "../contracts/IFirewall.js";
|
|
|
8
8
|
import type { IAuthenticator } from "../contracts/IAuthenticator.js";
|
|
9
9
|
import type { ISecuredArea } from "../contracts/ISecuredArea.js";
|
|
10
10
|
import type { IFirewallDescription, IRoleHierarchyDescription } from "../contracts/IFirewallDescription.js";
|
|
11
|
+
/**
|
|
12
|
+
* `Cache-Control` par défaut d'une réponse dont la CSP porte un nonce : jamais
|
|
13
|
+
* gardée par un cache partagé (le nonce y serait servi à plusieurs visiteurs),
|
|
14
|
+
* toujours redemandée avant réutilisation (ADR-0024 §C2).
|
|
15
|
+
*/
|
|
16
|
+
export declare const NONCED_DOCUMENT_CACHE_CONTROL = "private, no-cache";
|
|
11
17
|
/**
|
|
12
18
|
* Orchestrateur de sécurité Nodefony — refonte 2026 (P6).
|
|
13
19
|
*
|
|
@@ -27,7 +33,7 @@ import type { IFirewallDescription, IRoleHierarchyDescription } from "../contrac
|
|
|
27
33
|
*
|
|
28
34
|
* Conformité : tout 401 porte un challenge `WWW-Authenticate` (RFC 9110
|
|
29
35
|
* §15.5.2) — celui du premier authenticator de la zone qui en déclare un, sinon
|
|
30
|
-
* le défi de repli {@link
|
|
36
|
+
* le défi de repli {@link SESSION_AUTH_CHALLENGE}.
|
|
31
37
|
*
|
|
32
38
|
* CORS, CSRF et autorisation par décorateurs viennent se brancher en S4/S5.
|
|
33
39
|
* Toutes les structures sont **lazy** (perf : une app sans zone = zéro alloc).
|
|
@@ -141,20 +147,35 @@ declare class Firewall extends Service implements IFirewall {
|
|
|
141
147
|
/**
|
|
142
148
|
* En-têtes de sécurité APPLICATIFS (P6 J5) — CSP, Referrer-Policy, isolation
|
|
143
149
|
* cross-origin (COOP/COEP/CORP), Origin-Agent-Cluster, Permissions-Policy.
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* SANS le ré-émettre. No-op si désactivé
|
|
150
|
+
* Appelé par la réponse juste avant son `writeHead`, une fois son type fixé :
|
|
151
|
+
* la politique suit ce type (ADR-0024 §C11). Complète le socle transport de
|
|
152
|
+
* `@nodefony/http` (nosniff/frame/HSTS) SANS le ré-émettre. No-op si désactivé
|
|
153
|
+
* ou réponse non-HTTP (WS).
|
|
147
154
|
*
|
|
148
155
|
* En-têtes constants = table figée pré-calculée au boot (0 alloc/concat). Le CSP
|
|
149
|
-
* nonce/req
|
|
156
|
+
* nonce/req ajoute 1 `join` + 1 `setHeader`, sur un document seulement.
|
|
157
|
+
*
|
|
158
|
+
* @param context - contexte de la réponse.
|
|
159
|
+
* @param contentType - `Content-Type` de la réponse ; `null` = sans contenu ;
|
|
160
|
+
* omis = type inconnu, servi comme un document (politique complète).
|
|
161
|
+
* @param servedAsFile - le corps est servi tel quel (fichier statique,
|
|
162
|
+
* relais) : aucun gabarit ne l'a écrit, il ne peut pas porter de nonce. Un
|
|
163
|
+
* document reçoit alors la politique sans nonce ni `'strict-dynamic'`
|
|
164
|
+
* (ADR-0024 §C1) ; les autres types n'en changent pas.
|
|
150
165
|
*/
|
|
151
|
-
applySecurityHeaders(context: ContextType): void;
|
|
166
|
+
applySecurityHeaders(context: ContextType, contentType?: string | null, servedAsFile?: boolean): void;
|
|
152
167
|
/**
|
|
153
168
|
* Déclare des directives CSP additionnelles pour `moduleName` (cf `IFirewall`).
|
|
154
169
|
* No-op si les en-têtes applicatifs sont désactivés (pas de CSP à étendre).
|
|
155
170
|
* Recompose `#securityHeaders` (merge + re-split nonce) — hors hot-path.
|
|
156
171
|
*/
|
|
157
|
-
registerCspOrigins(moduleName: string,
|
|
172
|
+
registerCspOrigins(moduleName: string, declared: CspFragment): void;
|
|
173
|
+
/**
|
|
174
|
+
* Le noyau HTTP a recalculé ses en-têtes de transport (édition à chaud de sa
|
|
175
|
+
* config) : la politique suit `frameOptions`, qui pilote aussi
|
|
176
|
+
* `frame-ancestors` (ADR-0024 §C5). Hors chemin chaud.
|
|
177
|
+
*/
|
|
178
|
+
onTransportHeadersChanged(): void;
|
|
158
179
|
/** Retire les directives CSP de `moduleName` et recompose si nécessaire. */
|
|
159
180
|
unregisterCspOrigins(moduleName: string): void;
|
|
160
181
|
log(pci: unknown, severity?: Severity, msgid?: Msgid, msg?: Message): Pdu;
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { type CspFragment } from "../src/csp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Famille d'une réponse au regard de la politique de contenu (ADR-0024 §C11) :
|
|
4
|
+
* `document` (HTML, XHTML, SVG — politique complète avec nonce et
|
|
5
|
+
* `Permissions-Policy`), `script` (JavaScript — politique complète SANS nonce,
|
|
6
|
+
* celle que prend un worker), `other` (tout le reste — politique courte figée).
|
|
7
|
+
*/
|
|
8
|
+
export type ResponseKind = "document" | "script" | "other";
|
|
9
|
+
/**
|
|
10
|
+
* Range une réponse dans sa famille d'après son `Content-Type`.
|
|
11
|
+
*
|
|
12
|
+
* @param contentType - valeur de `Content-Type` (paramètres admis) ; `null` =
|
|
13
|
+
* réponse sans contenu ; `undefined` = type INCONNU de l'appelant, traité en
|
|
14
|
+
* document — la politique la plus complète, jamais une plus faible par défaut.
|
|
15
|
+
* @returns la famille dont la réponse reçoit la politique.
|
|
16
|
+
*/
|
|
17
|
+
export declare function responseKindOf(contentType: string | null | undefined): ResponseKind;
|
|
2
18
|
/**
|
|
3
19
|
* Sous-ensemble APPLICATIF de la config `headers` (cf defineSecurityConfig).
|
|
4
20
|
*
|
|
5
21
|
* `hsts` / `frameguard` / `noSniff` n'y figurent PAS volontairement : ces trois
|
|
6
|
-
* en-têtes « transport » sont posés par `@nodefony/http`
|
|
7
|
-
* (`
|
|
22
|
+
* en-têtes « transport » sont posés par `@nodefony/http` sur toute réponse
|
|
23
|
+
* (`HttpKernel.transportHeaders`, figés au démarrage) pour couvrir AUSSI les fichiers statiques,
|
|
8
24
|
* les erreurs précoces et un serveur sans module security (secure-by-default).
|
|
9
25
|
* `@nodefony/security` ne les ré-émet pas → une seule source par en-tête.
|
|
10
26
|
*/
|
|
@@ -18,6 +34,12 @@ export interface ISecurityHeadersOptions {
|
|
|
18
34
|
corp?: string | undefined;
|
|
19
35
|
originAgentCluster?: boolean | undefined;
|
|
20
36
|
permissionsPolicy?: string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Valeur de `Reporting-Endpoints` (`groupe="url"`), quand la politique
|
|
39
|
+
* désigne un groupe `report-to` composé par le framework (ADR-0024 §C7).
|
|
40
|
+
* Posé là où la politique complète part : document, script, fichier servi.
|
|
41
|
+
*/
|
|
42
|
+
reportingEndpoints?: string | null | undefined;
|
|
21
43
|
}
|
|
22
44
|
/**
|
|
23
45
|
* En-têtes de sécurité **applicatifs** de Nodefony (couche `@nodefony/security`,
|
|
@@ -26,19 +48,65 @@ export interface ISecurityHeadersOptions {
|
|
|
26
48
|
* cross-origin COOP/COEP/CORP, Origin-Agent-Cluster, Permissions-Policy) → zéro
|
|
27
49
|
* alloc, zéro concat par requête (le firewall la pose telle quelle).
|
|
28
50
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
51
|
+
* Pour le CSP, deux régimes mutuellement exclusifs : **statique** (posé tel quel via
|
|
52
|
+
* `headers`, 0 alloc/req) OU **nonce par requête** (`cspNonces` + placeholder
|
|
53
|
+
* `{{nonce}}`) → segments pré-split au boot, recomposés par requête via
|
|
54
|
+
* `cspFor(nonce)` (1 concaténation, aucun parse/regex dans le hot-path).
|
|
55
|
+
*
|
|
56
|
+
* **Deux variantes, composées au boot** (ADR-0024 §C6) : la politique telle
|
|
57
|
+
* qu'écrite, servie en TLS, et la même SANS `upgrade-insecure-requests`, servie
|
|
58
|
+
* en clair — sur une page `http`, la directive réécrirait les sous-ressources en
|
|
59
|
+
* `https`, que le serveur ne sert pas. Sans la directive, les deux variantes sont
|
|
60
|
+
* le MÊME objet : par requête, un booléen choisit, rien de plus.
|
|
34
61
|
*
|
|
35
62
|
* @see Fetch Metadata · W3C CSP Level 3 · WHATWG (COOP/COEP/CORP).
|
|
36
63
|
*/
|
|
37
64
|
export declare class SecurityHeaders {
|
|
38
65
|
#private;
|
|
39
|
-
|
|
40
|
-
|
|
66
|
+
/**
|
|
67
|
+
* @param o - options des en-têtes (config `security.headers`, CSP déjà fusionnée).
|
|
68
|
+
* @param onRejectedExtra - appelé quand les directives `@Csp` d'une route sortent
|
|
69
|
+
* de la grammaire CSP3 : la route sert alors la politique de base.
|
|
70
|
+
*/
|
|
71
|
+
constructor(o: ISecurityHeadersOptions, onRejectedExtra?: (error: unknown) => void);
|
|
72
|
+
/**
|
|
73
|
+
* Table d'en-têtes figée d'un DOCUMENT servi tel quel — fichier statique,
|
|
74
|
+
* réponse relayée — dont le corps ne passe par aucun gabarit et ne peut donc
|
|
75
|
+
* pas porter de nonce (ADR-0024 §C1) : politique complète sans nonce ni
|
|
76
|
+
* `'strict-dynamic'` orphelin, et tous les en-têtes d'un document.
|
|
77
|
+
*
|
|
78
|
+
* @param tls - la page est servie en `https` (garde `upgrade-insecure-requests`).
|
|
79
|
+
* @returns la table à poser telle quelle.
|
|
80
|
+
*/
|
|
81
|
+
headersForServedFile(tls: boolean): Readonly<Record<string, string>>;
|
|
82
|
+
/**
|
|
83
|
+
* Table d'en-têtes figée d'une réponse qui n'est pas un document (ADR-0024
|
|
84
|
+
* §C11) : JavaScript (politique complète sans nonce, Referrer-Policy, COEP,
|
|
85
|
+
* CORP) ou reste (politique courte `default-src 'none'`, Referrer-Policy,
|
|
86
|
+
* CORP). Ni l'une ni l'autre ne porte de nonce ; COOP, Origin-Agent-Cluster
|
|
87
|
+
* et Permissions-Policy, lus sur un document seulement, n'y figurent pas.
|
|
88
|
+
*
|
|
89
|
+
* @param kind - famille de la réponse (cf {@link responseKindOf}).
|
|
90
|
+
* @param tls - la page est servie en `https` (garde `upgrade-insecure-requests`).
|
|
91
|
+
* @returns la table à poser telle quelle.
|
|
92
|
+
*/
|
|
93
|
+
headersForResource(kind: Exclude<ResponseKind, "document">, tls: boolean): Readonly<Record<string, string>>;
|
|
94
|
+
/** Table d'en-têtes applicatifs CONSTANTS (figée) d'une réponse en clair. */
|
|
41
95
|
get headers(): Readonly<Record<string, string>>;
|
|
96
|
+
/**
|
|
97
|
+
* Table constante SANS la CSP statique d'une réponse en clair — pour une
|
|
98
|
+
* réponse qui pose sa propre politique (route `@Csp`), afin que la CSP ne
|
|
99
|
+
* soit écrite qu'une fois.
|
|
100
|
+
*/
|
|
101
|
+
get headersWithoutCsp(): Readonly<Record<string, string>>;
|
|
102
|
+
/**
|
|
103
|
+
* Table d'en-têtes constants selon le transport de la réponse.
|
|
104
|
+
*
|
|
105
|
+
* @param tls - la page est servie en `https` (proxy de confiance compris).
|
|
106
|
+
* @param withoutCsp - omettre la CSP statique (route `@Csp` qui pose la sienne).
|
|
107
|
+
* @returns la table figée à poser telle quelle.
|
|
108
|
+
*/
|
|
109
|
+
headersFor(tls: boolean, withoutCsp?: boolean): Readonly<Record<string, string>>;
|
|
42
110
|
/** `true` si le CSP est recomposé par requête avec un nonce (→ `cspFor`). */
|
|
43
111
|
get hasNonce(): boolean;
|
|
44
112
|
/**
|
|
@@ -48,9 +116,11 @@ export declare class SecurityHeaders {
|
|
|
48
116
|
* `'`/`;`/espace) → aucune évasion possible du token CSP.
|
|
49
117
|
*
|
|
50
118
|
* @param nonce - nonce base64 de la requête (`Context.cspNonce`).
|
|
119
|
+
* @param tls - la page est servie en `https` : la politique garde
|
|
120
|
+
* `upgrade-insecure-requests`. Défaut : en clair.
|
|
51
121
|
* @returns la valeur `Content-Security-Policy` à poser sur la réponse.
|
|
52
122
|
*/
|
|
53
|
-
cspFor(nonce: string): string;
|
|
123
|
+
cspFor(nonce: string, tls?: boolean): string;
|
|
54
124
|
/**
|
|
55
125
|
* Recompose le CSP en fusionnant ADDITIVEMENT les directives `extra` d'une
|
|
56
126
|
* route (`@Csp`) dans le CSP de base, PUIS en substituant le `nonce`. Le merge
|
|
@@ -60,8 +130,9 @@ export declare class SecurityHeaders {
|
|
|
60
130
|
*
|
|
61
131
|
* @param nonce - nonce base64 de la requête (ignoré si le CSP n'a pas de nonce).
|
|
62
132
|
* @param extra - directives additionnelles de la route (`directive → sources`).
|
|
133
|
+
* @param tls - la page est servie en `https`. Défaut : en clair.
|
|
63
134
|
* @returns la valeur `Content-Security-Policy` à poser sur la réponse.
|
|
64
135
|
*/
|
|
65
|
-
cspForExtra(nonce: string, extra: CspFragment): string;
|
|
136
|
+
cspForExtra(nonce: string, extra: CspFragment, tls?: boolean): string;
|
|
66
137
|
}
|
|
67
138
|
export default SecurityHeaders;
|
|
@@ -143,3 +143,16 @@ declare class WebAuthnService extends Service {
|
|
|
143
143
|
}
|
|
144
144
|
export default WebAuthnService;
|
|
145
145
|
export { WebAuthnService };
|
|
146
|
+
/**
|
|
147
|
+
* Origines WebAuthn rendues en ASCII (punycode), forme sous laquelle le
|
|
148
|
+
* navigateur les présente — `https://café.example` → `https://xn--caf-dma.example`.
|
|
149
|
+
*
|
|
150
|
+
* Une entrée qui n'est pas une origine (`scheme://hôte[:port]`) est écartée et
|
|
151
|
+
* signalée : la garder ferait échouer CHAQUE cérémonie, sans que personne ne
|
|
152
|
+
* sache pourquoi.
|
|
153
|
+
*
|
|
154
|
+
* @param origins - origines telles qu'écrites dans la config.
|
|
155
|
+
* @param onInvalid - appelé pour chaque entrée écartée.
|
|
156
|
+
* @returns les origines valides, sérialisées en ASCII, dans l'ordre reçu.
|
|
157
|
+
*/
|
|
158
|
+
export declare function asciiOrigins(origins: readonly string[], onInvalid: (origin: string) => void): string[];
|
|
@@ -17,7 +17,10 @@ export interface IWebhookRegisterInput {
|
|
|
17
17
|
readonly enabled?: boolean;
|
|
18
18
|
/** Identité de l'admin créateur (traçabilité). */
|
|
19
19
|
readonly createdBy?: string | null;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Périmètre d'un locataire — réservé, **refusé s'il est rempli** tant que le
|
|
22
|
+
* multi-tenant n'existe pas (ADR-0023 W2). Absent ou `null` : abonnement sans périmètre.
|
|
23
|
+
*/
|
|
21
24
|
readonly tenantId?: string | null;
|
|
22
25
|
/** Métadonnées extensibles. */
|
|
23
26
|
readonly metadata?: Record<string, unknown>;
|
|
@@ -79,6 +82,9 @@ declare class WebhookService extends Service {
|
|
|
79
82
|
* (la seule occasion de le lire pour le copier).
|
|
80
83
|
*
|
|
81
84
|
* @throws SsrfError si l'URL est invalide / cible non publique.
|
|
85
|
+
* @throws Error si un motif de `events` porte plus de trois `*` (ADR-0023 W1).
|
|
86
|
+
* @throws Error si `tenantId` est rempli : le périmètre d'un locataire n'est pas
|
|
87
|
+
* encore appliqué à la livraison (ADR-0023 W2) — l'accepter le rendrait trompeur.
|
|
82
88
|
*/
|
|
83
89
|
register(input: IWebhookRegisterInput): Promise<IWebhookSecretReveal>;
|
|
84
90
|
/**
|
|
@@ -52,6 +52,9 @@ export declare function parseAuditQuery(query: Readonly<Record<string, string |
|
|
|
52
52
|
* - `GET /nodefony/security/api/firewall` — introspection du firewall (zones,
|
|
53
53
|
* authenticators montés, défenses) — état RUNTIME, secrets exclus.
|
|
54
54
|
* - `GET /nodefony/security/api/roleHierarchy` — hiérarchie de rôles + résolution.
|
|
55
|
+
* - `GET /nodefony/security/api/csp-reports` — violations CSP reçues (ADR-0024
|
|
56
|
+
* §C7), dédoublonnées, la plus récente d’abord ; champs écrits par le
|
|
57
|
+
* navigateur, donc à afficher comme du texte.
|
|
55
58
|
* - `GET /nodefony/security/api/apikeys` — toutes les clés API (gouvernance),
|
|
56
59
|
* `GET …/apikeys/status` — backend du token store (« où on écrit »),
|
|
57
60
|
* `POST …/apikeys/{id}/revoke` — révocation par id (réponse à incident).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { IAuditEventDraft } from "../../contracts/IAuditEvent.js";
|
|
2
|
+
/** Réglages de l'agrégation des refus (section `security.audit.aggregate`). */
|
|
3
|
+
export interface IAuditAggregateOptions {
|
|
4
|
+
/** Durée d'une fenêtre, en ms, ouverte au premier refus d'une clé. */
|
|
5
|
+
windowMs: number;
|
|
6
|
+
/** Refus d'une clé écrits tels quels par fenêtre ; les suivants sont comptés. */
|
|
7
|
+
burst: number;
|
|
8
|
+
/** Clés suivies au plus ; au-delà, les refus se regroupent sans l'IP. */
|
|
9
|
+
maxKeys: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Agrège les REFUS du journal d'audit par source, pour qu'un flot ne se paie pas
|
|
13
|
+
* d'une ligne par requête (OWASP Logging Cheat Sheet : la journalisation ne doit
|
|
14
|
+
* pas épuiser les ressources ni le journal de la base).
|
|
15
|
+
*
|
|
16
|
+
* Clé : catégorie, action, motif, ressource et IP — l'IP résolue par le contexte
|
|
17
|
+
* (`trustProxy`), donc non forgeable par le client. L'acteur n'entre PAS dans la
|
|
18
|
+
* clé : un bourrage d'identifiants change d'acteur à chaque essai, c'est sa
|
|
19
|
+
* source qui le trahit.
|
|
20
|
+
*
|
|
21
|
+
* Par clé et par fenêtre (ouverte au premier refus) : les `burst` premiers refus
|
|
22
|
+
* sont écrits tels quels — l'enquêteur voit les premiers essais, acteur compris ;
|
|
23
|
+
* les suivants sont seulement comptés, et la fenêtre close émet UNE ligne de
|
|
24
|
+
* synthèse (`metadata.aggregated`). Mémoire bornée : au-delà de `maxKeys` clés,
|
|
25
|
+
* les refus se regroupent sous l'IP `*`.
|
|
26
|
+
*
|
|
27
|
+
* Pur : l'horloge est passée à chaque appel, la synthèse part par `emit`. Les
|
|
28
|
+
* succès n'y passent jamais (l'appelant les écarte).
|
|
29
|
+
*/
|
|
30
|
+
export declare class AuditRefusalAggregator {
|
|
31
|
+
#private;
|
|
32
|
+
/**
|
|
33
|
+
* @param options - fenêtre, rafale écrite telle quelle, borne de clés.
|
|
34
|
+
* @param emit - écrit une ligne de synthèse dans le journal.
|
|
35
|
+
*/
|
|
36
|
+
constructor(options: IAuditAggregateOptions, emit: (draft: IAuditEventDraft) => void);
|
|
37
|
+
/** Nombre de clés suivies (fenêtres ouvertes). */
|
|
38
|
+
get size(): number;
|
|
39
|
+
/**
|
|
40
|
+
* Compte un refus.
|
|
41
|
+
*
|
|
42
|
+
* @param draft - l'événement de refus.
|
|
43
|
+
* @param now - horloge, en ms.
|
|
44
|
+
* @returns `true` : l'écrire tel quel ; `false` : il est compté dans la synthèse.
|
|
45
|
+
*/
|
|
46
|
+
admit(draft: IAuditEventDraft, now: number): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Ferme les fenêtres échues : émet leur synthèse et libère leurs clés.
|
|
49
|
+
*
|
|
50
|
+
* @param now - horloge, en ms.
|
|
51
|
+
*/
|
|
52
|
+
sweep(now: number): void;
|
|
53
|
+
/** Émet la synthèse de toutes les fenêtres ouvertes (arrêt) et vide la table. */
|
|
54
|
+
flush(): void;
|
|
55
|
+
}
|
|
@@ -6,6 +6,17 @@ import type { ISecurityConfig } from "../../config/defineModuleConfig.js";
|
|
|
6
6
|
import { type IProviderRoleMapping } from "../oauth/providerRoles.js";
|
|
7
7
|
/** Comment le sujet d'un jeton devient un utilisateur de cette application. */
|
|
8
8
|
export type ExternalSubjectPolicy = "require" | "ephemeral";
|
|
9
|
+
/**
|
|
10
|
+
* Cet utilisateur a-t-il été fabriqué pour un appelant machine SANS compte local (mode `ephemeral`) ?
|
|
11
|
+
*
|
|
12
|
+
* L'identité d'admission temps réel en tire `account: false` : rien ne
|
|
13
|
+
* pourrait relire cet appelant par son `id` (`<émetteur>#<sujet>`), et un
|
|
14
|
+
* abonnement durable ne doit pas s'y adosser (ADR-0021 §S10 point 6). Lu sur
|
|
15
|
+
* le FAIT (l'objet vient du résolveur), jamais déduit de la forme de `id`.
|
|
16
|
+
*
|
|
17
|
+
* @param user - utilisateur résolu par le pare-feu.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isEphemeralSubject(user: IUser): boolean;
|
|
9
20
|
/** Un émetteur reconnu, et la façon dont ses sujets entrent chez nous. */
|
|
10
21
|
export interface IExternalIssuerBinding {
|
|
11
22
|
/** Émetteur de confiance, sous sa forme canonique. */
|
|
@@ -3,7 +3,7 @@ import type { ContextType } from "@nodefony/http";
|
|
|
3
3
|
import type { IUser } from "@nodefony/user";
|
|
4
4
|
import type { IAuthenticator } from "../../contracts/IAuthenticator.js";
|
|
5
5
|
import type { IToken } from "../../contracts/IToken.js";
|
|
6
|
-
import type
|
|
6
|
+
import { type IJwtRuntime } from "../token/jwtRuntime.js";
|
|
7
7
|
/**
|
|
8
8
|
* Authentification par **JWT Bearer** (RFC 6750) — réservée API service↔service /
|
|
9
9
|
* agents (le web utilise la session BFF). Vérifie un access token EdDSA signé par
|
|
@@ -15,7 +15,8 @@ import type { IJwtRuntime } from "../token/jwtRuntime.js";
|
|
|
15
15
|
* - **clé par `kid` depuis le keyset LOCAL** (`createLocalJWKSet`) — jamais
|
|
16
16
|
* `jku`/`jwk` de l'en-tête (injection de clé / SSRF, §3.5).
|
|
17
17
|
* - **`aud` (§3.9) + `iss` (§3.8) obligatoires** + `typ:"at+jwt"` (§3.11, sépare
|
|
18
|
-
* access et refresh) + `exp`/`nbf` (jose).
|
|
18
|
+
* access et refresh) + `exp`/`nbf` (jose). L'audience est celle de la ZONE
|
|
19
|
+
* (`resource`, ou l'audience par défaut), jamais toute la liste `jwt.audiences`.
|
|
19
20
|
* - **révocation** : denylist `jti` + seuil `invalidBefore` par porteur (le JWT
|
|
20
21
|
* est auto-porté et non révocable sans état serveur).
|
|
21
22
|
* - **sujet revérifié** (§3.10) : `loadUserByIdentifier(sub)` → compte disparu,
|
|
@@ -48,12 +49,25 @@ export declare class JwtAuthenticator implements IAuthenticator {
|
|
|
48
49
|
* qu'un credential qui disparaîtrait sans laisser de trace.
|
|
49
50
|
*/
|
|
50
51
|
supports(context: ContextType): boolean;
|
|
51
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Extrait le token brut (non vérifié) → porté par un `UserToken` type `"jwt"`,
|
|
54
|
+
* avec la ressource de la zone quand elle en déclare une.
|
|
55
|
+
*
|
|
56
|
+
* L'audience transite par le token parce que `authenticate()` ne reçoit pas
|
|
57
|
+
* le contexte (même patron que `ExternalJwtAuthenticator`). Rien n'est posé
|
|
58
|
+
* pour une zone sans ressource : l'audience par défaut s'y applique.
|
|
59
|
+
*/
|
|
52
60
|
createToken(context: ContextType): Promise<IToken>;
|
|
53
61
|
/**
|
|
54
62
|
* Vérifie la signature + les claims du JWT, applique la révocation et résout le
|
|
55
63
|
* sujet — ou lève un 401 au message uniforme.
|
|
56
64
|
*
|
|
65
|
+
* L'audience exigée est celle de la ZONE, jamais la liste blanche d'émission
|
|
66
|
+
* entière (RFC 8707, RFC 9068 §4) : la `resource` de la zone si elle en
|
|
67
|
+
* déclare une, sinon l'audience par défaut — l'application elle-même. Un
|
|
68
|
+
* jeton émis pour la porte MCP, ou pour la ressource d'une autre zone, figure
|
|
69
|
+
* dans `jwt.audiences` sans pour autant ouvrir celle-ci.
|
|
70
|
+
*
|
|
57
71
|
* @throws AuthenticationError (401) — token absent/invalide/expiré/révoqué, ou
|
|
58
72
|
* sujet disparu/banni.
|
|
59
73
|
* @throws Error (câblage : keystore/store/users absents) — logguée ERROR par le
|
|
@@ -57,8 +57,10 @@ export declare class SessionAuthenticator implements IAuthenticator {
|
|
|
57
57
|
* token. Les contrôles d'état (existe, actif, non verrouillé) vivent dans
|
|
58
58
|
* {@link resolveSessionIdentity} — partagés avec `AuthFlow.me()`.
|
|
59
59
|
*
|
|
60
|
-
* @throws
|
|
61
|
-
* compte verrouillé ou désactiv
|
|
60
|
+
* @throws StaleSessionError (401, message uniforme) — session orpheline,
|
|
61
|
+
* compte verrouillé ou désactivé, liaison au compte rompue : un état à
|
|
62
|
+
* purger ({@link onFailure}). Une PANNE de la source d'identité remonte
|
|
63
|
+
* telle quelle, et ne purge rien.
|
|
62
64
|
*/
|
|
63
65
|
authenticate(token: IToken): Promise<IToken>;
|
|
64
66
|
/**
|
|
@@ -66,7 +68,12 @@ export declare class SessionAuthenticator implements IAuthenticator {
|
|
|
66
68
|
* (`saveSession`) lie le blob au principal courant (string attendu).
|
|
67
69
|
*/
|
|
68
70
|
onSuccess(context: ContextType, token: IToken): Promise<void>;
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Une session périmée est DÉTRUITE, côté serveur (entrée du stockage) et
|
|
73
|
+
* côté navigateur (cookie expiré, RFC 6265 §5.3) — sans quoi le navigateur la
|
|
74
|
+
* renvoie à chaque requête et chaque page lui répond 401, sans issue pour
|
|
75
|
+
* l'utilisateur. Le firewall poursuit ensuite la chaîne de la zone.
|
|
76
|
+
*/
|
|
77
|
+
onFailure(context: ContextType, error: Error): Promise<void>;
|
|
71
78
|
}
|
|
72
79
|
export default SessionAuthenticator;
|