emilsoftware-utilities 2.0.0-dev.73 → 2.0.0-dev.75

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.
Files changed (45) hide show
  1. package/README.MD +2 -0
  2. package/dist/accessi/resource-server/token.js +62 -18
  3. package/dist/accessi/resource-server/token.js.map +1 -1
  4. package/dist/accessi/resource-server/types.d.ts +22 -0
  5. package/dist/accessi/resource-server/types.js.map +1 -1
  6. package/dist/accessi/server/index.d.ts +1 -1
  7. package/dist/accessi/server/index.js +2 -1
  8. package/dist/accessi/server/index.js.map +1 -1
  9. package/dist/accessi/server/standalone.d.ts +2 -4
  10. package/dist/accessi/server/standalone.js +4 -79
  11. package/dist/accessi/server/standalone.js.map +1 -1
  12. package/dist/accessi-module/AccessiModule.d.ts +12 -3
  13. package/dist/accessi-module/AccessiModule.js.map +1 -1
  14. package/dist/accessi-module/Console/console.js +2 -1
  15. package/dist/accessi-module/Console/wiki.js +2 -1
  16. package/dist/accessi-module/Console/wiki.js.map +1 -1
  17. package/dist/accessi-module/Services/AuthService/AuthService.js +2 -2
  18. package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
  19. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.d.ts +9 -1
  20. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js +53 -1
  21. package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js.map +1 -1
  22. package/dist/accessi-module/health/nest.d.ts +31 -0
  23. package/dist/accessi-module/health/nest.js +89 -0
  24. package/dist/accessi-module/health/nest.js.map +1 -0
  25. package/dist/accessi-module/health/readiness.d.ts +53 -0
  26. package/dist/accessi-module/health/readiness.js +110 -0
  27. package/dist/accessi-module/health/readiness.js.map +1 -0
  28. package/dist/accessi-module/index.d.ts +6 -0
  29. package/dist/accessi-module/index.js +16 -1
  30. package/dist/accessi-module/index.js.map +1 -1
  31. package/dist/accessi-module/jwt/jwt.strategy.js +1 -35
  32. package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
  33. package/dist/accessi-module/middleware/authenticateGen.js +1 -35
  34. package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
  35. package/dist/accessi-module/security/jwtTokenOptions.d.ts +44 -9
  36. package/dist/accessi-module/security/jwtTokenOptions.js +118 -7
  37. package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
  38. package/dist/accessi-module/security/validateOptions.js +31 -4
  39. package/dist/accessi-module/security/validateOptions.js.map +1 -1
  40. package/dist/emilsoftware-utilities-2.0.0-dev.75.tgz +0 -0
  41. package/docs/jwt-key-rotation.md +143 -0
  42. package/docs/proxy-ip.md +99 -0
  43. package/docs/standalone-service.md +25 -0
  44. package/package.json +5 -3
  45. package/dist/emilsoftware-utilities-2.0.0-dev.73.tgz +0 -0
@@ -1,22 +1,57 @@
1
- import type * as jwt from "jsonwebtoken";
1
+ import * as jwt from "jsonwebtoken";
2
2
  import type { AccessiOptions } from "../AccessiModule";
3
+ import type { AccessiJwtPurpose } from "./passwordResetToken";
3
4
  /** Algoritmi supportati per gli access token Accessi. */
4
5
  export type AccessiAccessTokenAlgorithm = "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "ES256" | "ES384" | "ES512";
6
+ /**
7
+ * Evento emesso quando un token presenta un `kid` che non appartiene né alla
8
+ * chiave corrente né alle chiavi precedenti configurate.
9
+ *
10
+ * Serve per collegare un alert operativo (log, metrica, webhook) senza
11
+ * accoppiare il modulo a uno specifico sistema di monitoraggio.
12
+ */
13
+ export interface AccessiUnknownKeyIdEvent {
14
+ keyId: string;
15
+ algorithm?: string;
16
+ /** `access` o `refresh`, per distinguere i token coinvolti. */
17
+ purpose: AccessiJwtPurpose;
18
+ }
19
+ /** Configurazione di verifica con chiave corrente, chiavi precedenti e alert. */
20
+ export interface AccessiJwtVerificationConfig {
21
+ /** Chiave corrente. Resta il campo principale per i consumer esistenti. */
22
+ key: jwt.Secret;
23
+ verifyOptions: jwt.VerifyOptions;
24
+ /** Chiavi indicizzate per `kid`: la corrente vince sulle precedenti. */
25
+ keysByKeyId: ReadonlyMap<string, jwt.Secret>;
26
+ /** Chiavi provate per i token legacy privi di `kid`, corrente per prima. */
27
+ fallbackKeys: readonly jwt.Secret[];
28
+ purpose: AccessiJwtPurpose;
29
+ onUnknownKeyId?: (event: AccessiUnknownKeyIdEvent) => void;
30
+ }
31
+ /**
32
+ * Chiavi da provare per un token.
33
+ *
34
+ * Un `kid` noto seleziona una sola chiave: non si ricade sulle altre, così un
35
+ * token con intestazione manomessa non viene verificato per tentativi.
36
+ */
37
+ export declare function resolveAccessiVerificationKeys(config: AccessiJwtVerificationConfig, token: string): readonly jwt.Secret[];
38
+ /**
39
+ * Verifica un JWT Accessi con la chiave corretta (corrente o precedente).
40
+ *
41
+ * Lancia l'errore di `jsonwebtoken` quando nessuna chiave candidata verifica il
42
+ * token; per un `kid` sconosciuto lancia un errore dedicato dopo aver emesso
43
+ * l'evento di alert.
44
+ */
45
+ export declare function verifyAccessiJwt(token: string, config: AccessiJwtVerificationConfig): unknown;
5
46
  export declare function getAccessiAccessTokenSigningConfig(options: AccessiOptions): {
6
47
  key: jwt.Secret;
7
48
  algorithm: AccessiAccessTokenAlgorithm;
8
49
  signOptions: Pick<jwt.SignOptions, "issuer" | "audience" | "keyid">;
9
50
  };
10
- export declare function getAccessiAccessTokenVerificationConfig(options: AccessiOptions): {
11
- key: jwt.Secret;
12
- verifyOptions: jwt.VerifyOptions;
13
- };
51
+ export declare function getAccessiAccessTokenVerificationConfig(options: AccessiOptions): AccessiJwtVerificationConfig;
14
52
  export declare function getAccessiRefreshTokenSigningConfig(options: AccessiOptions): {
15
53
  key: jwt.Secret;
16
54
  algorithm: AccessiAccessTokenAlgorithm;
17
55
  signOptions: Pick<jwt.SignOptions, "issuer" | "audience" | "keyid">;
18
56
  };
19
- export declare function getAccessiRefreshTokenVerificationConfig(options: AccessiOptions): {
20
- key: jwt.Secret;
21
- verifyOptions: jwt.VerifyOptions;
22
- };
57
+ export declare function getAccessiRefreshTokenVerificationConfig(options: AccessiOptions): AccessiJwtVerificationConfig;
@@ -1,9 +1,45 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.resolveAccessiVerificationKeys = resolveAccessiVerificationKeys;
37
+ exports.verifyAccessiJwt = verifyAccessiJwt;
3
38
  exports.getAccessiAccessTokenSigningConfig = getAccessiAccessTokenSigningConfig;
4
39
  exports.getAccessiAccessTokenVerificationConfig = getAccessiAccessTokenVerificationConfig;
5
40
  exports.getAccessiRefreshTokenSigningConfig = getAccessiRefreshTokenSigningConfig;
6
41
  exports.getAccessiRefreshTokenVerificationConfig = getAccessiRefreshTokenVerificationConfig;
42
+ const jwt = __importStar(require("jsonwebtoken"));
7
43
  const passwordResetToken_1 = require("./passwordResetToken");
8
44
  function resolveAlgorithm(options) {
9
45
  var _a;
@@ -26,9 +62,16 @@ function getTokenSigningConfig(options, purpose) {
26
62
  : {})), (options.jwtOptions.keyId ? { keyid: options.jwtOptions.keyId } : {})),
27
63
  };
28
64
  }
