emilsoftware-utilities 2.0.0-dev.78 → 2.0.0-dev.79
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/dist/accessi/client/generated/admin.d.ts +21 -1
- package/dist/accessi/client/generated/admin.js +35 -1
- package/dist/accessi/client/generated/admin.js.map +1 -1
- package/dist/accessi-module/AccessiModule.d.ts +3 -0
- package/dist/accessi-module/AccessiModule.js +4 -0
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/console.js +1 -0
- package/dist/accessi-module/Console/generated/emilsoftware-utilities.d.ts +21 -1
- package/dist/accessi-module/Console/generated/emilsoftware-utilities.js +35 -1
- package/dist/accessi-module/Console/generated/emilsoftware-utilities.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/exportAuditOrder.d.ts +11 -0
- package/dist/accessi-module/Console/generated/model/exportAuditOrder.js +15 -0
- package/dist/accessi-module/Console/generated/model/exportAuditOrder.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/exportAuditParams.d.ts +37 -0
- package/dist/accessi-module/Console/generated/model/exportAuditParams.js +3 -0
- package/dist/accessi-module/Console/generated/model/exportAuditParams.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/index.d.ts +4 -0
- package/dist/accessi-module/Console/generated/model/index.js +4 -0
- package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/queryAuditOrder.d.ts +11 -0
- package/dist/accessi-module/Console/generated/model/queryAuditOrder.js +15 -0
- package/dist/accessi-module/Console/generated/model/queryAuditOrder.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/queryAuditParams.d.ts +37 -0
- package/dist/accessi-module/Console/generated/model/queryAuditParams.js +3 -0
- package/dist/accessi-module/Console/generated/model/queryAuditParams.js.map +1 -0
- package/dist/accessi-module/Console/wiki.js +1 -0
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Controllers/AccessiAuditController.d.ts +16 -0
- package/dist/accessi-module/Controllers/AccessiAuditController.js +155 -0
- package/dist/accessi-module/Controllers/AccessiAuditController.js.map +1 -0
- package/dist/accessi-module/Controllers/AuthController.js +15 -0
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Controllers/ConsoleEntryController.js +6 -0
- package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
- package/dist/accessi-module/Dtos/AuditDtos.d.ts +10 -0
- package/dist/accessi-module/Dtos/AuditDtos.js +68 -0
- package/dist/accessi-module/Dtos/AuditDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/index.d.ts +1 -0
- package/dist/accessi-module/Dtos/index.js +1 -0
- package/dist/accessi-module/Dtos/index.js.map +1 -1
- package/dist/accessi-module/index.d.ts +5 -1
- package/dist/accessi-module/index.js +14 -3
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/security/audit.d.ts +68 -3
- package/dist/accessi-module/security/audit.js +177 -4
- package/dist/accessi-module/security/audit.js.map +1 -1
- package/dist/accessi-module/security/auditSinks.d.ts +55 -0
- package/dist/accessi-module/security/auditSinks.js +228 -0
- package/dist/accessi-module/security/auditSinks.js.map +1 -0
- package/dist/accessi-module/security/jwtTokenOptions.js +2 -0
- package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.js +7 -0
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
- package/dist/accessi-module/security/sessionRevocation.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.79.tgz +0 -0
- package/docs/audit.md +136 -0
- package/package.json +3 -3
- package/dist/emilsoftware-utilities-2.0.0-dev.78.tgz +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { AccessiAuditQueryableSink, AccessiAuditRecord, AccessiAuditSink } from './audit';
|
|
2
|
+
import type { AccessiRedisEvalClient } from '../observability/redisMetrics';
|
|
3
|
+
/** Opzioni del sink in memoria. */
|
|
4
|
+
export interface MemoryAccessiAuditSinkOptions {
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Numero massimo di record conservati. Default: 10000. */
|
|
7
|
+
maxRecords?: number;
|
|
8
|
+
/** Retention in millisecondi. Default: nessuna scadenza. */
|
|
9
|
+
retentionMs?: number;
|
|
10
|
+
/** Orologio iniettabile per i test. */
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
export interface MemoryAccessiAuditSink extends AccessiAuditQueryableSink {
|
|
14
|
+
all(): AccessiAuditRecord[];
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Sink in memoria con query e retention.
|
|
19
|
+
*
|
|
20
|
+
* Adatto a sviluppo, test e singola istanza. Per piu istanze usare
|
|
21
|
+
* `createRedisAccessiAuditSink` o un sink esterno.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createMemoryAccessiAuditSink(options?: MemoryAccessiAuditSinkOptions): MemoryAccessiAuditSink;
|
|
24
|
+
/** Sink HTTP: inoltra ogni record a un endpoint esterno (SIEM, collector). */
|
|
25
|
+
export interface HttpAccessiAuditSinkOptions {
|
|
26
|
+
name?: string;
|
|
27
|
+
url: string;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
fetch?: typeof fetch;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function createHttpAccessiAuditSink(options: HttpAccessiAuditSinkOptions): AccessiAuditSink;
|
|
33
|
+
/**
|
|
34
|
+
* Adattatore per database, OpenTelemetry o qualunque destinazione custom.
|
|
35
|
+
*
|
|
36
|
+
* La callback riceve un record gia redatto e serializzabile.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCallbackAccessiAuditSink(write: (record: AccessiAuditRecord) => void | Promise<void>, name?: string): AccessiAuditSink;
|
|
39
|
+
export interface RedisAccessiAuditSinkOptions {
|
|
40
|
+
client: AccessiRedisEvalClient;
|
|
41
|
+
name?: string;
|
|
42
|
+
/** Default: `accessi:audit:`. */
|
|
43
|
+
keyPrefix?: string;
|
|
44
|
+
/** Numero massimo di record. Default: 10000. */
|
|
45
|
+
maxRecords?: number;
|
|
46
|
+
/** Retention Redis in millisecondi. Default: 30 giorni. */
|
|
47
|
+
retentionMs?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Sink Redis persistente, condiviso tra istanze e interrogabile.
|
|
51
|
+
*
|
|
52
|
+
* Usa una lista con `LTRIM` per limitare i record e `PEXPIRE` per la retention.
|
|
53
|
+
* Le query leggono la lista e filtrano lato applicazione.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createRedisAccessiAuditSink(options: RedisAccessiAuditSinkOptions): AccessiAuditQueryableSink;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createMemoryAccessiAuditSink = createMemoryAccessiAuditSink;
|
|
13
|
+
exports.createHttpAccessiAuditSink = createHttpAccessiAuditSink;
|
|
14
|
+
exports.createCallbackAccessiAuditSink = createCallbackAccessiAuditSink;
|
|
15
|
+
exports.createRedisAccessiAuditSink = createRedisAccessiAuditSink;
|
|
16
|
+
const redisMetrics_1 = require("../observability/redisMetrics");
|
|
17
|
+
const DEFAULT_LIMIT = 100;
|
|
18
|
+
const MAX_LIMIT = 1000;
|
|
19
|
+
function normalizeQuery(query = {}) {
|
|
20
|
+
const limit = Number.isFinite(query.limit)
|
|
21
|
+
? Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query.limit)))
|
|
22
|
+
: DEFAULT_LIMIT;
|
|
23
|
+
const offset = Number.isFinite(query.offset) ? Math.max(0, Math.trunc(query.offset)) : 0;
|
|
24
|
+
return { limit, offset, order: query.order === 'asc' ? 'asc' : 'desc' };
|
|
25
|
+
}
|
|
26
|
+
function recordTime(record) {
|
|
27
|
+
const time = Date.parse(record.timestamp);
|
|
28
|
+
return Number.isFinite(time) ? time : 0;
|
|
29
|
+
}
|
|
30
|
+
function matches(record, query) {
|
|
31
|
+
if (query.event && record.event !== query.event)
|
|
32
|
+
return false;
|
|
33
|
+
if (query.codiceUtente !== undefined && Number(record.codiceUtente) !== query.codiceUtente)
|
|
34
|
+
return false;
|
|
35
|
+
const time = recordTime(record);
|
|
36
|
+
if (query.from) {
|
|
37
|
+
const from = Date.parse(query.from);
|
|
38
|
+
if (Number.isFinite(from) && time < from)
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (query.to) {
|
|
42
|
+
const to = Date.parse(query.to);
|
|
43
|
+
if (Number.isFinite(to) && time > to)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function paginate(records, query) {
|
|
49
|
+
const { limit, offset, order } = normalizeQuery(query);
|
|
50
|
+
const filtered = records.filter((record) => matches(record, query));
|
|
51
|
+
const ordered = order === 'asc' ? filtered : [...filtered].reverse();
|
|
52
|
+
return {
|
|
53
|
+
records: ordered.slice(offset, offset + limit),
|
|
54
|
+
total: filtered.length,
|
|
55
|
+
limit,
|
|
56
|
+
offset,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function cloneRecord(record) {
|
|
60
|
+
return JSON.parse(JSON.stringify(record));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Sink in memoria con query e retention.
|
|
64
|
+
*
|
|
65
|
+
* Adatto a sviluppo, test e singola istanza. Per piu istanze usare
|
|
66
|
+
* `createRedisAccessiAuditSink` o un sink esterno.
|
|
67
|
+
*/
|
|
68
|
+
function createMemoryAccessiAuditSink(options = {}) {
|
|
69
|
+
var _a, _b, _c, _d;
|
|
70
|
+
const maxRecords = Math.max(1, Math.trunc((_a = options.maxRecords) !== null && _a !== void 0 ? _a : 10000));
|
|
71
|
+
const retentionMs = Math.max(0, Number((_b = options.retentionMs) !== null && _b !== void 0 ? _b : 0));
|
|
72
|
+
const now = (_c = options.now) !== null && _c !== void 0 ? _c : Date.now;
|
|
73
|
+
let records = [];
|
|
74
|
+
const purgeExpired = () => {
|
|
75
|
+
if (!retentionMs)
|
|
76
|
+
return;
|
|
77
|
+
const threshold = now() - retentionMs;
|
|
78
|
+
records = records.filter((record) => recordTime(record) >= threshold);
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
name: (_d = options.name) !== null && _d !== void 0 ? _d : 'memory',
|
|
82
|
+
write(record) {
|
|
83
|
+
records.push(cloneRecord(record));
|
|
84
|
+
if (records.length > maxRecords)
|
|
85
|
+
records = records.slice(records.length - maxRecords);
|
|
86
|
+
purgeExpired();
|
|
87
|
+
},
|
|
88
|
+
query() {
|
|
89
|
+
return __awaiter(this, arguments, void 0, function* (query = {}) {
|
|
90
|
+
purgeExpired();
|
|
91
|
+
return paginate(records, query);
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
purge(before) {
|
|
95
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
96
|
+
const threshold = before.getTime();
|
|
97
|
+
const beforeCount = records.length;
|
|
98
|
+
records = records.filter((record) => recordTime(record) >= threshold);
|
|
99
|
+
return beforeCount - records.length;
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
all() {
|
|
103
|
+
return records.map(cloneRecord);
|
|
104
|
+
},
|
|
105
|
+
clear() {
|
|
106
|
+
records = [];
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function createHttpAccessiAuditSink(options) {
|
|
111
|
+
var _a;
|
|
112
|
+
if (!(options === null || options === void 0 ? void 0 : options.url))
|
|
113
|
+
throw new Error('Audit sink HTTP: url obbligatoria.');
|
|
114
|
+
return {
|
|
115
|
+
name: (_a = options.name) !== null && _a !== void 0 ? _a : 'http',
|
|
116
|
+
write(record) {
|
|
117
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
118
|
+
var _a, _b, _c;
|
|
119
|
+
const fetchImpl = (_a = options.fetch) !== null && _a !== void 0 ? _a : globalThis.fetch;
|
|
120
|
+
if (typeof fetchImpl !== 'function') {
|
|
121
|
+
throw new Error('Audit sink HTTP: fetch non disponibile.');
|
|
122
|
+
}
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const timeout = setTimeout(() => controller.abort(), (_b = options.timeoutMs) !== null && _b !== void 0 ? _b : 3000);
|
|
125
|
+
try {
|
|
126
|
+
const response = yield fetchImpl(options.url, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: Object.assign({ 'Content-Type': 'application/json' }, ((_c = options.headers) !== null && _c !== void 0 ? _c : {})),
|
|
129
|
+
body: JSON.stringify(record),
|
|
130
|
+
signal: controller.signal,
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new Error(`Audit sink HTTP: risposta ${response.status}.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clearTimeout(timeout);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Adattatore per database, OpenTelemetry o qualunque destinazione custom.
|
|
145
|
+
*
|
|
146
|
+
* La callback riceve un record gia redatto e serializzabile.
|
|
147
|
+
*/
|
|
148
|
+
function createCallbackAccessiAuditSink(write, name = 'callback') {
|
|
149
|
+
return { name, write };
|
|
150
|
+
}
|
|
151
|
+
const REDIS_WRITE_SCRIPT = `
|
|
152
|
+
local key = KEYS[1]
|
|
153
|
+
redis.call('RPUSH', key, ARGV[1])
|
|
154
|
+
redis.call('LTRIM', key, -tonumber(ARGV[2]), -1)
|
|
155
|
+
redis.call('PEXPIRE', key, ARGV[3])
|
|
156
|
+
return 1
|
|
157
|
+
`;
|
|
158
|
+
const REDIS_READ_SCRIPT = `return redis.call('LRANGE', KEYS[1], 0, -1)`;
|
|
159
|
+
const REDIS_REWRITE_SCRIPT = `
|
|
160
|
+
local key = KEYS[1]
|
|
161
|
+
redis.call('DEL', key)
|
|
162
|
+
for i = 2, #ARGV do
|
|
163
|
+
redis.call('RPUSH', key, ARGV[i])
|
|
164
|
+
end
|
|
165
|
+
redis.call('PEXPIRE', key, ARGV[1])
|
|
166
|
+
return #ARGV - 1
|
|
167
|
+
`;
|
|
168
|
+
function parseRedisRecords(value) {
|
|
169
|
+
if (!Array.isArray(value))
|
|
170
|
+
return [];
|
|
171
|
+
const records = [];
|
|
172
|
+
for (const entry of value) {
|
|
173
|
+
if (typeof entry !== 'string')
|
|
174
|
+
continue;
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(entry);
|
|
177
|
+
if (parsed && typeof parsed.event === 'string' && typeof parsed.timestamp === 'string') {
|
|
178
|
+
records.push(parsed);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (_a) {
|
|
182
|
+
// Un record corrotto non deve bloccare query ed export.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return records;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Sink Redis persistente, condiviso tra istanze e interrogabile.
|
|
189
|
+
*
|
|
190
|
+
* Usa una lista con `LTRIM` per limitare i record e `PEXPIRE` per la retention.
|
|
191
|
+
* Le query leggono la lista e filtrano lato applicazione.
|
|
192
|
+
*/
|
|
193
|
+
function createRedisAccessiAuditSink(options) {
|
|
194
|
+
var _a, _b, _c, _d;
|
|
195
|
+
if (!(options === null || options === void 0 ? void 0 : options.client) || typeof options.client.eval !== 'function') {
|
|
196
|
+
throw new Error('Audit sink Redis: client.eval non configurato.');
|
|
197
|
+
}
|
|
198
|
+
const client = (0, redisMetrics_1.instrumentAccessiRedisClient)('audit', options.client);
|
|
199
|
+
const key = `${((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:audit:').replace(/:*$/, ':')}events`;
|
|
200
|
+
const maxRecords = Math.max(1, Math.trunc((_b = options.maxRecords) !== null && _b !== void 0 ? _b : 10000));
|
|
201
|
+
const retentionMs = Math.max(1000, Math.trunc((_c = options.retentionMs) !== null && _c !== void 0 ? _c : 30 * 24 * 60 * 60 * 1000));
|
|
202
|
+
const readAll = () => __awaiter(this, void 0, void 0, function* () { return parseRedisRecords(yield client.eval(REDIS_READ_SCRIPT, 1, key)); });
|
|
203
|
+
return {
|
|
204
|
+
name: (_d = options.name) !== null && _d !== void 0 ? _d : 'redis',
|
|
205
|
+
write(record) {
|
|
206
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
207
|
+
yield client.eval(REDIS_WRITE_SCRIPT, 1, key, JSON.stringify(record), maxRecords, retentionMs);
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
query() {
|
|
211
|
+
return __awaiter(this, arguments, void 0, function* (query = {}) {
|
|
212
|
+
return paginate(yield readAll(), query);
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
purge(before) {
|
|
216
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
217
|
+
const records = yield readAll();
|
|
218
|
+
const kept = records.filter((record) => recordTime(record) >= before.getTime());
|
|
219
|
+
const removed = records.length - kept.length;
|
|
220
|
+
if (removed === 0)
|
|
221
|
+
return 0;
|
|
222
|
+
yield client.eval(REDIS_REWRITE_SCRIPT, 1, key, retentionMs, ...kept.map((record) => JSON.stringify(record)));
|
|
223
|
+
return removed;
|
|
224
|
+
});
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=auditSinks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auditSinks.js","sourceRoot":"","sources":["../../../src/accessi-module/security/auditSinks.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n AccessiAuditPage,\n AccessiAuditQuery,\n AccessiAuditQueryableSink,\n AccessiAuditRecord,\n AccessiAuditSink,\n} from './audit';\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\nimport type { AccessiRedisEvalClient } from '../observability/redisMetrics';\n\nconst DEFAULT_LIMIT = 100;\nconst MAX_LIMIT = 1_000;\n\nfunction normalizeQuery(query: AccessiAuditQuery = {}): Required<Pick<AccessiAuditQuery, 'limit' | 'offset' | 'order'>> {\n const limit = Number.isFinite(query.limit)\n ? Math.min(MAX_LIMIT, Math.max(1, Math.trunc(query.limit as number)))\n : DEFAULT_LIMIT;\n const offset = Number.isFinite(query.offset) ? Math.max(0, Math.trunc(query.offset as number)) : 0;\n return { limit, offset, order: query.order === 'asc' ? 'asc' : 'desc' };\n}\n\nfunction recordTime(record: AccessiAuditRecord): number {\n const time = Date.parse(record.timestamp);\n return Number.isFinite(time) ? time : 0;\n}\n\nfunction matches(record: AccessiAuditRecord, query: AccessiAuditQuery): boolean {\n if (query.event && record.event !== query.event) return false;\n if (query.codiceUtente !== undefined && Number(record.codiceUtente) !== query.codiceUtente) return false;\n const time = recordTime(record);\n if (query.from) {\n const from = Date.parse(query.from);\n if (Number.isFinite(from) && time < from) return false;\n }\n if (query.to) {\n const to = Date.parse(query.to);\n if (Number.isFinite(to) && time > to) return false;\n }\n return true;\n}\n\nfunction paginate(records: AccessiAuditRecord[], query: AccessiAuditQuery): AccessiAuditPage {\n const { limit, offset, order } = normalizeQuery(query);\n const filtered = records.filter((record) => matches(record, query));\n const ordered = order === 'asc' ? filtered : [...filtered].reverse();\n return {\n records: ordered.slice(offset, offset + limit),\n total: filtered.length,\n limit,\n offset,\n };\n}\n\nfunction cloneRecord(record: AccessiAuditRecord): AccessiAuditRecord {\n return JSON.parse(JSON.stringify(record)) as AccessiAuditRecord;\n}\n\n/** Opzioni del sink in memoria. */\nexport interface MemoryAccessiAuditSinkOptions {\n name?: string;\n /** Numero massimo di record conservati. Default: 10000. */\n maxRecords?: number;\n /** Retention in millisecondi. Default: nessuna scadenza. */\n retentionMs?: number;\n /** Orologio iniettabile per i test. */\n now?: () => number;\n}\n\nexport interface MemoryAccessiAuditSink extends AccessiAuditQueryableSink {\n all(): AccessiAuditRecord[];\n clear(): void;\n}\n\n/**\n * Sink in memoria con query e retention.\n *\n * Adatto a sviluppo, test e singola istanza. Per piu istanze usare\n * `createRedisAccessiAuditSink` o un sink esterno.\n */\nexport function createMemoryAccessiAuditSink(\n options: MemoryAccessiAuditSinkOptions = {},\n): MemoryAccessiAuditSink {\n const maxRecords = Math.max(1, Math.trunc(options.maxRecords ?? 10_000));\n const retentionMs = Math.max(0, Number(options.retentionMs ?? 0));\n const now = options.now ?? Date.now;\n let records: AccessiAuditRecord[] = [];\n\n const purgeExpired = (): void => {\n if (!retentionMs) return;\n const threshold = now() - retentionMs;\n records = records.filter((record) => recordTime(record) >= threshold);\n };\n\n return {\n name: options.name ?? 'memory',\n write(record) {\n records.push(cloneRecord(record));\n if (records.length > maxRecords) records = records.slice(records.length - maxRecords);\n purgeExpired();\n },\n async query(query = {}) {\n purgeExpired();\n return paginate(records, query);\n },\n async purge(before: Date) {\n const threshold = before.getTime();\n const beforeCount = records.length;\n records = records.filter((record) => recordTime(record) >= threshold);\n return beforeCount - records.length;\n },\n all() {\n return records.map(cloneRecord);\n },\n clear() {\n records = [];\n },\n };\n}\n\n/** Sink HTTP: inoltra ogni record a un endpoint esterno (SIEM, collector). */\nexport interface HttpAccessiAuditSinkOptions {\n name?: string;\n url: string;\n headers?: Record<string, string>;\n fetch?: typeof fetch;\n timeoutMs?: number;\n}\n\nexport function createHttpAccessiAuditSink(\n options: HttpAccessiAuditSinkOptions,\n): AccessiAuditSink {\n if (!options?.url) throw new Error('Audit sink HTTP: url obbligatoria.');\n return {\n name: options.name ?? 'http',\n async write(record) {\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new Error('Audit sink HTTP: fetch non disponibile.');\n }\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 3000);\n try {\n const response = await fetchImpl(options.url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n ...(options.headers ?? {}),\n },\n body: JSON.stringify(record),\n signal: controller.signal,\n });\n if (!response.ok) {\n throw new Error(`Audit sink HTTP: risposta ${response.status}.`);\n }\n } finally {\n clearTimeout(timeout);\n }\n },\n };\n}\n\n/**\n * Adattatore per database, OpenTelemetry o qualunque destinazione custom.\n *\n * La callback riceve un record gia redatto e serializzabile.\n */\nexport function createCallbackAccessiAuditSink(\n write: (record: AccessiAuditRecord) => void | Promise<void>,\n name = 'callback',\n): AccessiAuditSink {\n return { name, write };\n}\n\nexport interface RedisAccessiAuditSinkOptions {\n client: AccessiRedisEvalClient;\n name?: string;\n /** Default: `accessi:audit:`. */\n keyPrefix?: string;\n /** Numero massimo di record. Default: 10000. */\n maxRecords?: number;\n /** Retention Redis in millisecondi. Default: 30 giorni. */\n retentionMs?: number;\n}\n\nconst REDIS_WRITE_SCRIPT = `\nlocal key = KEYS[1]\nredis.call('RPUSH', key, ARGV[1])\nredis.call('LTRIM', key, -tonumber(ARGV[2]), -1)\nredis.call('PEXPIRE', key, ARGV[3])\nreturn 1\n`;\n\nconst REDIS_READ_SCRIPT = `return redis.call('LRANGE', KEYS[1], 0, -1)`;\n\nconst REDIS_REWRITE_SCRIPT = `\nlocal key = KEYS[1]\nredis.call('DEL', key)\nfor i = 2, #ARGV do\n redis.call('RPUSH', key, ARGV[i])\nend\nredis.call('PEXPIRE', key, ARGV[1])\nreturn #ARGV - 1\n`;\n\nfunction parseRedisRecords(value: unknown): AccessiAuditRecord[] {\n if (!Array.isArray(value)) return [];\n const records: AccessiAuditRecord[] = [];\n for (const entry of value) {\n if (typeof entry !== 'string') continue;\n try {\n const parsed = JSON.parse(entry) as AccessiAuditRecord;\n if (parsed && typeof parsed.event === 'string' && typeof parsed.timestamp === 'string') {\n records.push(parsed);\n }\n } catch {\n // Un record corrotto non deve bloccare query ed export.\n }\n }\n return records;\n}\n\n/**\n * Sink Redis persistente, condiviso tra istanze e interrogabile.\n *\n * Usa una lista con `LTRIM` per limitare i record e `PEXPIRE` per la retention.\n * Le query leggono la lista e filtrano lato applicazione.\n */\nexport function createRedisAccessiAuditSink(\n options: RedisAccessiAuditSinkOptions,\n): AccessiAuditQueryableSink {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Audit sink Redis: client.eval non configurato.');\n }\n const client = instrumentAccessiRedisClient('audit', options.client);\n const key = `${(options.keyPrefix ?? 'accessi:audit:').replace(/:*$/, ':')}events`;\n const maxRecords = Math.max(1, Math.trunc(options.maxRecords ?? 10_000));\n const retentionMs = Math.max(1_000, Math.trunc(options.retentionMs ?? 30 * 24 * 60 * 60 * 1000));\n\n const readAll = async (): Promise<AccessiAuditRecord[]> =>\n parseRedisRecords(await client.eval(REDIS_READ_SCRIPT, 1, key));\n\n return {\n name: options.name ?? 'redis',\n async write(record) {\n await client.eval(REDIS_WRITE_SCRIPT, 1, key, JSON.stringify(record), maxRecords, retentionMs);\n },\n async query(query = {}) {\n return paginate(await readAll(), query);\n },\n async purge(before: Date) {\n const records = await readAll();\n const kept = records.filter((record) => recordTime(record) >= before.getTime());\n const removed = records.length - kept.length;\n if (removed === 0) return 0;\n await client.eval(\n REDIS_REWRITE_SCRIPT,\n 1,\n key,\n retentionMs,\n ...kept.map((record) => JSON.stringify(record)),\n );\n return removed;\n },\n };\n}\n"]}
|
|
@@ -42,6 +42,7 @@ exports.getAccessiRefreshTokenVerificationConfig = getAccessiRefreshTokenVerific
|
|
|
42
42
|
const jwt = __importStar(require("jsonwebtoken"));
|
|
43
43
|
const passwordResetToken_1 = require("./passwordResetToken");
|
|
44
44
|
const metrics_1 = require("../observability/metrics");
|
|
45
|
+
const audit_1 = require("./audit");
|
|
45
46
|
function resolveAlgorithm(options) {
|
|
46
47
|
var _a;
|
|
47
48
|
return (_a = options.jwtOptions.algorithm) !== null && _a !== void 0 ? _a : "HS256";
|
|
@@ -130,6 +131,7 @@ function resolveAccessiVerificationKeys(config, token) {
|
|
|
130
131
|
return [key];
|
|
131
132
|
(_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 }));
|
|
132
133
|
(0, metrics_1.recordAccessiMetric)('accessi_jwt_unknown_key_id_total', 1, Object.assign({ purpose: config.purpose }, (header.algorithm ? { algorithm: header.algorithm } : {})));
|
|
134
|
+
(0, audit_1.auditAccessi)('jwt_unknown_key_id', Object.assign({ kid: header.keyId, purpose: config.purpose }, (header.algorithm ? { algorithm: header.algorithm } : {})));
|
|
133
135
|
return [];
|
|
134
136
|
}
|
|
135
137
|
/**
|
|
@@ -1 +1 @@
|
|
|
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\";\nimport { recordAccessiMetric } from \"../observability/metrics\";\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 recordAccessiMetric('accessi_jwt_unknown_key_id_total', 1, {\n purpose: config.purpose,\n ...(header.algorithm ? { algorithm: header.algorithm } : {}),\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"]}
|
|
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\";\nimport { recordAccessiMetric } from \"../observability/metrics\";\nimport { auditAccessi } from \"./audit\";\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 recordAccessiMetric('accessi_jwt_unknown_key_id_total', 1, {\n purpose: config.purpose,\n ...(header.algorithm ? { algorithm: header.algorithm } : {}),\n });\n auditAccessi('jwt_unknown_key_id', {\n kid: header.keyId,\n purpose: config.purpose,\n ...(header.algorithm ? { algorithm: header.algorithm } : {}),\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"]}
|
|
@@ -15,6 +15,7 @@ const common_1 = require("@nestjs/common");
|
|
|
15
15
|
const Utilities_1 = require("../../Utilities");
|
|
16
16
|
const rateLimitStore_1 = require("./rateLimitStore");
|
|
17
17
|
const metrics_1 = require("../observability/metrics");
|
|
18
|
+
const audit_1 = require("./audit");
|
|
18
19
|
const DEFAULT_PUBLIC_AUTH_RATE_LIMITS = {
|
|
19
20
|
login: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },
|
|
20
21
|
twoFactorVerify: { windowMs: 15 * 60 * 1000, maxAttempts: 20 },
|
|
@@ -82,6 +83,11 @@ function checkPublicAuthRateLimit(options_1, scope_1, req_1) {
|
|
|
82
83
|
const ipDecision = yield consumeRateLimit(store, `${scope}|ip|${ipAddress}`, rule);
|
|
83
84
|
if (!ipDecision.allowed) {
|
|
84
85
|
(0, metrics_1.countAccessiMetric)(options, 'rate_limit_blocked_total', { scope, layer: 'public' });
|
|
86
|
+
(0, audit_1.auditAccessi)('public_rate_limited', {
|
|
87
|
+
scope,
|
|
88
|
+
ip: ipAddress,
|
|
89
|
+
retryAfterSeconds: ipDecision.retryAfterSeconds,
|
|
90
|
+
});
|
|
85
91
|
return ipDecision;
|
|
86
92
|
}
|
|
87
93
|
let retryAfterSeconds = 0;
|
|
@@ -97,6 +103,7 @@ function checkPublicAuthRateLimit(options_1, scope_1, req_1) {
|
|
|
97
103
|
}
|
|
98
104
|
if (retryAfterSeconds > 0) {
|
|
99
105
|
(0, metrics_1.countAccessiMetric)(options, 'rate_limit_blocked_total', { scope, layer: 'public' });
|
|
106
|
+
(0, audit_1.auditAccessi)('public_rate_limited', { scope, ip: ipAddress, retryAfterSeconds });
|
|
100
107
|
return { allowed: false, retryAfterSeconds };
|
|
101
108
|
}
|
|
102
109
|
return { allowed: true, retryAfterSeconds: 0 };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publicAuthRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/publicAuthRateLimit.ts"],"names":[],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"publicAuthRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/publicAuthRateLimit.ts"],"names":[],"mappings":"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","sourcesContent":["import { HttpException, HttpStatus } from '@nestjs/common';\nimport { Request, Response } from 'express';\nimport { RestUtilities } from '../../Utilities';\nimport type { AccessiOptions, PublicAuthRateLimitRuleOptions } from '../AccessiModule';\nimport { getDefaultRateLimitStore } from './rateLimitStore';\nimport type { RateLimitDecision, RateLimitStore } from './rateLimitStore';\nimport { countAccessiMetric } from '../observability/metrics';\nimport { auditAccessi } from './audit';\n\nexport type PublicAuthRateLimitScope =\n | 'login'\n | 'twoFactorVerify'\n | 'twoFactorResend'\n | 'register'\n | 'passwordResetEmail'\n | 'passwordResetConfirm'\n | 'refresh'\n | 'getUserByToken'\n | 'adminBootstrap'\n | 'consoleEntry';\n\nconst DEFAULT_PUBLIC_AUTH_RATE_LIMITS: Record<\n PublicAuthRateLimitScope,\n PublicAuthRateLimitRuleOptions\n> = {\n login: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },\n twoFactorVerify: { windowMs: 15 * 60 * 1000, maxAttempts: 20 },\n twoFactorResend: { windowMs: 15 * 60 * 1000, maxAttempts: 5 },\n register: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },\n passwordResetEmail: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },\n passwordResetConfirm: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },\n refresh: { windowMs: 5 * 60 * 1000, maxAttempts: 60 },\n getUserByToken: { windowMs: 5 * 60 * 1000, maxAttempts: 30 },\n adminBootstrap: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },\n consoleEntry: { windowMs: 5 * 60 * 1000, maxAttempts: 30 },\n};\n\nfunction getRule(\n options: AccessiOptions,\n scope: PublicAuthRateLimitScope,\n): PublicAuthRateLimitRuleOptions {\n const configuredRule = options.publicAuthRateLimit?.[scope];\n\n if (\n configuredRule &&\n Number.isFinite(configuredRule.windowMs) &&\n configuredRule.windowMs > 0 &&\n Number.isFinite(configuredRule.maxAttempts) &&\n configuredRule.maxAttempts > 0\n ) {\n return {\n windowMs: Math.trunc(configuredRule.windowMs),\n maxAttempts: Math.trunc(configuredRule.maxAttempts),\n };\n }\n\n return DEFAULT_PUBLIC_AUTH_RATE_LIMITS[scope];\n}\n\nasync function consumeRateLimit(\n store: RateLimitStore,\n key: string,\n rule: PublicAuthRateLimitRuleOptions,\n): Promise<RateLimitDecision> {\n try {\n return await store.consume(key, rule.windowMs, rule.maxAttempts);\n } catch {\n throw new HttpException(\n {\n code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n message: 'Rate limit non disponibile. Riprova piu tardi.',\n },\n HttpStatus.SERVICE_UNAVAILABLE,\n );\n }\n}\n\nfunction normalizeIdentifier(identifier: string): string | null {\n if (typeof identifier !== 'string') {\n return null;\n }\n\n const normalized = identifier.trim().toLowerCase();\n return normalized === '' ? null : normalized;\n}\n\n/**\n * Applica il rate limit per IP e, opzionalmente, per soggetto.\n *\n * Con `publicAuthRateLimit.store` e possibile usare uno store Redis condiviso;\n * senza store usa la memoria del processo.\n */\nexport async function checkPublicAuthRateLimit(\n options: AccessiOptions,\n scope: PublicAuthRateLimitScope,\n req: Request,\n identifiers: string[] = [],\n): Promise<RateLimitDecision> {\n if (options.publicAuthRateLimit?.enabled === false) {\n return { allowed: true, retryAfterSeconds: 0 };\n }\n\n const rule = getRule(options, scope);\n const store = options.publicAuthRateLimit?.store ?? getDefaultRateLimitStore();\n const ipAddress =\n normalizeIdentifier(req.ip ?? req.socket?.remoteAddress ?? 'unknown') ?? 'unknown';\n\n // Consuma prima il bucket per IP: se gia bloccato non crea bucket per soggetto,\n // evitando crescita di memoria con identificatori sempre diversi.\n const ipDecision = await consumeRateLimit(store, `${scope}|ip|${ipAddress}`, rule);\n if (!ipDecision.allowed) {\n countAccessiMetric(options, 'rate_limit_blocked_total', { scope, layer: 'public' });\n auditAccessi('public_rate_limited', {\n scope,\n ip: ipAddress,\n retryAfterSeconds: ipDecision.retryAfterSeconds,\n });\n return ipDecision;\n }\n\n let retryAfterSeconds = 0;\n for (const identifier of identifiers) {\n const normalizedIdentifier = normalizeIdentifier(identifier);\n if (!normalizedIdentifier) {\n continue;\n }\n\n const subjectDecision = await consumeRateLimit(\n store,\n `${scope}|ip-subject|${ipAddress}|${normalizedIdentifier}`,\n rule,\n );\n if (!subjectDecision.allowed) {\n retryAfterSeconds = Math.max(retryAfterSeconds, subjectDecision.retryAfterSeconds);\n }\n }\n\n if (retryAfterSeconds > 0) {\n countAccessiMetric(options, 'rate_limit_blocked_total', { scope, layer: 'public' });\n auditAccessi('public_rate_limited', { scope, ip: ipAddress, retryAfterSeconds });\n return { allowed: false, retryAfterSeconds };\n }\n\n return { allowed: true, retryAfterSeconds: 0 };\n}\n\nexport function sendPublicAuthRateLimitExceeded(\n res: Response,\n retryAfterSeconds: number,\n): Response {\n res.setHeader('Retry-After', String(retryAfterSeconds));\n return RestUtilities.sendErrorMessage(\n res,\n 'Troppi tentativi. Riprova piu tardi.',\n 'PublicAuthRateLimit',\n HttpStatus.TOO_MANY_REQUESTS,\n );\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionRevocation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/sessionRevocation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\nimport { auditAccessi } from './audit';\nimport { countAccessiMetric } from '../observability/metrics';\nimport { isAccessiAccessTokenRevoked } from './accessTokenRevocation';\nimport { isRefreshTokenFamilyRevoked } from './refreshTokenRotation';\n\n/**\n * Revoca delle sessioni per utente.\n *\n * La revoca e un timestamp: un token emesso prima del cutoff viene rifiutato.\n * I token emessi dopo il cutoff restano validi. I token includono `iatMs`\n * (millisecondi) per rendere la revoca deterministica anche quando il cambio\n * password e il login avvengono nello stesso secondo; per i token legacy senza\n * `iatMs` si usa `iat` in secondi.\n */\nexport interface SessionRevocationStore {\n revokeUser(codiceUtente: number, revokedAtMs: number): Promise<void>;\n getUserRevocation(codiceUtente: number): Promise<number | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface SessionRevocationOptions {\n /** Default: disabilitata se non viene fornito uno store. */\n enabled?: boolean;\n /** Store condiviso (es. Redis) per invalidare le sessioni tra istanze. */\n store?: SessionRevocationStore;\n /** TTL delle chiavi Redis. Default: 365 giorni. */\n ttlMs?: number;\n}\n\nexport interface MemorySessionRevocationStore extends SessionRevocationStore {\n clear(): void;\n}\n\nconst REVOKE_USER_SCRIPT =\n \"if tonumber(ARGV[2]) > 0 then return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) else return redis.call('SET', KEYS[1], ARGV[1]) end\";\nconst GET_USER_SCRIPT = \"return redis.call('GET', KEYS[1])\";\n\nexport function createMemorySessionRevocationStore(): MemorySessionRevocationStore {\n const revokedAt = new Map<number, number>();\n return {\n async revokeUser(codiceUtente, timestamp) {\n revokedAt.set(codiceUtente, timestamp);\n },\n async getUserRevocation(codiceUtente) {\n return revokedAt.get(codiceUtente) ?? null;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n revokedAt.clear();\n },\n };\n}\n\nexport interface SessionRevocationRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisSessionRevocationStoreOptions {\n client: SessionRevocationRedisClient;\n /** Default: `accessi:revoked-user:`. */\n keyPrefix?: string;\n /** Default: 365 giorni. */\n ttlMs?: number;\n}\n\n/**\n * Store Redis per la revoca sessioni.\n *\n * `revokeUser` scrive un timestamp con TTL; `getUserRevocation` lo legge.\n * Il TTL deve essere maggiore della durata massima dei token.\n */\nexport function createRedisSessionRevocationStore(\n options: RedisSessionRevocationStoreOptions,\n): SessionRevocationStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis session revocation store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:revoked-user:').replace(/:+$/, ':');\n const ttlMs = Math.max(0, Math.trunc(options.ttlMs ?? 365 * 24 * 60 * 60 * 1000));\n const client = instrumentAccessiRedisClient('session_revocation', options.client);\n\n return {\n async revokeUser(codiceUtente, revokedAtMs) {\n await client.eval(\n REVOKE_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n String(revokedAtMs),\n ttlMs,\n );\n },\n\n async getUserRevocation(codiceUtente) {\n const value = await client.eval(\n GET_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n );\n if (typeof value !== 'string' && typeof value !== 'number') return null;\n const timestamp = Number(value);\n return Number.isFinite(timestamp) ? timestamp : null;\n },\n\n async healthCheck() {\n try {\n const result = await client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n\nconst defaultMemoryStore = createMemorySessionRevocationStore();\n\n/** Store di default in memoria, usato quando `sessionRevocation.store` non e configurato ma la revoca e abilitata. */\nexport function getSessionRevocationStore(options: AccessiOptions): SessionRevocationStore | undefined {\n if (options.sessionRevocation?.enabled === false) return undefined;\n if (options.sessionRevocation?.store) return options.sessionRevocation.store;\n return options.sessionRevocation?.enabled === true ? defaultMemoryStore : undefined;\n}\n\n/**\n * Issued-at in millisecondi. I token emessi da questa versione includono `iatMs`;\n * per quelli legacy si ricade su `iat` (secondi) per restare compatibili.\n */\nexport function resolveAccessiTokenIssuedAtMs(payload: Record<string, unknown>): number | null {\n const issuedAtMs = Number(payload.iatMs);\n if (Number.isFinite(issuedAtMs) && issuedAtMs > 0) return issuedAtMs;\n\n const issuedAt = Number(payload.iat);\n if (!Number.isFinite(issuedAt)) return null;\n return issuedAt * 1000;\n}\n\n/**\n * True se il token e stato revocato: famiglia refresh revocata, `jti` revocato\n * oppure sessione utente revocata prima dell'emissione del token.\n */\nexport async function isAccessiTokenRevoked(\n options: AccessiOptions,\n payload: Record<string, unknown>,\n codiceUtente: number,\n): Promise<boolean> {\n if (await isRefreshTokenFamilyRevoked(options, payload.fid)) return true;\n if (await isAccessiAccessTokenRevoked(options, payload)) return true;\n\n const store = getSessionRevocationStore(options);\n if (!store) return false;\n\n const issuedAtMs = resolveAccessiTokenIssuedAtMs(payload);\n if (issuedAtMs === null) return true;\n\n const revokedAt = await store.getUserRevocation(codiceUtente);\n if (revokedAt === null) return false;\n\n // I token emessi da questa versione hanno `iatMs`: un token emesso nello\n // stesso millisecondo della revoca e considerato revocato. Per i token\n // legacy (solo `iat`, secondi) si mantiene il confronto per secondo.\n const preciseIssuedAtMs = Number(payload.iatMs);\n if (Number.isFinite(preciseIssuedAtMs) && preciseIssuedAtMs > 0) {\n return issuedAtMs <= revokedAt;\n }\n return issuedAtMs < Math.floor(revokedAt / 1000) * 1000;\n}\n\n/** Revoca tutte le sessioni dell'utente. Richiede uno store configurato. */\nexport async function revokeAccessiUserSessions(\n options: AccessiOptions,\n codiceUtente: number,\n revokedAtMs: number = Date.now(),\n reason: string = 'manual',\n): Promise<void> {\n const store = getSessionRevocationStore(options);\n if (!store) {\n throw new Error('Session revocation store non configurato.');\n }\n await store.revokeUser(codiceUtente, revokedAtMs);\n auditAccessi('accessi_sessions_revoked', { codiceUtente, revokedAtMs, reason });\n countAccessiMetric(options, 'sessions_revoked_total', { reason });\n}\n\n/**\n * Variante per i flussi applicativi (cambio password, blocco, cancellazione):\n * se la revoca non e configurata non fallisce, perche quei flussi devono\n * continuare a funzionare anche senza store condiviso.\n */\nexport async function revokeAccessiUserSessionsIfConfigured(\n options: AccessiOptions,\n codiceUtente: number,\n reason: string = 'manual',\n revokedAtMs: number = Date.now(),\n): Promise<boolean> {\n if (!getSessionRevocationStore(options)) return false;\n await revokeAccessiUserSessions(options, codiceUtente, revokedAtMs, reason);\n return true;\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultSessionRevocations(): void {\n defaultMemoryStore.clear();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sessionRevocation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/sessionRevocation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\nimport { auditAccessi } from './audit';\nimport { countAccessiMetric } from '../observability/metrics';import { isAccessiAccessTokenRevoked } from './accessTokenRevocation';\nimport { isRefreshTokenFamilyRevoked } from './refreshTokenRotation';\n\n/**\n * Revoca delle sessioni per utente.\n *\n * La revoca e un timestamp: un token emesso prima del cutoff viene rifiutato.\n * I token emessi dopo il cutoff restano validi. I token includono `iatMs`\n * (millisecondi) per rendere la revoca deterministica anche quando il cambio\n * password e il login avvengono nello stesso secondo; per i token legacy senza\n * `iatMs` si usa `iat` in secondi.\n */\nexport interface SessionRevocationStore {\n revokeUser(codiceUtente: number, revokedAtMs: number): Promise<void>;\n getUserRevocation(codiceUtente: number): Promise<number | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface SessionRevocationOptions {\n /** Default: disabilitata se non viene fornito uno store. */\n enabled?: boolean;\n /** Store condiviso (es. Redis) per invalidare le sessioni tra istanze. */\n store?: SessionRevocationStore;\n /** TTL delle chiavi Redis. Default: 365 giorni. */\n ttlMs?: number;\n}\n\nexport interface MemorySessionRevocationStore extends SessionRevocationStore {\n clear(): void;\n}\n\nconst REVOKE_USER_SCRIPT =\n \"if tonumber(ARGV[2]) > 0 then return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) else return redis.call('SET', KEYS[1], ARGV[1]) end\";\nconst GET_USER_SCRIPT = \"return redis.call('GET', KEYS[1])\";\n\nexport function createMemorySessionRevocationStore(): MemorySessionRevocationStore {\n const revokedAt = new Map<number, number>();\n return {\n async revokeUser(codiceUtente, timestamp) {\n revokedAt.set(codiceUtente, timestamp);\n },\n async getUserRevocation(codiceUtente) {\n return revokedAt.get(codiceUtente) ?? null;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n revokedAt.clear();\n },\n };\n}\n\nexport interface SessionRevocationRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisSessionRevocationStoreOptions {\n client: SessionRevocationRedisClient;\n /** Default: `accessi:revoked-user:`. */\n keyPrefix?: string;\n /** Default: 365 giorni. */\n ttlMs?: number;\n}\n\n/**\n * Store Redis per la revoca sessioni.\n *\n * `revokeUser` scrive un timestamp con TTL; `getUserRevocation` lo legge.\n * Il TTL deve essere maggiore della durata massima dei token.\n */\nexport function createRedisSessionRevocationStore(\n options: RedisSessionRevocationStoreOptions,\n): SessionRevocationStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis session revocation store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:revoked-user:').replace(/:+$/, ':');\n const ttlMs = Math.max(0, Math.trunc(options.ttlMs ?? 365 * 24 * 60 * 60 * 1000));\n const client = instrumentAccessiRedisClient('session_revocation', options.client);\n\n return {\n async revokeUser(codiceUtente, revokedAtMs) {\n await client.eval(\n REVOKE_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n String(revokedAtMs),\n ttlMs,\n );\n },\n\n async getUserRevocation(codiceUtente) {\n const value = await client.eval(\n GET_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n );\n if (typeof value !== 'string' && typeof value !== 'number') return null;\n const timestamp = Number(value);\n return Number.isFinite(timestamp) ? timestamp : null;\n },\n\n async healthCheck() {\n try {\n const result = await client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n\nconst defaultMemoryStore = createMemorySessionRevocationStore();\n\n/** Store di default in memoria, usato quando `sessionRevocation.store` non e configurato ma la revoca e abilitata. */\nexport function getSessionRevocationStore(options: AccessiOptions): SessionRevocationStore | undefined {\n if (options.sessionRevocation?.enabled === false) return undefined;\n if (options.sessionRevocation?.store) return options.sessionRevocation.store;\n return options.sessionRevocation?.enabled === true ? defaultMemoryStore : undefined;\n}\n\n/**\n * Issued-at in millisecondi. I token emessi da questa versione includono `iatMs`;\n * per quelli legacy si ricade su `iat` (secondi) per restare compatibili.\n */\nexport function resolveAccessiTokenIssuedAtMs(payload: Record<string, unknown>): number | null {\n const issuedAtMs = Number(payload.iatMs);\n if (Number.isFinite(issuedAtMs) && issuedAtMs > 0) return issuedAtMs;\n\n const issuedAt = Number(payload.iat);\n if (!Number.isFinite(issuedAt)) return null;\n return issuedAt * 1000;\n}\n\n/**\n * True se il token e stato revocato: famiglia refresh revocata, `jti` revocato\n * oppure sessione utente revocata prima dell'emissione del token.\n */\nexport async function isAccessiTokenRevoked(\n options: AccessiOptions,\n payload: Record<string, unknown>,\n codiceUtente: number,\n): Promise<boolean> {\n if (await isRefreshTokenFamilyRevoked(options, payload.fid)) return true;\n if (await isAccessiAccessTokenRevoked(options, payload)) return true;\n\n const store = getSessionRevocationStore(options);\n if (!store) return false;\n\n const issuedAtMs = resolveAccessiTokenIssuedAtMs(payload);\n if (issuedAtMs === null) return true;\n\n const revokedAt = await store.getUserRevocation(codiceUtente);\n if (revokedAt === null) return false;\n\n // I token emessi da questa versione hanno `iatMs`: un token emesso nello\n // stesso millisecondo della revoca e considerato revocato. Per i token\n // legacy (solo `iat`, secondi) si mantiene il confronto per secondo.\n const preciseIssuedAtMs = Number(payload.iatMs);\n if (Number.isFinite(preciseIssuedAtMs) && preciseIssuedAtMs > 0) {\n return issuedAtMs <= revokedAt;\n }\n return issuedAtMs < Math.floor(revokedAt / 1000) * 1000;\n}\n\n/** Revoca tutte le sessioni dell'utente. Richiede uno store configurato. */\nexport async function revokeAccessiUserSessions(\n options: AccessiOptions,\n codiceUtente: number,\n revokedAtMs: number = Date.now(),\n reason: string = 'manual',\n): Promise<void> {\n const store = getSessionRevocationStore(options);\n if (!store) {\n throw new Error('Session revocation store non configurato.');\n }\n await store.revokeUser(codiceUtente, revokedAtMs);\n auditAccessi('accessi_sessions_revoked', { codiceUtente, revokedAtMs, reason });\n countAccessiMetric(options, 'sessions_revoked_total', { reason });\n}\n\n/**\n * Variante per i flussi applicativi (cambio password, blocco, cancellazione):\n * se la revoca non e configurata non fallisce, perche quei flussi devono\n * continuare a funzionare anche senza store condiviso.\n */\nexport async function revokeAccessiUserSessionsIfConfigured(\n options: AccessiOptions,\n codiceUtente: number,\n reason: string = 'manual',\n revokedAtMs: number = Date.now(),\n): Promise<boolean> {\n if (!getSessionRevocationStore(options)) return false;\n await revokeAccessiUserSessions(options, codiceUtente, revokedAtMs, reason);\n return true;\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultSessionRevocations(): void {\n defaultMemoryStore.clear();\n}\n"]}
|
|
Binary file
|