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.
- package/README.MD +2 -0
- package/dist/accessi/resource-server/token.js +62 -18
- package/dist/accessi/resource-server/token.js.map +1 -1
- package/dist/accessi/resource-server/types.d.ts +22 -0
- package/dist/accessi/resource-server/types.js.map +1 -1
- package/dist/accessi/server/index.d.ts +1 -1
- package/dist/accessi/server/index.js +2 -1
- package/dist/accessi/server/index.js.map +1 -1
- package/dist/accessi/server/standalone.d.ts +2 -4
- package/dist/accessi/server/standalone.js +4 -79
- package/dist/accessi/server/standalone.js.map +1 -1
- package/dist/accessi-module/AccessiModule.d.ts +12 -3
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/console.js +2 -1
- package/dist/accessi-module/Console/wiki.js +2 -1
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Services/AuthService/AuthService.js +2 -2
- package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.d.ts +9 -1
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js +53 -1
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js.map +1 -1
- package/dist/accessi-module/health/nest.d.ts +31 -0
- package/dist/accessi-module/health/nest.js +89 -0
- package/dist/accessi-module/health/nest.js.map +1 -0
- package/dist/accessi-module/health/readiness.d.ts +53 -0
- package/dist/accessi-module/health/readiness.js +110 -0
- package/dist/accessi-module/health/readiness.js.map +1 -0
- package/dist/accessi-module/index.d.ts +6 -0
- package/dist/accessi-module/index.js +16 -1
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/jwt/jwt.strategy.js +1 -35
- package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
- package/dist/accessi-module/middleware/authenticateGen.js +1 -35
- package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
- package/dist/accessi-module/security/jwtTokenOptions.d.ts +44 -9
- package/dist/accessi-module/security/jwtTokenOptions.js +118 -7
- package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
- package/dist/accessi-module/security/validateOptions.js +31 -4
- package/dist/accessi-module/security/validateOptions.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.75.tgz +0 -0
- package/docs/jwt-key-rotation.md +143 -0
- package/docs/proxy-ip.md +99 -0
- package/docs/standalone-service.md +25 -0
- package/package.json +5 -3
- package/dist/emilsoftware-utilities-2.0.0-dev.73.tgz +0 -0
|
@@ -1,22 +1,57 @@
|
|
|
1
|
-
import
|
|
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
|
-
/**
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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":"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","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 (((
|
|
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 (((
|
|
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 (((
|
|
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"]}
|
|
Binary file
|
|
@@ -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.
|
package/docs/proxy-ip.md
ADDED
|
@@ -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.
|