29
- /** Configurazione di verifica: chiave pubblica per RS/ES, segreto derivato per HS. */
65
+ /**
66
+ * Configurazione di verifica.
67
+ *
68
+ * Con algoritmi asimmetrici accetta anche le chiavi precedenti: un token
69
+ * firmato prima di una rotazione resta valido finché la sua chiave pubblica
70
+ * compare in `previousPublicKeys`. I token legacy senza `kid` vengono provati
71
+ * su tutte le chiavi candidate, la corrente per prima.
72
+ */
30
73
  function getTokenVerificationConfig(options, purpose) {
31
- var _a;
74
+ var _a, _b, _c;
32
75
  const algorithm = resolveAlgorithm(options);
33
76
  const key = algorithm.startsWith("HS")
34
77
  ? (0, passwordResetToken_1.getAccessiJwtSecret)(options, purpose)
@@ -36,12 +79,80 @@ function getTokenVerificationConfig(options, purpose) {
36
79
  if (!key) {
37
80
  throw new Error(`jwtOptions.publicKey e obbligatoria per verificare token ${algorithm}.`);
38
81
  }
39
- return {
40
- key,
41
- verifyOptions: Object.assign(Object.assign({ algorithms: [algorithm] }, (options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {})), (options.jwtOptions.audience
82
+ const algorithms = new Set([algorithm]);
83
+ const keysByKeyId = new Map();
84
+ const fallbackKeys = [key];
85
+ if (options.jwtOptions.keyId) {
86
+ keysByKeyId.set(options.jwtOptions.keyId, key);
87
+ }
88
+ if (!algorithm.startsWith("HS")) {
89
+ for (const previous of (_b = options.jwtOptions.previousPublicKeys) !== null && _b !== void 0 ? _b : []) {
90
+ const previousAlgorithm = (_c = previous.algorithm) !== null && _c !== void 0 ? _c : algorithm;
91
+ algorithms.add(previousAlgorithm);
92
+ fallbackKeys.push(previous.publicKey);
93
+ if (previous.keyId && !keysByKeyId.has(previous.keyId)) {
94
+ keysByKeyId.set(previous.keyId, previous.publicKey);
95
+ }
96
+ }
97
+ }
98
+ return Object.assign({ key, verifyOptions: Object.assign(Object.assign({ algorithms: [...algorithms] }, (options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {})), (options.jwtOptions.audience
42
99
  ? { audience: options.jwtOptions.audience }
43
- : {})),
44
- };
100
+ : {})), keysByKeyId,
101
+ fallbackKeys,
102
+ purpose }, (options.jwtOptions.onUnknownKeyId
103
+ ? { onUnknownKeyId: options.jwtOptions.onUnknownKeyId }
104
+ : {}));
105
+ }
106
+ function readTokenHeader(token) {
107
+ const decoded = jwt.decode(token, { complete: true });
108
+ if (!decoded || typeof decoded === "string" || !decoded.header)
109
+ return {};
110
+ const header = decoded.header;
111
+ return Object.assign(Object.assign({}, (typeof header.kid === "string" && header.kid !== "" ? { keyId: header.kid } : {})), (typeof header.alg === "string" ? { algorithm: header.alg } : {}));
112
+ }
113
+ /**
114
+ * Chiavi da provare per un token.
115
+ *
116
+ * Un `kid` noto seleziona una sola chiave: non si ricade sulle altre, così un
117
+ * token con intestazione manomessa non viene verificato per tentativi.
118
+ */
119
+ function resolveAccessiVerificationKeys(config, token) {
120
+ var _a, _b;
121
+ const header = readTokenHeader(token);
122
+ // Con HMAC il kid non partecipa alla crittografia: un kid cambiato non deve
123
+ // invalidare token firmati con lo stesso segreto.
124
+ const hmacOnly = ((_a = config.verifyOptions.algorithms) !== null && _a !== void 0 ? _a : []).every((algorithm) => String(algorithm).startsWith("HS"));
125
+ if (!header.keyId || hmacOnly)
126
+ return config.fallbackKeys;
127
+ const key = config.keysByKeyId.get(header.keyId);
128
+ if (key)
129
+ return [key];
130
+ (_b = config.onUnknownKeyId) === null || _b === void 0 ? void 0 : _b.call(config, Object.assign(Object.assign({ keyId: header.keyId }, (header.algorithm ? { algorithm: header.algorithm } : {})), { purpose: config.purpose }));
131
+ return [];
132
+ }
133
+ /**
134
+ * Verifica un JWT Accessi con la chiave corretta (corrente o precedente).
135
+ *
136
+ * Lancia l'errore di `jsonwebtoken` quando nessuna chiave candidata verifica il
137
+ * token; per un `kid` sconosciuto lancia un errore dedicato dopo aver emesso
138
+ * l'evento di alert.
139
+ */
140
+ function verifyAccessiJwt(token, config) {
141
+ const keys = resolveAccessiVerificationKeys(config, token);
142
+ if (keys.length === 0) {
143
+ const { keyId } = readTokenHeader(token);
144
+ throw new Error(`Chiave JWT sconosciuta per kid "${keyId !== null && keyId !== void 0 ? keyId : "assente"}".`);
145
+ }
146
+ let lastError;
147
+ for (const key of keys) {
148
+ try {
149
+ return jwt.verify(token, key, config.verifyOptions);
150
+ }
151
+ catch (error) {
152
+ lastError = error;
153
+ }
154
+ }
155
+ throw lastError instanceof Error ? lastError : new Error("Token JWT non valido.");
45
156
  }
46
157
  function getAccessiAccessTokenSigningConfig(options) {
47
158
  return getTokenSigningConfig(options, "access");
@@ -1 +1 @@
1
- {"version":3,"file":"jwtTokenOptions.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwtTokenOptions.ts"],"names":[],"mappings":"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","sourcesContent":["import type * as jwt from \"jsonwebtoken\";\nimport type { AccessiOptions } from \"../AccessiModule\";\nimport { getAccessiJwtSecret } from \"./passwordResetToken\";\nimport type { AccessiJwtPurpose } from \"./passwordResetToken\";\n\n/** Algoritmi supportati per gli access token Accessi. */\nexport type AccessiAccessTokenAlgorithm =\n | \"HS256\"\n | \"HS384\"\n | \"HS512\"\n | \"RS256\"\n | \"RS384\"\n | \"RS512\"\n | \"ES256\"\n | \"ES384\"\n | \"ES512\";\n\nfunction resolveAlgorithm(options: AccessiOptions): AccessiAccessTokenAlgorithm {\n return options.jwtOptions.algorithm ?? \"HS256\";\n}\n\n/** Configurazione di firma: chiave, algoritmo e claim standard. */\nfunction getTokenSigningConfig(\n options: AccessiOptions,\n purpose: AccessiJwtPurpose,\n): {\n key: jwt.Secret;\n algorithm: AccessiAccessTokenAlgorithm;\n signOptions: Pick<jwt.SignOptions, \"issuer\" | \"audience\" | \"keyid\">;\n} {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options, purpose)\n : options.jwtOptions.privateKey;\n\n if (!key) {\n throw new Error(`jwtOptions.privateKey e obbligatoria per l'algoritmo ${algorithm}.`);\n }\n\n return {\n key,\n algorithm,\n signOptions: {\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.SignOptions[\"audience\"] }\n : {}),\n ...(options.jwtOptions.keyId ? { keyid: options.jwtOptions.keyId } : {}),\n },\n };\n}\n\n/** Configurazione di verifica: chiave pubblica per RS/ES, segreto derivato per HS. */\nfunction getTokenVerificationConfig(\n options: AccessiOptions,\n purpose: AccessiJwtPurpose,\n): {\n key: jwt.Secret;\n verifyOptions: jwt.VerifyOptions;\n} {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options, purpose)\n : (options.jwtOptions.publicKey ?? options.jwtOptions.privateKey);\n\n if (!key) {\n throw new Error(\n `jwtOptions.publicKey e obbligatoria per verificare token ${algorithm}.`,\n );\n }\n\n return {\n key,\n verifyOptions: {\n algorithms: [algorithm],\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.VerifyOptions[\"audience\"] }\n : {}),\n },\n };\n}\n\nexport function getAccessiAccessTokenSigningConfig(options: AccessiOptions) {\n return getTokenSigningConfig(options, \"access\");\n}\n\nexport function getAccessiAccessTokenVerificationConfig(options: AccessiOptions) {\n return getTokenVerificationConfig(options, \"access\");\n}\n\nexport function getAccessiRefreshTokenSigningConfig(options: AccessiOptions) {\n return getTokenSigningConfig(options, \"refresh\");\n}\n\nexport function getAccessiRefreshTokenVerificationConfig(options: AccessiOptions) {\n return getTokenVerificationConfig(options, \"refresh\");\n}\n"]}
1
+ {"version":3,"file":"jwtTokenOptions.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwtTokenOptions.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2JA,wEAqBC;AASD,4CAiBC;AAED,gFAEC;AAED,0FAEC;AAED,kFAEC;AAED,4FAEC;AA1ND,kDAAoC;AAEpC,6DAA2D;AA0C3D,SAAS,gBAAgB,CAAC,OAAuB;;IAC/C,OAAO,MAAA,OAAO,CAAC,UAAU,CAAC,SAAS,mCAAI,OAAO,CAAC;AACjD,CAAC;AAED,mEAAmE;AACnE,SAAS,qBAAqB,CAC5B,OAAuB,EACvB,OAA0B;IAM1B,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC;QACpC,CAAC,CAAC,IAAA,wCAAmB,EAAC,OAAO,EAAE,OAAO,CAAC;QACvC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC;IAElC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,wDAAwD,SAAS,GAAG,CAAC,CAAC;IACxF,CAAC;IAED,OAAO;QACL,GAAG;QACH,SAAS;QACT,WAAW,gDACN,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GACxE,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ;YAC7B,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAAuC,EAAE;YAC1E,CAAC,CAAC,EAAE,CAAC,GACJ,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACzE;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,0BAA0B,CACjC,OAAuB,EACvB,OAA0B;;IAE1B,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC;QACpC,CAAC,CAAC,IAAA,wCAAmB,EAAC,OAAO,EAAE,OAAO,CAAC;QACvC,CAAC,CAAC,CAAC,MAAA,OAAO,CAAC,UAAU,CAAC,SAAS,mCAAI,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;IAEpE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,4DAA4D,SAAS,GAAG,CACzE,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAS,CAAC,SAAS,CAAC,CAAC,CAAC;IAChD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAsB,CAAC;IAClD,MAAM,YAAY,GAAiB,CAAC,GAAG,CAAC,CAAC;IAEzC,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;QAC7B,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAChC,KAAK,MAAM,QAAQ,IAAI,MAAA,OAAO,CAAC,UAAU,CAAC,kBAAkB,mCAAI,EAAE,EAAE,CAAC;YACnE,MAAM,iBAAiB,GAAG,MAAA,QAAQ,CAAC,SAAS,mCAAI,SAAS,CAAC;YAC1D,UAAU,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;YAClC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YACtC,IAAI,QAAQ,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvD,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;YACtD,CAAC;QACH,CAAC;IACH,CAAC;IAED,uBACE,GAAG,EACH,aAAa,gCACX,UAAU,EAAE,CAAC,GAAG,UAAU,CAAoB,IAC3C,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GACxE,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ;YAC7B,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,UAAU,CAAC,QAAyC,EAAE;YAC5E,CAAC,CAAC,EAAE,CAAC,GAET,WAAW;QACX,YAAY;QACZ,OAAO,IACJ,CAAC,OAAO,CAAC,UAAU,CAAC,cAAc;QACnC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,UAAU,CAAC,cAAc,EAAE;QACvD,CAAC,CAAC,EAAE,CAAC,EACP;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,KAAa;IACpC,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,EAAE,CAAC;IAC1E,MAAM,MAAM,GAAG,OAAO,CAAC,MAA0C,CAAC;IAClE,uCACK,CAAC,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,GAClF,CAAC,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EACpE;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAgB,8BAA8B,CAC5C,MAAoC,EACpC,KAAa;;IAEb,MAAM,MAAM,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACtC,4EAA4E;IAC5E,kDAAkD;IAClD,MAAM,QAAQ,GAAG,CAAC,MAAA,MAAM,CAAC,aAAa,CAAC,UAAU,mCAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,EAAE,CAC3E,MAAM,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CACnC,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,QAAQ;QAAE,OAAO,MAAM,CAAC,YAAY,CAAC;IAE1D,MAAM,GAAG,GAAG,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACjD,IAAI,GAAG;QAAE,OAAO,CAAC,GAAG,CAAC,CAAC;IAEtB,MAAA,MAAM,CAAC,cAAc,qFACnB,KAAK,EAAE,MAAM,CAAC,KAAK,IAChB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAC5D,OAAO,EAAE,MAAM,CAAC,OAAO,IACvB,CAAC;IACH,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,KAAa,EAAE,MAAoC;IAClF,MAAM,IAAI,GAAG,8BAA8B,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,IAAI,KAAK,CAAC,mCAAmC,KAAK,aAAL,KAAK,cAAL,KAAK,GAAI,SAAS,IAAI,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,SAAkB,CAAC;IACvB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,CAAC;YACH,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;QACtD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,SAAS,GAAG,KAAK,CAAC;QACpB,CAAC;IACH,CAAC;IAED,MAAM,SAAS,YAAY,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;AACpF,CAAC;AAED,SAAgB,kCAAkC,CAAC,OAAuB;IACxE,OAAO,qBAAqB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAClD,CAAC;AAED,SAAgB,uCAAuC,CAAC,OAAuB;IAC7E,OAAO,0BAA0B,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AACvD,CAAC;AAED,SAAgB,mCAAmC,CAAC,OAAuB;IACzE,OAAO,qBAAqB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACnD,CAAC;AAED,SAAgB,wCAAwC,CAAC,OAAuB;IAC9E,OAAO,0BAA0B,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACxD,CAAC","sourcesContent":["import * as jwt from \"jsonwebtoken\";\nimport type { AccessiOptions } from \"../AccessiModule\";\nimport { getAccessiJwtSecret } from \"./passwordResetToken\";\nimport type { AccessiJwtPurpose } from \"./passwordResetToken\";\n\n/** Algoritmi supportati per gli access token Accessi. */\nexport type AccessiAccessTokenAlgorithm =\n | \"HS256\"\n | \"HS384\"\n | \"HS512\"\n | \"RS256\"\n | \"RS384\"\n | \"RS512\"\n | \"ES256\"\n | \"ES384\"\n | \"ES512\";\n\n/**\n * Evento emesso quando un token presenta un `kid` che non appartiene né alla\n * chiave corrente né alle chiavi precedenti configurate.\n *\n * Serve per collegare un alert operativo (log, metrica, webhook) senza\n * accoppiare il modulo a uno specifico sistema di monitoraggio.\n */\nexport interface AccessiUnknownKeyIdEvent {\n keyId: string;\n algorithm?: string;\n /** `access` o `refresh`, per distinguere i token coinvolti. */\n purpose: AccessiJwtPurpose;\n}\n\n/** Configurazione di verifica con chiave corrente, chiavi precedenti e alert. */\nexport interface AccessiJwtVerificationConfig {\n /** Chiave corrente. Resta il campo principale per i consumer esistenti. */\n key: jwt.Secret;\n verifyOptions: jwt.VerifyOptions;\n /** Chiavi indicizzate per `kid`: la corrente vince sulle precedenti. */\n keysByKeyId: ReadonlyMap<string, jwt.Secret>;\n /** Chiavi provate per i token legacy privi di `kid`, corrente per prima. */\n fallbackKeys: readonly jwt.Secret[];\n purpose: AccessiJwtPurpose;\n onUnknownKeyId?: (event: AccessiUnknownKeyIdEvent) => void;\n}\n\nfunction resolveAlgorithm(options: AccessiOptions): AccessiAccessTokenAlgorithm {\n return options.jwtOptions.algorithm ?? \"HS256\";\n}\n\n/** Configurazione di firma: chiave, algoritmo e claim standard. */\nfunction getTokenSigningConfig(\n options: AccessiOptions,\n purpose: AccessiJwtPurpose,\n): {\n key: jwt.Secret;\n algorithm: AccessiAccessTokenAlgorithm;\n signOptions: Pick<jwt.SignOptions, \"issuer\" | \"audience\" | \"keyid\">;\n} {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options, purpose)\n : options.jwtOptions.privateKey;\n\n if (!key) {\n throw new Error(`jwtOptions.privateKey e obbligatoria per l'algoritmo ${algorithm}.`);\n }\n\n return {\n key,\n algorithm,\n signOptions: {\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.SignOptions[\"audience\"] }\n : {}),\n ...(options.jwtOptions.keyId ? { keyid: options.jwtOptions.keyId } : {}),\n },\n };\n}\n\n/**\n * Configurazione di verifica.\n *\n * Con algoritmi asimmetrici accetta anche le chiavi precedenti: un token\n * firmato prima di una rotazione resta valido finché la sua chiave pubblica\n * compare in `previousPublicKeys`. I token legacy senza `kid` vengono provati\n * su tutte le chiavi candidate, la corrente per prima.\n */\nfunction getTokenVerificationConfig(\n options: AccessiOptions,\n purpose: AccessiJwtPurpose,\n): AccessiJwtVerificationConfig {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options, purpose)\n : (options.jwtOptions.publicKey ?? options.jwtOptions.privateKey);\n\n if (!key) {\n throw new Error(\n `jwtOptions.publicKey e obbligatoria per verificare token ${algorithm}.`,\n );\n }\n\n const algorithms = new Set<string>([algorithm]);\n const keysByKeyId = new Map<string, jwt.Secret>();\n const fallbackKeys: jwt.Secret[] = [key];\n\n if (options.jwtOptions.keyId) {\n keysByKeyId.set(options.jwtOptions.keyId, key);\n }\n\n if (!algorithm.startsWith(\"HS\")) {\n for (const previous of options.jwtOptions.previousPublicKeys ?? []) {\n const previousAlgorithm = previous.algorithm ?? algorithm;\n algorithms.add(previousAlgorithm);\n fallbackKeys.push(previous.publicKey);\n if (previous.keyId && !keysByKeyId.has(previous.keyId)) {\n keysByKeyId.set(previous.keyId, previous.publicKey);\n }\n }\n }\n\n return {\n key,\n verifyOptions: {\n algorithms: [...algorithms] as jwt.Algorithm[],\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.VerifyOptions[\"audience\"] }\n : {}),\n },\n keysByKeyId,\n fallbackKeys,\n purpose,\n ...(options.jwtOptions.onUnknownKeyId\n ? { onUnknownKeyId: options.jwtOptions.onUnknownKeyId }\n : {}),\n };\n}\n\nfunction readTokenHeader(token: string): { keyId?: string; algorithm?: string } {\n const decoded = jwt.decode(token, { complete: true });\n if (!decoded || typeof decoded === \"string\" || !decoded.header) return {};\n const header = decoded.header as { kid?: unknown; alg?: unknown };\n return {\n ...(typeof header.kid === \"string\" && header.kid !== \"\" ? { keyId: header.kid } : {}),\n ...(typeof header.alg === \"string\" ? { algorithm: header.alg } : {}),\n };\n}\n\n/**\n * Chiavi da provare per un token.\n *\n * Un `kid` noto seleziona una sola chiave: non si ricade sulle altre, così un\n * token con intestazione manomessa non viene verificato per tentativi.\n */\nexport function resolveAccessiVerificationKeys(\n config: AccessiJwtVerificationConfig,\n token: string,\n): readonly jwt.Secret[] {\n const header = readTokenHeader(token);\n // Con HMAC il kid non partecipa alla crittografia: un kid cambiato non deve\n // invalidare token firmati con lo stesso segreto.\n const hmacOnly = (config.verifyOptions.algorithms ?? []).every((algorithm) =>\n String(algorithm).startsWith(\"HS\"),\n );\n if (!header.keyId || hmacOnly) return config.fallbackKeys;\n\n const key = config.keysByKeyId.get(header.keyId);\n if (key) return [key];\n\n config.onUnknownKeyId?.({\n keyId: header.keyId,\n ...(header.algorithm ? { algorithm: header.algorithm } : {}),\n purpose: config.purpose,\n });\n return [];\n}\n\n/**\n * Verifica un JWT Accessi con la chiave corretta (corrente o precedente).\n *\n * Lancia l'errore di `jsonwebtoken` quando nessuna chiave candidata verifica il\n * token; per un `kid` sconosciuto lancia un errore dedicato dopo aver emesso\n * l'evento di alert.\n */\nexport function verifyAccessiJwt(token: string, config: AccessiJwtVerificationConfig): unknown {\n const keys = resolveAccessiVerificationKeys(config, token);\n if (keys.length === 0) {\n const { keyId } = readTokenHeader(token);\n throw new Error(`Chiave JWT sconosciuta per kid \"${keyId ?? \"assente\"}\".`);\n }\n\n let lastError: unknown;\n for (const key of keys) {\n try {\n return jwt.verify(token, key, config.verifyOptions);\n } catch (error) {\n lastError = error;\n }\n }\n\n throw lastError instanceof Error ? lastError : new Error(\"Token JWT non valido.\");\n}\n\nexport function getAccessiAccessTokenSigningConfig(options: AccessiOptions) {\n return getTokenSigningConfig(options, \"access\");\n}\n\nexport function getAccessiAccessTokenVerificationConfig(options: AccessiOptions) {\n return getTokenVerificationConfig(options, \"access\");\n}\n\nexport function getAccessiRefreshTokenSigningConfig(options: AccessiOptions) {\n return getTokenSigningConfig(options, \"refresh\");\n}\n\nexport function getAccessiRefreshTokenVerificationConfig(options: AccessiOptions) {\n return getTokenVerificationConfig(options, \"refresh\");\n}\n"]}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateAccessiOptions = validateAccessiOptions;
4
4
  exports.assertValidAccessiOptions = assertValidAccessiOptions;
5
+ const crypto_1 = require("crypto");
5
6
  function isHttpUrl(value) {
6
7
  if (!value)
7
8
  return false;
@@ -24,7 +25,7 @@ function positive(value) {
24
25
  * comportamento storico.
25
26
  */
26
27
  function validateAccessiOptions(options) {
27
- var _a, _b, _c, _d;
28
+ var _a, _b, _c, _d, _e;
28
29
  const errors = [];
29
30
  const warnings = [];
30
31
  const addError = (code, message) => errors.push({ code, message });
@@ -63,6 +64,32 @@ function validateAccessiOptions(options) {
63
64
  addWarning('ACCESSI_JWKS_PATH_DEFAULT', 'jwksPath non configurato: verra usato accessi/.well-known/jwks.json.');
64
65
  }
65
66
  }
67
+ const previousKeys = (_b = jwt === null || jwt === void 0 ? void 0 : jwt.previousPublicKeys) !== null && _b !== void 0 ? _b : [];
68
+ if (previousKeys.length > 0 && algorithm.startsWith('HS')) {
69
+ addWarning('ACCESSI_JWT_PREVIOUS_KEYS_WITH_HMAC', 'previousPublicKeys e configurato ma algorithm e HS*: le chiavi verranno ignorate.');
70
+ }
71
+ if (!algorithm.startsWith('HS') && previousKeys.length > 0) {
72
+ const knownKeyIds = new Set();
73
+ if (jwt === null || jwt === void 0 ? void 0 : jwt.keyId)
74
+ knownKeyIds.add(jwt.keyId);
75
+ previousKeys.forEach((previous, index) => {
76
+ try {
77
+ (0, crypto_1.createPublicKey)(previous.publicKey);
78
+ }
79
+ catch (_a) {
80
+ addError('ACCESSI_JWT_PREVIOUS_KEY_INVALID', `previousPublicKeys[${index}] non e una chiave pubblica PEM valida.`);
81
+ }
82
+ if (!previous.keyId) {
83
+ addWarning('ACCESSI_JWT_PREVIOUS_KEY_ID_MISSING', `previousPublicKeys[${index}] non ha keyId: verra usato il fingerprint della chiave.`);
84
+ return;
85
+ }
86
+ if (knownKeyIds.has(previous.keyId)) {
87
+ addWarning('ACCESSI_JWT_PREVIOUS_KEY_DUPLICATE_KID', `previousPublicKeys[${index}].keyId "${previous.keyId}" e duplicato: vince la chiave corrente.`);
88
+ return;
89
+ }
90
+ knownKeyIds.add(previous.keyId);
91
+ });
92
+ }
66
93
  if (!(jwt === null || jwt === void 0 ? void 0 : jwt.issuer)) {
67
94
  addWarning('ACCESSI_JWT_ISSUER_MISSING', 'jwtOptions.issuer e consigliato in multi-servizio.');
68
95
  }
@@ -84,10 +111,10 @@ function validateAccessiOptions(options) {
84
111
  if ((options === null || options === void 0 ? void 0 : options.publicBaseUrl) && !isHttpUrl(options.publicBaseUrl)) {
85
112
  addWarning('ACCESSI_PUBLIC_BASE_URL_INVALID', 'publicBaseUrl dovrebbe essere un URL http/https.');
86
113
  }
87
- if (((_b = options === null || options === void 0 ? void 0 : options.publicAuthRateLimit) === null || _b === void 0 ? void 0 : _b.login) && !positive(options.publicAuthRateLimit.login.windowMs)) {
114
+ if (((_c = options === null || options === void 0 ? void 0 : options.publicAuthRateLimit) === null || _c === void 0 ? void 0 : _c.login) && !positive(options.publicAuthRateLimit.login.windowMs)) {
88
115
  addError('ACCESSI_RATE_LIMIT_WINDOW_INVALID', 'publicAuthRateLimit.login.windowMs deve essere positivo.');
89
116
  }
90
- if (((_c = options === null || options === void 0 ? void 0 : options.publicAuthRateLimit) === null || _c === void 0 ? void 0 : _c.login) && !positive(options.publicAuthRateLimit.login.maxAttempts)) {
117
+ if (((_d = options === null || options === void 0 ? void 0 : options.publicAuthRateLimit) === null || _d === void 0 ? void 0 : _d.login) && !positive(options.publicAuthRateLimit.login.maxAttempts)) {
91
118
  addError('ACCESSI_RATE_LIMIT_ATTEMPTS_INVALID', 'publicAuthRateLimit.login.maxAttempts deve essere positivo.');
92
119
  }
93
120
  if (options === null || options === void 0 ? void 0 : options.edgeRateLimit) {
@@ -98,7 +125,7 @@ function validateAccessiOptions(options) {
98
125
  addError('ACCESSI_EDGE_RATE_LIMIT_ATTEMPTS_INVALID', 'edgeRateLimit.maxAttempts deve essere positivo.');
99
126
  }
100
127
  }
101
- if (((_d = options === null || options === void 0 ? void 0 : options.consoleEntry) === null || _d === void 0 ? void 0 : _d.ttlMs) !== undefined && !positive(options.consoleEntry.ttlMs)) {
128
+ if (((_e = options === null || options === void 0 ? void 0 : options.consoleEntry) === null || _e === void 0 ? void 0 : _e.ttlMs) !== undefined && !positive(options.consoleEntry.ttlMs)) {
102
129
  addError('ACCESSI_CONSOLE_ENTRY_TTL_INVALID', 'consoleEntry.ttlMs deve essere positivo.');
103
130
  }
104
131
  return { errors, warnings };
@@ -1 +1 @@
1
- {"version":3,"file":"validateOptions.js","sourceRoot":"","sources":["../../../src/accessi-module/security/validateOptions.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\n\nexport interface AccessiOptionsValidationIssue {\n code: string;\n message: string;\n}\n\nexport interface AccessiOptionsValidationResult {\n errors: AccessiOptionsValidationIssue[];\n warnings: AccessiOptionsValidationIssue[];\n}\n\nfunction isHttpUrl(value: string | undefined): boolean {\n if (!value) return false;\n try {\n const url = new URL(value);\n return url.protocol === 'http:' || url.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\nfunction positive(value: number | undefined): boolean {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n/**\n * Validazione fail-fast della configurazione Accessi.\n *\n * `createAccessiService` usa `assertValidAccessiOptions`; i bootstrap esistenti\n * possono usare `validateAccessiOptions` per loggare warning senza rompere il\n * comportamento storico.\n */\nexport function validateAccessiOptions(options: AccessiOptions): AccessiOptionsValidationResult {\n const errors: AccessiOptionsValidationIssue[] = [];\n const warnings: AccessiOptionsValidationIssue[] = [];\n\n const addError = (code: string, message: string) => errors.push({ code, message });\n const addWarning = (code: string, message: string) => warnings.push({ code, message });\n\n if (!options?.databaseOptions) {\n addError('ACCESSI_DB_OPTIONS_MISSING', 'databaseOptions e obbligatorio.');\n }\n\n const jwt = options?.jwtOptions;\n const secret = jwt?.secret || process.env.ACC_JWT_SECRET;\n if (!secret) {\n addError('ACCESSI_JWT_SECRET_MISSING', 'jwtOptions.secret e obbligatorio.');\n } else if (secret.length < 32) {\n addWarning('ACCESSI_JWT_SECRET_WEAK', 'jwtOptions.secret dovrebbe avere almeno 32 caratteri.');\n }\n\n if (!jwt?.expiresIn) {\n addError('ACCESSI_JWT_EXPIRES_MISSING', 'jwtOptions.expiresIn e obbligatorio.');\n }\n\n const algorithm = jwt?.algorithm ?? 'HS256';\n if (algorithm.startsWith('HS')) {\n if (jwt?.privateKey || jwt?.publicKey) {\n addWarning(\n 'ACCESSI_JWT_ASYMMETRIC_KEYS_WITH_HMAC',\n 'Sono configurate chiavi asimmetriche ma algorithm e HS256: verranno ignorate.',\n );\n }\n } else {\n if (!jwt?.privateKey) {\n addError('ACCESSI_JWT_PRIVATE_KEY_MISSING', `jwtOptions.privateKey e obbligatoria per ${algorithm}.`);\n }\n if (!jwt?.publicKey && !jwt?.privateKey) {\n addError('ACCESSI_JWT_PUBLIC_KEY_MISSING', `jwtOptions.publicKey e obbligatoria per ${algorithm}.`);\n }\n if (!jwt?.keyId) {\n addWarning('ACCESSI_JWT_KEY_ID_MISSING', 'jwtOptions.keyId e consigliato per la rotazione JWKS.');\n }\n if (!jwt?.jwksPath) {\n addWarning(\n 'ACCESSI_JWKS_PATH_DEFAULT',\n 'jwksPath non configurato: verra usato accessi/.well-known/jwks.json.',\n );\n }\n }\n\n if (!jwt?.issuer) {\n addWarning('ACCESSI_JWT_ISSUER_MISSING', 'jwtOptions.issuer e consigliato in multi-servizio.');\n }\n if (!jwt?.audience) {\n addWarning('ACCESSI_JWT_AUDIENCE_MISSING', 'jwtOptions.audience e consigliato in multi-servizio.');\n }\n\n if (!options?.encryptionKey) {\n addError('ACCESSI_ENCRYPTION_KEY_MISSING', 'encryptionKey e obbligatoria.');\n } else if (options.encryptionKey.length < 16) {\n addWarning('ACCESSI_ENCRYPTION_KEY_WEAK', 'encryptionKey dovrebbe avere almeno 16 caratteri.');\n }\n\n if (!isHttpUrl(options?.confirmationEmailUrl)) {\n addWarning('ACCESSI_CONFIRMATION_URL_INVALID', 'confirmationEmailUrl dovrebbe essere un URL http/https.');\n }\n if (!isHttpUrl(options?.confirmationEmailReturnUrl)) {\n addWarning('ACCESSI_CONFIRMATION_RETURN_URL_INVALID', 'confirmationEmailReturnUrl dovrebbe essere un URL http/https.');\n }\n if (options?.publicBaseUrl && !isHttpUrl(options.publicBaseUrl)) {\n addWarning('ACCESSI_PUBLIC_BASE_URL_INVALID', 'publicBaseUrl dovrebbe essere un URL http/https.');\n }\n\n if (options?.publicAuthRateLimit?.login && !positive(options.publicAuthRateLimit.login.windowMs)) {\n addError('ACCESSI_RATE_LIMIT_WINDOW_INVALID', 'publicAuthRateLimit.login.windowMs deve essere positivo.');\n }\n if (options?.publicAuthRateLimit?.login && !positive(options.publicAuthRateLimit.login.maxAttempts)) {\n addError('ACCESSI_RATE_LIMIT_ATTEMPTS_INVALID', 'publicAuthRateLimit.login.maxAttempts deve essere positivo.');\n }\n if (options?.edgeRateLimit) {\n if (options.edgeRateLimit.windowMs !== undefined && !positive(options.edgeRateLimit.windowMs)) {\n addError('ACCESSI_EDGE_RATE_LIMIT_WINDOW_INVALID', 'edgeRateLimit.windowMs deve essere positivo.');\n }\n if (options.edgeRateLimit.maxAttempts !== undefined && !positive(options.edgeRateLimit.maxAttempts)) {\n addError('ACCESSI_EDGE_RATE_LIMIT_ATTEMPTS_INVALID', 'edgeRateLimit.maxAttempts deve essere positivo.');\n }\n }\n if (options?.consoleEntry?.ttlMs !== undefined && !positive(options.consoleEntry.ttlMs)) {\n addError('ACCESSI_CONSOLE_ENTRY_TTL_INVALID', 'consoleEntry.ttlMs deve essere positivo.');\n }\n\n return { errors, warnings };\n}\n\n/** Lancia un errore aggregato se la configurazione non e valida. */\nexport function assertValidAccessiOptions(options: AccessiOptions): void {\n const result = validateAccessiOptions(options);\n if (result.errors.length === 0) return;\n throw new Error(\n `Configurazione Accessi non valida:\\n${result.errors\n .map((issue) => `- ${issue.code}: ${issue.message}`)\n .join('\\n')}`,\n );\n}\n"]}
1
+ {"version":3,"file":"validateOptions.js","sourceRoot":"","sources":["../../../src/accessi-module/security/validateOptions.ts"],"names":[],"mappings":"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","sourcesContent":["import { createPublicKey } from 'crypto';\nimport type { AccessiOptions } from '../AccessiModule';\n\nexport interface AccessiOptionsValidationIssue {\n code: string;\n message: string;\n}\n\nexport interface AccessiOptionsValidationResult {\n errors: AccessiOptionsValidationIssue[];\n warnings: AccessiOptionsValidationIssue[];\n}\n\nfunction isHttpUrl(value: string | undefined): boolean {\n if (!value) return false;\n try {\n const url = new URL(value);\n return url.protocol === 'http:' || url.protocol === 'https:';\n } catch {\n return false;\n }\n}\n\nfunction positive(value: number | undefined): boolean {\n return typeof value === 'number' && Number.isFinite(value) && value > 0;\n}\n\n/**\n * Validazione fail-fast della configurazione Accessi.\n *\n * `createAccessiService` usa `assertValidAccessiOptions`; i bootstrap esistenti\n * possono usare `validateAccessiOptions` per loggare warning senza rompere il\n * comportamento storico.\n */\nexport function validateAccessiOptions(options: AccessiOptions): AccessiOptionsValidationResult {\n const errors: AccessiOptionsValidationIssue[] = [];\n const warnings: AccessiOptionsValidationIssue[] = [];\n\n const addError = (code: string, message: string) => errors.push({ code, message });\n const addWarning = (code: string, message: string) => warnings.push({ code, message });\n\n if (!options?.databaseOptions) {\n addError('ACCESSI_DB_OPTIONS_MISSING', 'databaseOptions e obbligatorio.');\n }\n\n const jwt = options?.jwtOptions;\n const secret = jwt?.secret || process.env.ACC_JWT_SECRET;\n if (!secret) {\n addError('ACCESSI_JWT_SECRET_MISSING', 'jwtOptions.secret e obbligatorio.');\n } else if (secret.length < 32) {\n addWarning('ACCESSI_JWT_SECRET_WEAK', 'jwtOptions.secret dovrebbe avere almeno 32 caratteri.');\n }\n\n if (!jwt?.expiresIn) {\n addError('ACCESSI_JWT_EXPIRES_MISSING', 'jwtOptions.expiresIn e obbligatorio.');\n }\n\n const algorithm = jwt?.algorithm ?? 'HS256';\n if (algorithm.startsWith('HS')) {\n if (jwt?.privateKey || jwt?.publicKey) {\n addWarning(\n 'ACCESSI_JWT_ASYMMETRIC_KEYS_WITH_HMAC',\n 'Sono configurate chiavi asimmetriche ma algorithm e HS256: verranno ignorate.',\n );\n }\n } else {\n if (!jwt?.privateKey) {\n addError('ACCESSI_JWT_PRIVATE_KEY_MISSING', `jwtOptions.privateKey e obbligatoria per ${algorithm}.`);\n }\n if (!jwt?.publicKey && !jwt?.privateKey) {\n addError('ACCESSI_JWT_PUBLIC_KEY_MISSING', `jwtOptions.publicKey e obbligatoria per ${algorithm}.`);\n }\n if (!jwt?.keyId) {\n addWarning('ACCESSI_JWT_KEY_ID_MISSING', 'jwtOptions.keyId e consigliato per la rotazione JWKS.');\n }\n if (!jwt?.jwksPath) {\n addWarning(\n 'ACCESSI_JWKS_PATH_DEFAULT',\n 'jwksPath non configurato: verra usato accessi/.well-known/jwks.json.',\n );\n }\n }\n\n const previousKeys = jwt?.previousPublicKeys ?? [];\n if (previousKeys.length > 0 && algorithm.startsWith('HS')) {\n addWarning(\n 'ACCESSI_JWT_PREVIOUS_KEYS_WITH_HMAC',\n 'previousPublicKeys e configurato ma algorithm e HS*: le chiavi verranno ignorate.',\n );\n }\n if (!algorithm.startsWith('HS') && previousKeys.length > 0) {\n const knownKeyIds = new Set<string>();\n if (jwt?.keyId) knownKeyIds.add(jwt.keyId);\n previousKeys.forEach((previous, index) => {\n try {\n createPublicKey(previous.publicKey);\n } catch {\n addError(\n 'ACCESSI_JWT_PREVIOUS_KEY_INVALID',\n `previousPublicKeys[${index}] non e una chiave pubblica PEM valida.`,\n );\n }\n if (!previous.keyId) {\n addWarning(\n 'ACCESSI_JWT_PREVIOUS_KEY_ID_MISSING',\n `previousPublicKeys[${index}] non ha keyId: verra usato il fingerprint della chiave.`,\n );\n return;\n }\n if (knownKeyIds.has(previous.keyId)) {\n addWarning(\n 'ACCESSI_JWT_PREVIOUS_KEY_DUPLICATE_KID',\n `previousPublicKeys[${index}].keyId \"${previous.keyId}\" e duplicato: vince la chiave corrente.`,\n );\n return;\n }\n knownKeyIds.add(previous.keyId);\n });\n }\n\n if (!jwt?.issuer) {\n addWarning('ACCESSI_JWT_ISSUER_MISSING', 'jwtOptions.issuer e consigliato in multi-servizio.');\n }\n if (!jwt?.audience) {\n addWarning('ACCESSI_JWT_AUDIENCE_MISSING', 'jwtOptions.audience e consigliato in multi-servizio.');\n }\n\n if (!options?.encryptionKey) {\n addError('ACCESSI_ENCRYPTION_KEY_MISSING', 'encryptionKey e obbligatoria.');\n } else if (options.encryptionKey.length < 16) {\n addWarning('ACCESSI_ENCRYPTION_KEY_WEAK', 'encryptionKey dovrebbe avere almeno 16 caratteri.');\n }\n\n if (!isHttpUrl(options?.confirmationEmailUrl)) {\n addWarning('ACCESSI_CONFIRMATION_URL_INVALID', 'confirmationEmailUrl dovrebbe essere un URL http/https.');\n }\n if (!isHttpUrl(options?.confirmationEmailReturnUrl)) {\n addWarning('ACCESSI_CONFIRMATION_RETURN_URL_INVALID', 'confirmationEmailReturnUrl dovrebbe essere un URL http/https.');\n }\n if (options?.publicBaseUrl && !isHttpUrl(options.publicBaseUrl)) {\n addWarning('ACCESSI_PUBLIC_BASE_URL_INVALID', 'publicBaseUrl dovrebbe essere un URL http/https.');\n }\n\n if (options?.publicAuthRateLimit?.login && !positive(options.publicAuthRateLimit.login.windowMs)) {\n addError('ACCESSI_RATE_LIMIT_WINDOW_INVALID', 'publicAuthRateLimit.login.windowMs deve essere positivo.');\n }\n if (options?.publicAuthRateLimit?.login && !positive(options.publicAuthRateLimit.login.maxAttempts)) {\n addError('ACCESSI_RATE_LIMIT_ATTEMPTS_INVALID', 'publicAuthRateLimit.login.maxAttempts deve essere positivo.');\n }\n if (options?.edgeRateLimit) {\n if (options.edgeRateLimit.windowMs !== undefined && !positive(options.edgeRateLimit.windowMs)) {\n addError('ACCESSI_EDGE_RATE_LIMIT_WINDOW_INVALID', 'edgeRateLimit.windowMs deve essere positivo.');\n }\n if (options.edgeRateLimit.maxAttempts !== undefined && !positive(options.edgeRateLimit.maxAttempts)) {\n addError('ACCESSI_EDGE_RATE_LIMIT_ATTEMPTS_INVALID', 'edgeRateLimit.maxAttempts deve essere positivo.');\n }\n }\n if (options?.consoleEntry?.ttlMs !== undefined && !positive(options.consoleEntry.ttlMs)) {\n addError('ACCESSI_CONSOLE_ENTRY_TTL_INVALID', 'consoleEntry.ttlMs deve essere positivo.');\n }\n\n return { errors, warnings };\n}\n\n/** Lancia un errore aggregato se la configurazione non e valida. */\nexport function assertValidAccessiOptions(options: AccessiOptions): void {\n const result = validateAccessiOptions(options);\n if (result.errors.length === 0) return;\n throw new Error(\n `Configurazione Accessi non valida:\\n${result.errors\n .map((issue) => `- ${issue.code}: ${issue.message}`)\n .join('\\n')}`,\n );\n}\n"]}
@@ -0,0 +1,143 @@
1
+ # Rotazione delle chiavi JWT
2
+
3
+ Accessi firma gli access token (e i refresh token, se abilitati) con una chiave
4
+ corrente e pubblica le chiavi pubbliche nella JWKS. La rotazione non deve
5
+ invalidare i token gia emessi: durante il rollout la JWKS contiene la chiave
6
+ nuova e quelle precedenti, e la verifica accetta entrambe.
7
+
8
+ Riferimenti:
9
+
10
+ - `jwtOptions.keyId`, `jwtOptions.previousPublicKeys`, `jwtOptions.onUnknownKeyId`
11
+ - `generate-accessi-jwt-keys.cjs` (prima generazione)
12
+ - `rotate-accessi-jwt-keys.cjs` (rotazione)
13
+
14
+ ## Modello
15
+
16
+ | Elemento | Ruolo |
17
+ | --- | --- |
18
+ | `jwtOptions.privateKey` / `publicKey` | chiave corrente: firma i nuovi token |
19
+ | `jwtOptions.keyId` | `kid` pubblicato nell'header e nella JWKS |
20
+ | `jwtOptions.previousPublicKeys` | chiavi pubbliche ancora accettate in verifica |
21
+ | `ACCESSI_JWT_PREVIOUS_PUBLIC_KEYS` | le stesse chiavi lette da env, in JSON |
22
+ | `jwtOptions.onUnknownKeyId` | alert quando un token usa un `kid` sconosciuto |
23
+
24
+ Senza `keyId`, Accessi usa il fingerprint SHA-256 della chiave pubblica
25
+ (`base64url`, primi 16 caratteri). Lo stesso valore viene usato dallo script di
26
+ rotazione e dal resource server.
27
+
28
+ ## Configurazione da env
29
+
30
+ ```bash
31
+ ACCESSI_JWT_ALGORITHM=RS256
32
+ ACCESSI_JWT_PRIVATE_KEY="$(cat jwt-private.pem)"
33
+ ACCESSI_JWT_PUBLIC_KEY="$(cat jwt-public.pem)"
34
+ ACCESSI_JWT_KEY_ID=2026-10
35
+ ACCESSI_JWT_PREVIOUS_PUBLIC_KEYS='[
36
+ { "keyId": "2026-04", "publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----" }
37
+ ]'
38
+ ```
39
+
40
+ Forme accettate nell'array:
41
+
42
+ - stringa PEM;
43
+ - oggetto `{ "publicKey": "<PEM>", "keyId": "<kid>", "algorithm": "RS256" }`.
44
+
45
+ `keyId` e `algorithm` sono opzionali. Le sequenze `\n` letterali vengono
46
+ convertite in newline, cosi il valore resta su una riga nel secret manager. Un
47
+ JSON malformato o una chiave non PEM bloccano il boot (`ACCESSI_JWT_PREVIOUS_KEY_INVALID`),
48
+ invece di lasciare token non verificabili in produzione.
49
+
50
+ Con `HS256` le chiavi precedenti sono ignorate: la rotazione HMAC richiede di
51
+ cambiare `jwtOptions.secret`, che invalida tutti i token. Per una rotazione
52
+ senza interruzioni usare RS256 o ES256.
53
+
54
+ ## Procedura di rotazione
55
+
56
+ 1. **Genera la nuova coppia** con lo script, passando la chiave corrente come
57
+ precedente:
58
+
59
+ ```bash
60
+ npm run accessi:rotate-jwt-keys -- \
61
+ --out ./keys-2026-10 \
62
+ --current-public-key-file ./keys-2026-04/jwt-public.pem \
63
+ --current-key-id 2026-04
64
+ ```
65
+
66
+ Lo script scrive `jwt-private.pem`, `jwt-public.pem`, `jwks.json` e
67
+ `rotation.json` (contiene i valori `ACCESSI_*` da mettere nel secret manager).
68
+ Con `--previous-file` o `--previous` importa le chiavi precedenti gia in uso;
69
+ `--retain N` limita quante mantenerne (default 3).
70
+
71
+ 2. **Consumatori con JWKS**: nessuna azione. Al primo token con `kid` nuovo il
72
+ resource server forza un refresh della JWKS (throttle default 5 s,
73
+ `jwt.jwks.minRefreshIntervalMs`) e continua a verificare i token vecchi con le
74
+ chiavi precedenti.
75
+
76
+ 3. **Consumatori con `publicKey` statica**: distribuisci prima la nuova chiave
77
+ pubblica (o passa a `jwt.jwks`), poi cambia la chiave di firma. Chi usa la
78
+ sola chiave statica non puo conoscere in anticipo la chiave nuova.
79
+
80
+ 4. **Deploy di Accessi** con i nuovi valori. La JWKS pubblica subito nuova
81
+ chiave + precedenti, quindi i token emessi prima restano validi.
82
+
83
+ 5. **Verifica il rollout**:
84
+
85
+ ```bash
86
+ node scripts/rotate-accessi-jwt-keys.cjs --out ./keys-2026-10 \
87
+ --check https://accessi.example.com/api/accessi/.well-known/jwks.json --only-check
88
+ ```
89
+
90
+ Il comando fallisce se un `kid` del documento non e pubblicato. Esegui la
91
+ verifica su ogni istanza dietro il load balancer (o sull'endpoint pubblico)
92
+ prima di considerare conclusa la rotazione.
93
+
94
+ 6. **Rimuovi le chiavi precedenti** solo dopo la durata massima dei token:
95
+ `max(jwtOptions.expiresIn, jwtOptions.refreshExpiresIn)`. Finche restano in
96
+ `previousPublicKeys` i refresh token firmati con la vecchia chiave continuano
97
+ a funzionare.
98
+
99
+ ## Rollback
100
+
101
+ Se il deploy della nuova chiave va storto, ripristina i valori precedenti di
102
+ `privateKey`, `publicKey`, `keyId` e `previousPublicKeys`: i token firmati con la
103
+ chiave nuova smettono di essere validi, quelli vecchi tornano a esserlo. Per
104
+ questo la vecchia chiave privata va conservata nel secret manager finche la
105
+ rotazione non e conclusa.
106
+
107
+ ## Alert su `kid` sconosciuto
108
+
109
+ Un `kid` assente da chiave corrente e precedenti indica una rotazione incompleta
110
+ o un token manomesso. Collega l'evento al tuo sistema di allerta:
111
+
112
+ ```ts
113
+ const accessiOptions: AccessiOptions = {
114
+ // ...
115
+ jwtOptions: {
116
+ // ...
117
+ onUnknownKeyId: ({ keyId, algorithm, purpose }) => {
118
+ logger.warn({ keyId, algorithm, purpose }, 'JWT kid sconosciuto');
119
+ metrics.increment('accessi_jwt_unknown_kid');
120
+ },
121
+ },
122
+ };
123
+
124
+ // Resource server con JWKS remoto
125
+ const jwtOptions: AccessiResourceServerJwtOptions = {
126
+ jwks: { url: 'https://accessi.example.com/api/accessi/.well-known/jwks.json' },
127
+ onUnknownKeyId: ({ kid, algorithm, source }) => {
128
+ logger.warn({ kid, algorithm, source }, 'kid non presente nella JWKS');
129
+ },
130
+ };
131
+ ```
132
+
133
+ L'evento non cambia l'esito dell'autenticazione: l'eccezione di un logger o di
134
+ un webhook non trasforma un `401` in un `500`.
135
+
136
+ ## Cache e tempi
137
+
138
+ - La JWKS Accessi risponde con `Cache-Control: public, max-age=300`.
139
+ - Il resource server usa `cacheTtlMs` (default 300000) e, su `kid` sconosciuto,
140
+ un solo refresh forzato non piu frequente di `minRefreshIntervalMs` (default
141
+ 5000). Un flood di `kid` inventati non puo martellare la JWKS.
142
+ - Con CDN o reverse proxy davanti alla JWKS, non impostare un TTL superiore a
143
+ `cacheTtlMs`: la rotazione resterebbe invisibile fino alla scadenza della cache.
@@ -0,0 +1,99 @@
1
+ # IP reali dietro proxy
2
+
3
+ Rate limit, audit e revoca usano `req.ip`. Se il servizio sta dietro un load balancer o un reverse
4
+ proxy, `req.ip` e l'indirizzo del proxy finche `http.trustProxy` non e configurato correttamente:
5
+
6
+ - tutti i client finiscono nello stesso bucket → il rate limit blocca utenti legittimi;
7
+ - oppure un header non fidato diventa la chiave → il rate limit si aggira cambiando
8
+ `X-Forwarded-For` a ogni richiesta;
9
+ - i log di audit riportano l'IP del proxy invece del client.
10
+
11
+ `http.trustProxy` viene applicato sia all'host sia al sotto-app Accessi (`app.set('trust proxy', …)`)
12
+ e accetta la stessa sintassi di Express: `false`, numero di hop, `true`, `'loopback'`, CIDR.
13
+
14
+ ## Valore consigliato
15
+
16
+ | Deploy | `http.trustProxy` | Note |
17
+ | --- | --- | --- |
18
+ | Servizio esposto direttamente | `false` (default) | `X-Forwarded-For` viene ignorato: nessun bypass via header |
19
+ | Un proxy davanti (Nginx, Traefik, ALB) | `1` | Il proxy deve **accodare** l'IP reale in `X-Forwarded-For` |
20
+ | Due proxy (Cloudflare + Nginx, CDN + ingress) | `2` | Contare solo i proxy che il servizio vede davvero |
21
+ | Solo proxy locali | `'loopback'` | Fidarsi di `127.0.0.1`/`::1`, non di altre reti |
22
+ | Rete privata dedicata ai proxy | `'loopback, 10.0.0.0/8'` | Elencare le CIDR dei proxy, non usare `true` |
23
+ | `trustProxy: true` | **sconsigliato** | Fidarsi di tutti gli hop rende scegliibile dal client il valore piu a sinistra |
24
+
25
+ `npm run test:proxy-ip` verifica questi comportamenti su Express: senza trust proxy l'header non
26
+ cambia `req.ip`; con `1` il client reale e l'ultimo hop; un header anteposto dal client non sposta il
27
+ bucket; con `true` il valore piu a sinistra (falsificabile) vince.
28
+
29
+ ## Regola d'oro: il proxy deve accodare, non fidarsi
30
+
31
+ `X-Forwarded-For` e una lista `client, proxy1, proxy2`. Il client puo inviare il proprio header con
32
+ qualsiasi valore: il proxy deve **accodare** (`$proxy_add_x_forwarded_for`) o **sovrascrivere**
33
+ (`$remote_addr`). Con `trustProxy` numerico Express parte da destra e si ferma dopo N hop fidati,
34
+ quindi i valori anteposti dal client vengono ignorati.
35
+
36
+ Non usare `X-Real-IP` da solo: se il servizio e raggiungibile direttamente, e falsificabile quanto
37
+ `X-Forwarded-For`.
38
+
39
+ ## Nginx
40
+
41
+ ```nginx
42
+ location /api/accessi/ {
43
+ proxy_pass http://accessi_service;
44
+
45
+ # Un solo proxy: sovrascrivere l'header e evitare valori del client.
46
+ proxy_set_header X-Forwarded-For $remote_addr;
47
+ proxy_set_header X-Forwarded-Proto $scheme;
48
+ proxy_set_header Host $host;
49
+ }
50
+ ```
51
+
52
+ Con piu proxy davanti a Nginx usare `$proxy_add_x_forwarded_for` e impostare `trustProxy` al numero
53
+ totale di hop fidati.
54
+
55
+ ## Traefik
56
+
57
+ Traefik aggiunge `X-Forwarded-For` per impostazione predefinita. Con un solo Traefik davanti al
58
+ servizio usare `trustProxy: 1`. Se Traefik e dietro un altro ingress, contare anche quello.
59
+
60
+ ## Cloudflare
61
+
62
+ Cloudflare invia `CF-Connecting-IP` e accoda `X-Forwarded-For`. Due opzioni sicure:
63
+
64
+ 1. il servizio e raggiungibile **solo** da Cloudflare (firewall/authenticated origin pulls):
65
+ `trustProxy: 1`, perche l'unico hop e Cloudflare;
66
+ 2. c'e anche un ingress/Nginx tra Cloudflare e il servizio: `trustProxy: 2`.
67
+
68
+ Non impostare `trustProxy: true`: chi raggiunge l'origin direttamente potrebbe falsificare l'IP.
69
+ Se serve `CF-Connecting-IP`, va letto solo dopo aver verificato che la richiesta arrivi da un IP
70
+ Cloudflare.
71
+
72
+ ## Configurazione
73
+
74
+ ```ts
75
+ export const accessiOptions: AccessiOptions = {
76
+ // ...
77
+ http: {
78
+ trustProxy: 1, // un solo proxy davanti
79
+ },
80
+ edgeRateLimit: {
81
+ enabled: true,
82
+ windowMs: 60_000,
83
+ maxAttempts: 300,
84
+ },
85
+ };
86
+ ```
87
+
88
+ ## Verifica in produzione
89
+
90
+ 1. Da un client esterno, chiamare un endpoint pubblico con un header inventato:
91
+
92
+ ```bash
93
+ curl -H 'X-Forwarded-For: 6.6.6.6' https://accessi.example.com/api/accessi/auth/login
94
+ ```
95
+
96
+ 2. Controllare che l'audit (`accessi_rate_limited`, `edge_rate_limited`) riporti l'IP reale del
97
+ client e non `6.6.6.6`.
98
+ 3. Superare il limite da due IP diversi: se il secondo IP e bloccato subito, il bucket e condiviso e
99
+ `trustProxy` e troppo basso. Se cambiando header si sblocca, `trustProxy` e troppo alto.