emilsoftware-utilities 2.0.0-dev.68 → 2.0.0-dev.69
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/resource-server/errors.d.ts +1 -1
- package/dist/accessi/resource-server/errors.js.map +1 -1
- package/dist/accessi/resource-server/express.js +21 -0
- package/dist/accessi/resource-server/express.js.map +1 -1
- package/dist/accessi/resource-server/nest.js +13 -0
- package/dist/accessi/resource-server/nest.js.map +1 -1
- package/dist/accessi/resource-server/runtime.js +11 -1
- package/dist/accessi/resource-server/runtime.js.map +1 -1
- package/dist/accessi/resource-server/types.d.ts +7 -0
- package/dist/accessi/resource-server/types.js.map +1 -1
- package/dist/accessi/server/index.d.ts +1 -0
- package/dist/accessi/server/index.js +1 -0
- package/dist/accessi/server/index.js.map +1 -1
- package/dist/accessi/server/standalone.d.ts +41 -0
- package/dist/accessi/server/standalone.js +264 -0
- package/dist/accessi/server/standalone.js.map +1 -0
- package/dist/accessi-module/AccessiModule.d.ts +26 -1
- package/dist/accessi-module/AccessiModule.js +5 -2
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/accessiFetch.d.ts +5 -0
- package/dist/accessi-module/Console/accessiFetch.js +7 -4
- package/dist/accessi-module/Console/accessiFetch.js.map +1 -1
- package/dist/accessi-module/Console/console.js +12 -4
- package/dist/accessi-module/Console/generated/emilsoftware-utilities.d.ts +31 -1
- package/dist/accessi-module/Console/generated/emilsoftware-utilities.js +28 -3
- package/dist/accessi-module/Console/generated/emilsoftware-utilities.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/exchangeFederatedIdentityRequest.d.ts +12 -0
- package/dist/accessi-module/Console/generated/model/exchangeFederatedIdentityRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/exchangeFederatedIdentityRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/federatedAuthenticationResultDto.d.ts +15 -0
- package/dist/accessi-module/Console/generated/model/federatedAuthenticationResultDto.js +3 -0
- package/dist/accessi-module/Console/generated/model/federatedAuthenticationResultDto.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/federatedExchangeResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/federatedExchangeResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/federatedExchangeResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/index.d.ts +6 -0
- package/dist/accessi-module/Console/generated/model/index.js +6 -0
- package/dist/accessi-module/Console/generated/model/index.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/loginResult.d.ts +2 -0
- package/dist/accessi-module/Console/generated/model/loginResult.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/refreshTokenRequest.d.ts +10 -0
- package/dist/accessi-module/Console/generated/model/refreshTokenRequest.js +9 -0
- package/dist/accessi-module/Console/generated/model/refreshTokenRequest.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/refreshTokenResponse.d.ts +14 -0
- package/dist/accessi-module/Console/generated/model/refreshTokenResponse.js +3 -0
- package/dist/accessi-module/Console/generated/model/refreshTokenResponse.js.map +1 -0
- package/dist/accessi-module/Console/generated/model/refreshTokenResult.d.ts +13 -0
- package/dist/accessi-module/Console/generated/model/refreshTokenResult.js +3 -0
- package/dist/accessi-module/Console/generated/model/refreshTokenResult.js.map +1 -0
- package/dist/accessi-module/Console/wiki.js +8 -1
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Controllers/AccessiConsoleController.d.ts +6 -1
- package/dist/accessi-module/Controllers/AccessiConsoleController.js +24 -1
- package/dist/accessi-module/Controllers/AccessiConsoleController.js.map +1 -1
- package/dist/accessi-module/Controllers/AuthController.d.ts +3 -1
- package/dist/accessi-module/Controllers/AuthController.js +84 -2
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Dtos/FederatedExchangeDtos.d.ts +21 -0
- package/dist/accessi-module/Dtos/FederatedExchangeDtos.js +74 -0
- package/dist/accessi-module/Dtos/FederatedExchangeDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/LoginResponse.d.ts +1 -0
- package/dist/accessi-module/Dtos/LoginResponse.js +6 -0
- package/dist/accessi-module/Dtos/LoginResponse.js.map +1 -1
- package/dist/accessi-module/Dtos/RefreshTokenDtos.d.ts +12 -0
- package/dist/accessi-module/Dtos/RefreshTokenDtos.js +50 -0
- package/dist/accessi-module/Dtos/RefreshTokenDtos.js.map +1 -0
- package/dist/accessi-module/Dtos/index.d.ts +2 -0
- package/dist/accessi-module/Dtos/index.js +2 -0
- package/dist/accessi-module/Dtos/index.js.map +1 -1
- package/dist/accessi-module/Services/AuthService/AuthService.d.ts +14 -0
- package/dist/accessi-module/Services/AuthService/AuthService.js +99 -6
- package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js +1 -1
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js.map +1 -1
- package/dist/accessi-module/federated-auth/FederatedAuthExchangeController.d.ts +15 -0
- package/dist/accessi-module/federated-auth/FederatedAuthExchangeController.js +85 -0
- package/dist/accessi-module/federated-auth/FederatedAuthExchangeController.js.map +1 -0
- package/dist/accessi-module/federated-auth/FederatedAuthService.js +7 -1
- package/dist/accessi-module/federated-auth/FederatedAuthService.js.map +1 -1
- package/dist/accessi-module/index.d.ts +8 -0
- package/dist/accessi-module/index.js +40 -4
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/jwt/jwt.strategy.js +4 -0
- package/dist/accessi-module/jwt/jwt.strategy.js.map +1 -1
- package/dist/accessi-module/middleware/authenticateGen.js +4 -0
- package/dist/accessi-module/middleware/authenticateGen.js.map +1 -1
- package/dist/accessi-module/security/audit.d.ts +7 -0
- package/dist/accessi-module/security/audit.js +14 -0
- package/dist/accessi-module/security/audit.js.map +1 -0
- package/dist/accessi-module/security/authenticatedToken.d.ts +1 -1
- package/dist/accessi-module/security/authenticatedToken.js +3 -3
- package/dist/accessi-module/security/authenticatedToken.js.map +1 -1
- package/dist/accessi-module/security/consoleEntry.d.ts +2 -0
- package/dist/accessi-module/security/consoleEntry.js +16 -0
- package/dist/accessi-module/security/consoleEntry.js.map +1 -1
- package/dist/accessi-module/security/edgeRateLimit.d.ts +2 -0
- package/dist/accessi-module/security/edgeRateLimit.js +11 -0
- package/dist/accessi-module/security/edgeRateLimit.js.map +1 -1
- package/dist/accessi-module/security/jwksBootstrap.d.ts +15 -0
- package/dist/accessi-module/security/jwksBootstrap.js +44 -0
- package/dist/accessi-module/security/jwksBootstrap.js.map +1 -0
- package/dist/accessi-module/security/jwtTokenOptions.d.ts +9 -2
- package/dist/accessi-module/security/jwtTokenOptions.js +18 -4
- package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
- package/dist/accessi-module/security/passwordResetToken.d.ts +1 -1
- package/dist/accessi-module/security/passwordResetToken.js.map +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.d.ts +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.js +1 -0
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
- package/dist/accessi-module/security/rateLimitStore.d.ts +2 -0
- package/dist/accessi-module/security/rateLimitStore.js +16 -0
- package/dist/accessi-module/security/rateLimitStore.js.map +1 -1
- package/dist/accessi-module/security/refreshTokenRotation.d.ts +40 -0
- package/dist/accessi-module/security/refreshTokenRotation.js +91 -0
- package/dist/accessi-module/security/refreshTokenRotation.js.map +1 -0
- package/dist/accessi-module/security/sessionRevocation.d.ts +52 -0
- package/dist/accessi-module/security/sessionRevocation.js +124 -0
- package/dist/accessi-module/security/sessionRevocation.js.map +1 -0
- package/dist/accessi-module/security/validateOptions.d.ts +19 -0
- package/dist/accessi-module/security/validateOptions.js +115 -0
- package/dist/accessi-module/security/validateOptions.js.map +1 -0
- package/dist/emilsoftware-utilities-2.0.0-dev.69.tgz +0 -0
- package/dist/initEmilsoftwareModule.js +22 -4
- package/dist/initEmilsoftwareModule.js.map +1 -1
- package/dist/scripts/startAccessiService.d.ts +1 -0
- package/dist/scripts/startAccessiService.js +11 -0
- package/dist/scripts/startAccessiService.js.map +1 -0
- package/docs/accessi-openapi-fingerprint.json +190 -0
- package/docs/standalone-service.md +176 -0
- package/integration-prompt.md +160 -0
- package/package.json +9 -4
- package/dist/emilsoftware-utilities-2.0.0-dev.68.tgz +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit strutturato degli eventi di sicurezza Accessi.
|
|
3
|
+
*
|
|
4
|
+
* Il Logger redige gia campi come token, password, secret e authorization.
|
|
5
|
+
* Non inserire mai payload completi o segreti nei dettagli.
|
|
6
|
+
*/
|
|
7
|
+
export declare function auditAccessi(event: string, details?: Record<string, unknown>): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditAccessi = auditAccessi;
|
|
4
|
+
const Logger_1 = require("../../Logger");
|
|
5
|
+
/**
|
|
6
|
+
* Audit strutturato degli eventi di sicurezza Accessi.
|
|
7
|
+
*
|
|
8
|
+
* Il Logger redige gia campi come token, password, secret e authorization.
|
|
9
|
+
* Non inserire mai payload completi o segreti nei dettagli.
|
|
10
|
+
*/
|
|
11
|
+
function auditAccessi(event, details = {}) {
|
|
12
|
+
new Logger_1.Logger('AccessiAudit').warning(JSON.stringify(Object.assign({ event, timestamp: new Date().toISOString() }, details)));
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/audit.ts"],"names":[],"mappings":";;AAQA,oCAQC;AAhBD,yCAAsC;AAEtC;;;;;GAKG;AACH,SAAgB,YAAY,CAAC,KAAa,EAAE,UAAmC,EAAE;IAC/E,IAAI,eAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAChC,IAAI,CAAC,SAAS,iBACZ,KAAK,EACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAChC,OAAO,EACV,CACH,CAAC;AACJ,CAAC","sourcesContent":["import { Logger } from '../../Logger';\n\n/**\n * Audit strutturato degli eventi di sicurezza Accessi.\n *\n * Il Logger redige gia campi come token, password, secret e authorization.\n * Non inserire mai payload completi o segreti nei dettagli.\n */\nexport function auditAccessi(event: string, details: Record<string, unknown> = {}): void {\n new Logger('AccessiAudit').warning(\n JSON.stringify({\n event,\n timestamp: new Date().toISOString(),\n ...details,\n }),\n );\n}\n"]}
|
|
@@ -13,7 +13,7 @@ export interface AccessiAuthenticatedUserSnapshot {
|
|
|
13
13
|
/** Shared, strict Bearer parsing for the Nest guard and Express middleware. */
|
|
14
14
|
export declare function extractAccessiBearerToken(header: unknown): string | undefined;
|
|
15
15
|
/** Estrae il codice utente sia dai JWT Accessi moderni sia dai payload legacy compatibili. */
|
|
16
|
-
export declare function resolveCodiceUtenteFromTokenPayload(decoded: unknown): number | undefined;
|
|
16
|
+
export declare function resolveCodiceUtenteFromTokenPayload(decoded: unknown, expectedType?: 'access' | 'refresh'): number | undefined;
|
|
17
17
|
/**
|
|
18
18
|
* Mantiene le claim estranee al modulo ma sovrascrive il profilo con valori correnti e autorevoli dal database.
|
|
19
19
|
* `tokenPayload` conserva il payload verificato originale per audit o integrazioni che ne abbiano necessita.
|
|
@@ -21,10 +21,10 @@ function extractAccessiBearerToken(header) {
|
|
|
21
21
|
return (_a = /^Bearer[ \t]+([^\s]+)$/i.exec(header.trim())) === null || _a === void 0 ? void 0 : _a[1];
|
|
22
22
|
}
|
|
23
23
|
/** Estrae il codice utente sia dai JWT Accessi moderni sia dai payload legacy compatibili. */
|
|
24
|
-
function resolveCodiceUtenteFromTokenPayload(decoded) {
|
|
24
|
+
function resolveCodiceUtenteFromTokenPayload(decoded, expectedType = 'access') {
|
|
25
25
|
var _a;
|
|
26
|
-
// `typ` obbligatorio: un token senza tipo non e un
|
|
27
|
-
if (!isRecord(decoded) || decoded.typ !==
|
|
26
|
+
// `typ` obbligatorio: un token senza tipo non e un token Accessi valido.
|
|
27
|
+
if (!isRecord(decoded) || decoded.typ !== expectedType) {
|
|
28
28
|
return undefined;
|
|
29
29
|
}
|
|
30
30
|
const userData = isRecord(decoded.userData) ? decoded.userData : undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authenticatedToken.js","sourceRoot":"","sources":["../../../src/accessi-module/security/authenticatedToken.ts"],"names":[],"mappings":";;AAuBA,8DAGC;AAGD,
|
|
1
|
+
{"version":3,"file":"authenticatedToken.js","sourceRoot":"","sources":["../../../src/accessi-module/security/authenticatedToken.ts"],"names":[],"mappings":"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","sourcesContent":["import { StatoRegistrazione } from '../Dtos/StatoRegistrazione';\n\n/** Stato autorevole minimo riletto dal database per ogni verifica JWT. */\nexport interface AccessiAuthenticatedUserSnapshot {\n codiceUtente: number;\n email?: string;\n statoRegistrazione: StatoRegistrazione;\n flagSuper: boolean;\n flagAdmin: boolean;\n flagDueFattori?: boolean;\n passwordlessLoginEnabled?: boolean;\n passwordLoginEnabled?: boolean;\n}\n\nfunction normalizeBooleanFlag(value: unknown): boolean {\n return value === true || value === 1 || value === '1';\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** Shared, strict Bearer parsing for the Nest guard and Express middleware. */\nexport function extractAccessiBearerToken(header: unknown): string | undefined {\n if (typeof header !== 'string') return undefined;\n return /^Bearer[ \\t]+([^\\s]+)$/i.exec(header.trim())?.[1];\n}\n\n/** Estrae il codice utente sia dai JWT Accessi moderni sia dai payload legacy compatibili. */\nexport function resolveCodiceUtenteFromTokenPayload(\n decoded: unknown,\n expectedType: 'access' | 'refresh' = 'access',\n): number | undefined {\n // `typ` obbligatorio: un token senza tipo non e un token Accessi valido.\n if (!isRecord(decoded) || decoded.typ !== expectedType) {\n return undefined;\n }\n\n const userData = isRecord(decoded.userData) ? decoded.userData : undefined;\n const utente = isRecord(decoded.utente)\n ? decoded.utente\n : isRecord(userData?.utente)\n ? userData.utente\n : undefined;\n\n const codiceUtente = Number(utente?.codiceUtente ?? decoded.codiceUtente);\n if (!Number.isSafeInteger(codiceUtente) || codiceUtente <= 0) {\n return undefined;\n }\n\n return codiceUtente;\n}\n\n/**\n * Mantiene le claim estranee al modulo ma sovrascrive il profilo con valori correnti e autorevoli dal database.\n * `tokenPayload` conserva il payload verificato originale per audit o integrazioni che ne abbiano necessita.\n */\nexport function buildAuthenticatedTokenPayload(\n decoded: unknown,\n utente: AccessiAuthenticatedUserSnapshot,\n): Record<string, unknown> {\n const basePayload = isRecord(decoded) ? { ...decoded } : {};\n const userData = isRecord(basePayload.userData) ? { ...basePayload.userData } : {};\n\n const normalizedUser = {\n ...utente,\n flagSuper: normalizeBooleanFlag(utente.flagSuper),\n flagAdmin: normalizeBooleanFlag(utente.flagAdmin),\n flagDueFattori: normalizeBooleanFlag(utente.flagDueFattori),\n passwordlessLoginEnabled: normalizeBooleanFlag(utente.passwordlessLoginEnabled),\n };\n\n return {\n ...basePayload,\n codiceUtente: normalizedUser.codiceUtente,\n email: normalizedUser.email,\n statoRegistrazione: normalizedUser.statoRegistrazione,\n flagSuper: normalizedUser.flagSuper,\n flagAdmin: normalizedUser.flagAdmin,\n flagDueFattori: normalizedUser.flagDueFattori,\n passwordlessLoginEnabled: normalizedUser.passwordlessLoginEnabled,\n passwordLoginEnabled: normalizedUser.passwordLoginEnabled,\n utente: normalizedUser,\n userData: {\n ...userData,\n utente: normalizedUser,\n },\n tokenPayload: decoded,\n };\n}\n\n/**\n * Riconosce i token emessi dalla console per l'accesso temporaneo \"Login as\".\n * L'attore ha gia completato l'autenticazione amministrativa, quindi il token\n * non richiede un ulteriore codice email anche se il target ha la 2FA attiva.\n */\nexport function isLoginAsTokenPayload(decoded: unknown): boolean {\n if (!isRecord(decoded) || decoded.loginAs !== true) return false;\n return Array.isArray(decoded.amr) && decoded.amr.includes('login_as');\n}\n\n/** Enabling 2FA invalidates sessions which have not completed the required verification. */\nexport function isAccessiTokenAllowedForUser(decoded: unknown, user: AccessiAuthenticatedUserSnapshot): boolean {\n if (!isRecord(decoded)) return false;\n if (isLoginAsTokenPayload(decoded)) return true;\n const methods = Array.isArray(decoded.amr) ? decoded.amr : [];\n if (user.flagDueFattori && !methods.includes('otp')) return false;\n if (methods.includes('passwordless') && (!user.flagDueFattori || !user.passwordlessLoginEnabled)) return false;\n return true;\n}\n\n/** Un JWT puo essere usato soltanto da utenti nello stato confermato, indipendentemente dalla sua scadenza. */\nexport function isAuthenticatedUserEnabledForJwt(\n utente: AccessiAuthenticatedUserSnapshot | null | undefined,\n): utente is AccessiAuthenticatedUserSnapshot {\n return utente?.statoRegistrazione === StatoRegistrazione.CONF;\n}\n"]}
|
|
@@ -22,6 +22,8 @@ export interface ConsoleEntryTicket {
|
|
|
22
22
|
export interface ConsoleEntryStore {
|
|
23
23
|
issue(record: ConsoleEntryRecord, ttlMs: number): Promise<ConsoleEntryTicket>;
|
|
24
24
|
consume(ticket: string): Promise<ConsoleEntryRecord | null>;
|
|
25
|
+
/** Health check opzionale per readiness del servizio. */
|
|
26
|
+
healthCheck?(): Promise<boolean>;
|
|
25
27
|
}
|
|
26
28
|
/** Store in memoria con API sincrona per retrocompatibilita e API async per l'interfaccia. */
|
|
27
29
|
export interface MemoryConsoleEntryStore extends ConsoleEntryStore {
|
|
@@ -76,6 +76,11 @@ function createMemoryConsoleEntryStore(maxEntries = DEFAULT_MAX_ENTRIES) {
|
|
|
76
76
|
return consumeSync(ticket);
|
|
77
77
|
});
|
|
78
78
|
},
|
|
79
|
+
healthCheck() {
|
|
80
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
},
|
|
79
84
|
};
|
|
80
85
|
}
|
|
81
86
|
const defaultStore = createMemoryConsoleEntryStore();
|
|
@@ -179,6 +184,17 @@ function createRedisConsoleEntryStore(options) {
|
|
|
179
184
|
return decryptRecord(value, key);
|
|
180
185
|
});
|
|
181
186
|
},
|
|
187
|
+
healthCheck() {
|
|
188
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
189
|
+
try {
|
|
190
|
+
const result = yield options.client.eval("return redis.call('PING')", 0);
|
|
191
|
+
return String(result).toUpperCase() === 'PONG';
|
|
192
|
+
}
|
|
193
|
+
catch (_a) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
},
|
|
182
198
|
};
|
|
183
199
|
}
|
|
184
200
|
//# sourceMappingURL=consoleEntry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consoleEntry.js","sourceRoot":"","sources":["../../../src/accessi-module/security/consoleEntry.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';\n\n/**\n * Ticket di ingresso alla console, monouso e a breve scadenza.\n *\n * Serve a passare una sessione gia autenticata (frontend che possiede un JWT Accessi)\n * alla console amministrativa senza reinserire le credenziali e senza mettere il JWT\n * nell'URL: il frontend scambia il proprio JWT con un ticket opaco, apre la console\n * con `#entry=<ticket>` e la console lo scambia con il token originale.\n *\n * Il ticket e valido una sola volta, per pochi secondi, ed e legato all'utente che\n * lo ha richiesto (che deve avere accesso alla console). Lo store di default e in\n * memoria; in multi-istanza usare `createRedisConsoleEntryStore`.\n */\n\nexport interface ConsoleEntryRecord {\n token: string;\n codiceUtente: number;\n}\n\nexport interface ConsoleEntryTicket {\n ticket: string;\n expiresInSeconds: number;\n}\n\n/** Store pluggable: memoria per single instance, Redis per multi-istanza. */\nexport interface ConsoleEntryStore {\n issue(record: ConsoleEntryRecord, ttlMs: number): Promise<ConsoleEntryTicket>;\n consume(ticket: string): Promise<ConsoleEntryRecord | null>;\n}\n\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_ENTRIES = 5000;\nconst TICKET_PATTERN = /^[a-f0-9]{64}$/i;\n\ninterface StoredEntry extends ConsoleEntryRecord {\n expiresAt: number;\n}\n\n/** Store in memoria con API sincrona per retrocompatibilita e API async per l'interfaccia. */\nexport interface MemoryConsoleEntryStore extends ConsoleEntryStore {\n issueSync(record: ConsoleEntryRecord, ttlMs?: number): ConsoleEntryTicket;\n consumeSync(ticket: string): ConsoleEntryRecord | null;\n clear(): void;\n}\n\nfunction createTicket(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction normalizeTtl(ttlMs: number): number {\n return Math.max(1, Math.trunc(ttlMs));\n}\n\n/** Store in memoria: adatto a sviluppo e a una singola istanza. */\nexport function createMemoryConsoleEntryStore(\n maxEntries: number = DEFAULT_MAX_ENTRIES,\n): MemoryConsoleEntryStore {\n const entries = new Map<string, StoredEntry>();\n\n const sweep = (now: number): void => {\n for (const [ticket, record] of entries) {\n if (record.expiresAt <= now) entries.delete(ticket);\n }\n };\n\n const issueSync = (record: ConsoleEntryRecord, ttlMs: number = DEFAULT_TTL_MS): ConsoleEntryTicket => {\n // Il TTL negativo resta supportato per test e per il comportamento storico:\n // il ticket e immediatamente scaduto.\n const ttl = Math.trunc(ttlMs);\n const now = Date.now();\n sweep(now);\n\n while (entries.size >= maxEntries) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n\n const ticket = createTicket();\n entries.set(ticket, { ...record, expiresAt: now + ttl });\n return { ticket, expiresInSeconds: Math.max(0, Math.ceil(ttl / 1000)) };\n };\n\n const consumeSync = (ticket: string): ConsoleEntryRecord | null => {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const record = entries.get(ticket);\n if (!record) return null;\n entries.delete(ticket);\n if (record.expiresAt <= Date.now()) return null;\n return { token: record.token, codiceUtente: record.codiceUtente };\n };\n\n return {\n issueSync,\n consumeSync,\n clear: () => entries.clear(),\n async issue(record, ttlMs) {\n return issueSync(record, ttlMs);\n },\n async consume(ticket) {\n return consumeSync(ticket);\n },\n };\n}\n\nconst defaultStore = createMemoryConsoleEntryStore();\n\n/** Store di default in memoria, usato quando `AccessiOptions.consoleEntry.store` non e configurato. */\nexport function getDefaultConsoleEntryStore(): ConsoleEntryStore {\n return defaultStore;\n}\n\n// Funzioni storiche sincrone: mantengono il comportamento per test e consumer esistenti.\nexport function issueConsoleEntryTicket(\n record: ConsoleEntryRecord,\n ttlMs: number = DEFAULT_TTL_MS,\n): ConsoleEntryTicket {\n return defaultStore.issueSync(record, ttlMs);\n}\n\nexport function consumeConsoleEntryTicket(ticket: unknown): ConsoleEntryRecord | null {\n return typeof ticket === 'string' ? defaultStore.consumeSync(ticket) : null;\n}\n\n/** Solo per i test: azzera lo store in memoria di default. */\nexport function clearConsoleEntryTickets(): void {\n defaultStore.clear();\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface ConsoleEntryRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisConsoleEntryStoreOptions {\n client: ConsoleEntryRedisClient;\n /** Segreto per cifrare il JWT nel Redis. Non usare il JWT in chiaro. */\n encryptionKey: string | Buffer;\n /** Default: `accessi:console-entry:`. */\n keyPrefix?: string;\n /** Tentativi in caso di collisione del ticket. Default: 5. */\n maxIssueAttempts?: number;\n}\n\nconst SET_SCRIPT = \"return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2], 'NX')\";\nconst CONSUME_SCRIPT =\n \"local value = redis.call('GET', KEYS[1]) if value then redis.call('DEL', KEYS[1]) end return value\";\n\nfunction deriveEncryptionKey(secret: string | Buffer): Buffer {\n const source = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, 'utf8');\n if (source.length === 0) {\n throw new Error('Redis console entry encryptionKey non configurata.');\n }\n return source.length === 32 ? source : createHash('sha256').update(source).digest();\n}\n\nfunction encryptRecord(record: ConsoleEntryRecord, key: Buffer): string {\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const ciphertext = Buffer.concat([\n cipher.update(JSON.stringify(record), 'utf8'),\n cipher.final(),\n ]);\n const tag = cipher.getAuthTag();\n return [iv, tag, ciphertext].map((part) => part.toString('base64url')).join('.');\n}\n\nfunction decryptRecord(value: string, key: Buffer): ConsoleEntryRecord | null {\n const [ivPart, tagPart, ciphertextPart] = value.split('.');\n if (!ivPart || !tagPart || !ciphertextPart) return null;\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivPart, 'base64url'));\n decipher.setAuthTag(Buffer.from(tagPart, 'base64url'));\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(ciphertextPart, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n const parsed: unknown = JSON.parse(plaintext);\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n typeof (parsed as ConsoleEntryRecord).token !== 'string' ||\n typeof (parsed as ConsoleEntryRecord).codiceUtente !== 'number'\n ) {\n return null;\n }\n return parsed as ConsoleEntryRecord;\n } catch {\n return null;\n }\n}\n\n/**\n * Store Redis per ticket console.\n *\n * - il JWT viene cifrato con AES-256-GCM prima di essere scritto;\n * - `SET ... PX ... NX` evita collisioni;\n * - il consume usa `GET` + `DEL` atomici in Lua, quindi il ticket e monouso\n * anche con piu istanze.\n */\nexport function createRedisConsoleEntryStore(\n options: RedisConsoleEntryStoreOptions,\n): ConsoleEntryStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis console entry store: client.eval non configurato.');\n }\n\n const key = deriveEncryptionKey(options.encryptionKey);\n const keyPrefix = (options.keyPrefix ?? 'accessi:console-entry:').replace(/:+$/, ':');\n const maxAttempts = Math.max(1, options.maxIssueAttempts ?? 5);\n\n return {\n async issue(record, ttlMs) {\n const safeTtl = normalizeTtl(ttlMs);\n const value = encryptRecord(record, key);\n\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const ticket = createTicket();\n const result = await options.client.eval(\n SET_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n value,\n safeTtl,\n );\n if (result) {\n return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };\n }\n }\n\n throw new Error('Impossibile emettere un ticket console univoco.');\n },\n\n async consume(ticket) {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const value = await options.client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n );\n if (typeof value !== 'string') return null;\n return decryptRecord(value, key);\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"consoleEntry.js","sourceRoot":"","sources":["../../../src/accessi-module/security/consoleEntry.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';\n\n/**\n * Ticket di ingresso alla console, monouso e a breve scadenza.\n *\n * Serve a passare una sessione gia autenticata (frontend che possiede un JWT Accessi)\n * alla console amministrativa senza reinserire le credenziali e senza mettere il JWT\n * nell'URL: il frontend scambia il proprio JWT con un ticket opaco, apre la console\n * con `#entry=<ticket>` e la console lo scambia con il token originale.\n *\n * Il ticket e valido una sola volta, per pochi secondi, ed e legato all'utente che\n * lo ha richiesto (che deve avere accesso alla console). Lo store di default e in\n * memoria; in multi-istanza usare `createRedisConsoleEntryStore`.\n */\n\nexport interface ConsoleEntryRecord {\n token: string;\n codiceUtente: number;\n}\n\nexport interface ConsoleEntryTicket {\n ticket: string;\n expiresInSeconds: number;\n}\n\n/** Store pluggable: memoria per single instance, Redis per multi-istanza. */\nexport interface ConsoleEntryStore {\n issue(record: ConsoleEntryRecord, ttlMs: number): Promise<ConsoleEntryTicket>;\n consume(ticket: string): Promise<ConsoleEntryRecord | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_ENTRIES = 5000;\nconst TICKET_PATTERN = /^[a-f0-9]{64}$/i;\n\ninterface StoredEntry extends ConsoleEntryRecord {\n expiresAt: number;\n}\n\n/** Store in memoria con API sincrona per retrocompatibilita e API async per l'interfaccia. */\nexport interface MemoryConsoleEntryStore extends ConsoleEntryStore {\n issueSync(record: ConsoleEntryRecord, ttlMs?: number): ConsoleEntryTicket;\n consumeSync(ticket: string): ConsoleEntryRecord | null;\n clear(): void;\n}\n\nfunction createTicket(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction normalizeTtl(ttlMs: number): number {\n return Math.max(1, Math.trunc(ttlMs));\n}\n\n/** Store in memoria: adatto a sviluppo e a una singola istanza. */\nexport function createMemoryConsoleEntryStore(\n maxEntries: number = DEFAULT_MAX_ENTRIES,\n): MemoryConsoleEntryStore {\n const entries = new Map<string, StoredEntry>();\n\n const sweep = (now: number): void => {\n for (const [ticket, record] of entries) {\n if (record.expiresAt <= now) entries.delete(ticket);\n }\n };\n\n const issueSync = (record: ConsoleEntryRecord, ttlMs: number = DEFAULT_TTL_MS): ConsoleEntryTicket => {\n // Il TTL negativo resta supportato per test e per il comportamento storico:\n // il ticket e immediatamente scaduto.\n const ttl = Math.trunc(ttlMs);\n const now = Date.now();\n sweep(now);\n\n while (entries.size >= maxEntries) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n\n const ticket = createTicket();\n entries.set(ticket, { ...record, expiresAt: now + ttl });\n return { ticket, expiresInSeconds: Math.max(0, Math.ceil(ttl / 1000)) };\n };\n\n const consumeSync = (ticket: string): ConsoleEntryRecord | null => {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const record = entries.get(ticket);\n if (!record) return null;\n entries.delete(ticket);\n if (record.expiresAt <= Date.now()) return null;\n return { token: record.token, codiceUtente: record.codiceUtente };\n };\n\n return {\n issueSync,\n consumeSync,\n clear: () => entries.clear(),\n async issue(record, ttlMs) {\n return issueSync(record, ttlMs);\n },\n async consume(ticket) {\n return consumeSync(ticket);\n },\n async healthCheck() {\n return true;\n },\n };\n}\n\nconst defaultStore = createMemoryConsoleEntryStore();\n\n/** Store di default in memoria, usato quando `AccessiOptions.consoleEntry.store` non e configurato. */\nexport function getDefaultConsoleEntryStore(): ConsoleEntryStore {\n return defaultStore;\n}\n\n// Funzioni storiche sincrone: mantengono il comportamento per test e consumer esistenti.\nexport function issueConsoleEntryTicket(\n record: ConsoleEntryRecord,\n ttlMs: number = DEFAULT_TTL_MS,\n): ConsoleEntryTicket {\n return defaultStore.issueSync(record, ttlMs);\n}\n\nexport function consumeConsoleEntryTicket(ticket: unknown): ConsoleEntryRecord | null {\n return typeof ticket === 'string' ? defaultStore.consumeSync(ticket) : null;\n}\n\n/** Solo per i test: azzera lo store in memoria di default. */\nexport function clearConsoleEntryTickets(): void {\n defaultStore.clear();\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface ConsoleEntryRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisConsoleEntryStoreOptions {\n client: ConsoleEntryRedisClient;\n /** Segreto per cifrare il JWT nel Redis. Non usare il JWT in chiaro. */\n encryptionKey: string | Buffer;\n /** Default: `accessi:console-entry:`. */\n keyPrefix?: string;\n /** Tentativi in caso di collisione del ticket. Default: 5. */\n maxIssueAttempts?: number;\n}\n\nconst SET_SCRIPT = \"return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2], 'NX')\";\nconst CONSUME_SCRIPT =\n \"local value = redis.call('GET', KEYS[1]) if value then redis.call('DEL', KEYS[1]) end return value\";\n\nfunction deriveEncryptionKey(secret: string | Buffer): Buffer {\n const source = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, 'utf8');\n if (source.length === 0) {\n throw new Error('Redis console entry encryptionKey non configurata.');\n }\n return source.length === 32 ? source : createHash('sha256').update(source).digest();\n}\n\nfunction encryptRecord(record: ConsoleEntryRecord, key: Buffer): string {\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const ciphertext = Buffer.concat([\n cipher.update(JSON.stringify(record), 'utf8'),\n cipher.final(),\n ]);\n const tag = cipher.getAuthTag();\n return [iv, tag, ciphertext].map((part) => part.toString('base64url')).join('.');\n}\n\nfunction decryptRecord(value: string, key: Buffer): ConsoleEntryRecord | null {\n const [ivPart, tagPart, ciphertextPart] = value.split('.');\n if (!ivPart || !tagPart || !ciphertextPart) return null;\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivPart, 'base64url'));\n decipher.setAuthTag(Buffer.from(tagPart, 'base64url'));\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(ciphertextPart, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n const parsed: unknown = JSON.parse(plaintext);\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n typeof (parsed as ConsoleEntryRecord).token !== 'string' ||\n typeof (parsed as ConsoleEntryRecord).codiceUtente !== 'number'\n ) {\n return null;\n }\n return parsed as ConsoleEntryRecord;\n } catch {\n return null;\n }\n}\n\n/**\n * Store Redis per ticket console.\n *\n * - il JWT viene cifrato con AES-256-GCM prima di essere scritto;\n * - `SET ... PX ... NX` evita collisioni;\n * - il consume usa `GET` + `DEL` atomici in Lua, quindi il ticket e monouso\n * anche con piu istanze.\n */\nexport function createRedisConsoleEntryStore(\n options: RedisConsoleEntryStoreOptions,\n): ConsoleEntryStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis console entry store: client.eval non configurato.');\n }\n\n const key = deriveEncryptionKey(options.encryptionKey);\n const keyPrefix = (options.keyPrefix ?? 'accessi:console-entry:').replace(/:+$/, ':');\n const maxAttempts = Math.max(1, options.maxIssueAttempts ?? 5);\n\n return {\n async issue(record, ttlMs) {\n const safeTtl = normalizeTtl(ttlMs);\n const value = encryptRecord(record, key);\n\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const ticket = createTicket();\n const result = await options.client.eval(\n SET_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n value,\n safeTtl,\n );\n if (result) {\n return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };\n }\n }\n\n throw new Error('Impossibile emettere un ticket console univoco.');\n },\n\n async consume(ticket) {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const value = await options.client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n );\n if (typeof value !== 'string') return null;\n return decryptRecord(value, key);\n },\n\n async healthCheck() {\n try {\n const result = await options.client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n"]}
|
|
@@ -12,6 +12,8 @@ export interface AccessiEdgeRateLimitOptions {
|
|
|
12
12
|
store?: RateLimitStore;
|
|
13
13
|
/** Prefisso delle chiavi. Default: `edge`. */
|
|
14
14
|
keyPrefix?: string;
|
|
15
|
+
/** Audit strutturato degli eventi di blocco. Default: true. */
|
|
16
|
+
audit?: boolean;
|
|
15
17
|
/**
|
|
16
18
|
* Decide se applicare il limite alla richiesta.
|
|
17
19
|
*
|
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.createAccessiEdgeRateLimitMiddleware = createAccessiEdgeRateLimitMiddleware;
|
|
13
|
+
const audit_1 = require("./audit");
|
|
13
14
|
const rateLimitStore_1 = require("./rateLimitStore");
|
|
14
15
|
const DEFAULT_WINDOW_MS = 60000;
|
|
15
16
|
const DEFAULT_MAX_ATTEMPTS = 300;
|
|
@@ -42,6 +43,13 @@ function createAccessiEdgeRateLimitMiddleware(options = {}) {
|
|
|
42
43
|
try {
|
|
43
44
|
const decision = yield store.consume(`${keyPrefix}|ip|${ip}`, windowMs, maxAttempts);
|
|
44
45
|
if (!decision.allowed) {
|
|
46
|
+
if (options.audit !== false) {
|
|
47
|
+
(0, audit_1.auditAccessi)('edge_rate_limited', {
|
|
48
|
+
ip,
|
|
49
|
+
path: req.path,
|
|
50
|
+
retryAfterSeconds: decision.retryAfterSeconds,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
45
53
|
res.setHeader('Retry-After', String(decision.retryAfterSeconds));
|
|
46
54
|
res.status(429).json({
|
|
47
55
|
severity: 'error',
|
|
@@ -56,6 +64,9 @@ function createAccessiEdgeRateLimitMiddleware(options = {}) {
|
|
|
56
64
|
next();
|
|
57
65
|
}
|
|
58
66
|
catch (_d) {
|
|
67
|
+
if (options.audit !== false) {
|
|
68
|
+
(0, audit_1.auditAccessi)('rate_limit_unavailable', { ip, path: req.path });
|
|
69
|
+
}
|
|
59
70
|
res.status(503).json({
|
|
60
71
|
severity: 'error',
|
|
61
72
|
status: 503,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edgeRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/edgeRateLimit.ts"],"names":[],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"edgeRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/edgeRateLimit.ts"],"names":[],"mappings":"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","sourcesContent":["import type { NextFunction, Request, Response } from 'express';\nimport { auditAccessi } from './audit';\nimport { getDefaultRateLimitStore } from './rateLimitStore';\nimport type { RateLimitStore } from './rateLimitStore';\n\n/** Opzioni del limite grossolano per IP applicato davanti alle rotte Accessi. */\nexport interface AccessiEdgeRateLimitOptions {\n /** Default: true quando l'oggetto viene configurato. */\n enabled?: boolean;\n /** Finestra del limite. Default: 60000 ms. */\n windowMs?: number;\n /** Richieste consentite per IP nella finestra. Default: 300. */\n maxAttempts?: number;\n /** Store condiviso. Default: memoria. */\n store?: RateLimitStore;\n /** Prefisso delle chiavi. Default: `edge`. */\n keyPrefix?: string;\n /** Audit strutturato degli eventi di blocco. Default: true. */\n audit?: boolean;\n /**\n * Decide se applicare il limite alla richiesta.\n *\n * Default: `/api` e `/api/*`. Il bootstrap Accessi passa un matcher che include\n * anche OpenAPI e JWKS.\n */\n match?: (path: string, req: Request) => boolean;\n}\n\nexport type AccessiEdgeRateLimitMiddleware = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => Promise<void>;\n\nconst DEFAULT_WINDOW_MS = 60_000;\nconst DEFAULT_MAX_ATTEMPTS = 300;\n\nfunction defaultMatch(path: string): boolean {\n return path === '/api' || path.startsWith('/api/');\n}\n\n/**\n * Middleware edge per Accessi Service.\n *\n * Non sostituisce il rate limit applicativo per account: aggiunge una soglia\n * grossolana per IP, utile contro bot e floods prima che la richiesta raggiunga\n * controller, database o introspection.\n */\nexport function createAccessiEdgeRateLimitMiddleware(\n options: AccessiEdgeRateLimitOptions = {},\n): AccessiEdgeRateLimitMiddleware | undefined {\n if (options.enabled === false) return undefined;\n\n const windowMs = Math.max(1, Math.trunc(options.windowMs ?? DEFAULT_WINDOW_MS));\n const maxAttempts = Math.max(1, Math.trunc(options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS));\n const store = options.store ?? getDefaultRateLimitStore();\n const keyPrefix = (options.keyPrefix ?? 'edge').replace(/\\|+$/, '');\n const match = options.match ?? defaultMatch;\n\n return async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n if (!match(req.path, req)) {\n next();\n return;\n }\n\n const ip = req.ip ?? req.socket?.remoteAddress ?? 'unknown';\n try {\n const decision = await store.consume(\n `${keyPrefix}|ip|${ip}`,\n windowMs,\n maxAttempts,\n );\n if (!decision.allowed) {\n if (options.audit !== false) {\n auditAccessi('edge_rate_limited', {\n ip,\n path: req.path,\n retryAfterSeconds: decision.retryAfterSeconds,\n });\n }\n res.setHeader('Retry-After', String(decision.retryAfterSeconds));\n res.status(429).json({\n severity: 'error',\n status: 429,\n statusCode: 2,\n code: 'ACCESSI_EDGE_RATE_LIMITED',\n error: 'ACCESSI_EDGE_RATE_LIMITED',\n message: 'Troppe richieste. Riprova piu tardi.',\n });\n return;\n }\n next();\n } catch {\n if (options.audit !== false) {\n auditAccessi('rate_limit_unavailable', { ip, path: req.path });\n }\n res.status(503).json({\n severity: 'error',\n status: 503,\n statusCode: 2,\n code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n error: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n message: 'Rate limit non disponibile. Riprova piu tardi.',\n });\n }\n };\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { HttpAdapterHost } from '@nestjs/core';
|
|
3
|
+
import type { AccessiOptions } from '../AccessiModule';
|
|
4
|
+
/**
|
|
5
|
+
* Monta automaticamente la JWKS sull'Express adapter di Nest.
|
|
6
|
+
*
|
|
7
|
+
* Serve sia ai bootstrap standalone sia a `AccessiModule.forRoot` in un'app
|
|
8
|
+
* Nest ospitante, senza richiedere una chiamata manuale a `setupAccessiJwks`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class AccessiJwksBootstrapService implements OnModuleInit {
|
|
11
|
+
private readonly options;
|
|
12
|
+
private readonly adapterHost;
|
|
13
|
+
constructor(options: AccessiOptions, adapterHost: HttpAdapterHost);
|
|
14
|
+
onModuleInit(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.AccessiJwksBootstrapService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const core_1 = require("@nestjs/core");
|
|
18
|
+
const jwks_1 = require("./jwks");
|
|
19
|
+
/**
|
|
20
|
+
* Monta automaticamente la JWKS sull'Express adapter di Nest.
|
|
21
|
+
*
|
|
22
|
+
* Serve sia ai bootstrap standalone sia a `AccessiModule.forRoot` in un'app
|
|
23
|
+
* Nest ospitante, senza richiedere una chiamata manuale a `setupAccessiJwks`.
|
|
24
|
+
*/
|
|
25
|
+
let AccessiJwksBootstrapService = class AccessiJwksBootstrapService {
|
|
26
|
+
constructor(options, adapterHost) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.adapterHost = adapterHost;
|
|
29
|
+
}
|
|
30
|
+
onModuleInit() {
|
|
31
|
+
var _a, _b;
|
|
32
|
+
const instance = (_b = (_a = this.adapterHost.httpAdapter) === null || _a === void 0 ? void 0 : _a.getInstance) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
33
|
+
if (instance && typeof instance.get === 'function') {
|
|
34
|
+
(0, jwks_1.setupAccessiJwks)(instance, this.options);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
exports.AccessiJwksBootstrapService = AccessiJwksBootstrapService;
|
|
39
|
+
exports.AccessiJwksBootstrapService = AccessiJwksBootstrapService = __decorate([
|
|
40
|
+
(0, common_1.Injectable)(),
|
|
41
|
+
__param(0, (0, common_1.Inject)('ACCESSI_OPTIONS')),
|
|
42
|
+
__metadata("design:paramtypes", [Object, core_1.HttpAdapterHost])
|
|
43
|
+
], AccessiJwksBootstrapService);
|
|
44
|
+
//# sourceMappingURL=jwksBootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwksBootstrap.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwksBootstrap.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAkE;AAClE,uCAA+C;AAE/C,iCAA0C;AAG1C;;;;;GAKG;AAEI,IAAM,2BAA2B,GAAjC,MAAM,2BAA2B;IACtC,YAC8C,OAAuB,EAClD,WAA4B;QADD,YAAO,GAAP,OAAO,CAAgB;QAClD,gBAAW,GAAX,WAAW,CAAiB;IAC5C,CAAC;IAEJ,YAAY;;QACV,MAAM,QAAQ,GAAY,MAAA,MAAA,IAAI,CAAC,WAAW,CAAC,WAAW,0CAAE,WAAW,kDAAI,CAAC;QACxE,IAAI,QAAQ,IAAI,OAAQ,QAAkC,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;YAC9E,IAAA,uBAAgB,EAAC,QAAiC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;CACF,CAAA;AAZY,kEAA2B;sCAA3B,2BAA2B;IADvC,IAAA,mBAAU,GAAE;IAGR,WAAA,IAAA,eAAM,EAAC,iBAAiB,CAAC,CAAA;6CACI,sBAAe;GAHpC,2BAA2B,CAYvC","sourcesContent":["import { Inject, Injectable, OnModuleInit } from '@nestjs/common';\nimport { HttpAdapterHost } from '@nestjs/core';\nimport type { AccessiOptions } from '../AccessiModule';\nimport { setupAccessiJwks } from './jwks';\nimport type { AccessiJwksExpressApp } from './jwks';\n\n/**\n * Monta automaticamente la JWKS sull'Express adapter di Nest.\n *\n * Serve sia ai bootstrap standalone sia a `AccessiModule.forRoot` in un'app\n * Nest ospitante, senza richiedere una chiamata manuale a `setupAccessiJwks`.\n */\n@Injectable()\nexport class AccessiJwksBootstrapService implements OnModuleInit {\n constructor(\n @Inject('ACCESSI_OPTIONS') private readonly options: AccessiOptions,\n private readonly adapterHost: HttpAdapterHost,\n ) {}\n\n onModuleInit(): void {\n const instance: unknown = this.adapterHost.httpAdapter?.getInstance?.();\n if (instance && typeof (instance as AccessiJwksExpressApp).get === 'function') {\n setupAccessiJwks(instance as AccessiJwksExpressApp, this.options);\n }\n }\n}\n"]}
|
|
@@ -2,14 +2,21 @@ import type * as jwt from "jsonwebtoken";
|
|
|
2
2
|
import type { AccessiOptions } from "../AccessiModule";
|
|
3
3
|
/** Algoritmi supportati per gli access token Accessi. */
|
|
4
4
|
export type AccessiAccessTokenAlgorithm = "HS256" | "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "ES256" | "ES384" | "ES512";
|
|
5
|
-
/** Configurazione di firma: chiave, algoritmo e claim standard. */
|
|
6
5
|
export declare function getAccessiAccessTokenSigningConfig(options: AccessiOptions): {
|
|
7
6
|
key: jwt.Secret;
|
|
8
7
|
algorithm: AccessiAccessTokenAlgorithm;
|
|
9
8
|
signOptions: Pick<jwt.SignOptions, "issuer" | "audience" | "keyid">;
|
|
10
9
|
};
|
|
11
|
-
/** Configurazione di verifica: chiave pubblica per RS/ES, segreto derivato per HS. */
|
|
12
10
|
export declare function getAccessiAccessTokenVerificationConfig(options: AccessiOptions): {
|
|
13
11
|
key: jwt.Secret;
|
|
14
12
|
verifyOptions: jwt.VerifyOptions;
|
|
15
13
|
};
|
|
14
|
+
export declare function getAccessiRefreshTokenSigningConfig(options: AccessiOptions): {
|
|
15
|
+
key: jwt.Secret;
|
|
16
|
+
algorithm: AccessiAccessTokenAlgorithm;
|
|
17
|
+
signOptions: Pick<jwt.SignOptions, "issuer" | "audience" | "keyid">;
|
|
18
|
+
};
|
|
19
|
+
export declare function getAccessiRefreshTokenVerificationConfig(options: AccessiOptions): {
|
|
20
|
+
key: jwt.Secret;
|
|
21
|
+
verifyOptions: jwt.VerifyOptions;
|
|
22
|
+
};
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getAccessiAccessTokenSigningConfig = getAccessiAccessTokenSigningConfig;
|
|
4
4
|
exports.getAccessiAccessTokenVerificationConfig = getAccessiAccessTokenVerificationConfig;
|
|
5
|
+
exports.getAccessiRefreshTokenSigningConfig = getAccessiRefreshTokenSigningConfig;
|
|
6
|
+
exports.getAccessiRefreshTokenVerificationConfig = getAccessiRefreshTokenVerificationConfig;
|
|
5
7
|
const passwordResetToken_1 = require("./passwordResetToken");
|
|
6
8
|
function resolveAlgorithm(options) {
|
|
7
9
|
var _a;
|
|
8
10
|
return (_a = options.jwtOptions.algorithm) !== null && _a !== void 0 ? _a : "HS256";
|
|
9
11
|
}
|
|
10
12
|
/** Configurazione di firma: chiave, algoritmo e claim standard. */
|
|
11
|
-
function
|
|
13
|
+
function getTokenSigningConfig(options, purpose) {
|
|
12
14
|
const algorithm = resolveAlgorithm(options);
|
|
13
15
|
const key = algorithm.startsWith("HS")
|
|
14
|
-
? (0, passwordResetToken_1.getAccessiJwtSecret)(options)
|
|
16
|
+
? (0, passwordResetToken_1.getAccessiJwtSecret)(options, purpose)
|
|
15
17
|
: options.jwtOptions.privateKey;
|
|
16
18
|
if (!key) {
|
|
17
19
|
throw new Error(`jwtOptions.privateKey e obbligatoria per l'algoritmo ${algorithm}.`);
|
|
@@ -25,11 +27,11 @@ function getAccessiAccessTokenSigningConfig(options) {
|
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
29
|
/** Configurazione di verifica: chiave pubblica per RS/ES, segreto derivato per HS. */
|
|
28
|
-
function
|
|
30
|
+
function getTokenVerificationConfig(options, purpose) {
|
|
29
31
|
var _a;
|
|
30
32
|
const algorithm = resolveAlgorithm(options);
|
|
31
33
|
const key = algorithm.startsWith("HS")
|
|
32
|
-
? (0, passwordResetToken_1.getAccessiJwtSecret)(options)
|
|
34
|
+
? (0, passwordResetToken_1.getAccessiJwtSecret)(options, purpose)
|
|
33
35
|
: ((_a = options.jwtOptions.publicKey) !== null && _a !== void 0 ? _a : options.jwtOptions.privateKey);
|
|
34
36
|
if (!key) {
|
|
35
37
|
throw new Error(`jwtOptions.publicKey e obbligatoria per verificare token ${algorithm}.`);
|
|
@@ -41,4 +43,16 @@ function getAccessiAccessTokenVerificationConfig(options) {
|
|
|
41
43
|
: {})),
|
|
42
44
|
};
|
|
43
45
|
}
|
|
46
|
+
function getAccessiAccessTokenSigningConfig(options) {
|
|
47
|
+
return getTokenSigningConfig(options, "access");
|
|
48
|
+
}
|
|
49
|
+
function getAccessiAccessTokenVerificationConfig(options) {
|
|
50
|
+
return getTokenVerificationConfig(options, "access");
|
|
51
|
+
}
|
|
52
|
+
function getAccessiRefreshTokenSigningConfig(options) {
|
|
53
|
+
return getTokenSigningConfig(options, "refresh");
|
|
54
|
+
}
|
|
55
|
+
function getAccessiRefreshTokenVerificationConfig(options) {
|
|
56
|
+
return getTokenVerificationConfig(options, "refresh");
|
|
57
|
+
}
|
|
44
58
|
//# sourceMappingURL=jwtTokenOptions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwtTokenOptions.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwtTokenOptions.ts"],"names":[],"mappings":";;
|
|
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"]}
|
|
@@ -3,7 +3,7 @@ import type { AccessiOptions } from '../AccessiModule';
|
|
|
3
3
|
/** Durata predefinita del token di reset. La monouso e garantita anche dal nonce persistito in `UTENTI.KEYREG`. */
|
|
4
4
|
export declare const DEFAULT_PASSWORD_RESET_EXPIRES_IN = "1h";
|
|
5
5
|
/** Scopo della chiave derivata: separa le chiavi di access token, reset password e 2FA. */
|
|
6
|
-
export type AccessiJwtPurpose = 'access' | 'reset' | '2fa';
|
|
6
|
+
export type AccessiJwtPurpose = 'access' | 'refresh' | 'reset' | '2fa';
|
|
7
7
|
/**
|
|
8
8
|
* Deriva una chiave JWT per scopo dal segreto configurato (key separation senza nuova configurazione).
|
|
9
9
|
* Un leak della chiave di reset non compromette gli access token e viceversa.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"passwordResetToken.js","sourceRoot":"","sources":["../../../src/accessi-module/security/passwordResetToken.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBA,kDAOC;AAGD,4DAqBC;AAGD,4DAeC;AApED,kDAAoC;AACpC,mCAAoC;AAGpC,mHAAmH;AACtG,QAAA,iCAAiC,GAAG,IAAI,CAAC;AAUtD;;;GAGG;AACH,SAAgB,mBAAmB,CAAC,OAAuB,EAAE,UAA6B,QAAQ;;IAChG,MAAM,MAAM,GAAG,CAAA,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU,0CAAE,MAAM,KAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACzE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;IAED,OAAO,IAAA,mBAAU,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,eAAe,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxF,CAAC;AAED,oHAAoH;AACpH,SAAgB,wBAAwB,CACtC,YAAoB,EACpB,KAAa,EACb,MAAc,EACd,YAA0C,yCAAiC;IAE3E,IAAI,CAAC,YAAY,IAAI,CAAC,KAAK,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CACb;QACE,KAAK;QACL,GAAG,EAAE,gBAAgB;KACtB,EACD,MAAM,EACN;QACE,OAAO,EAAE,GAAG,YAAY,EAAE;QAC1B,SAAS;KACV,CACF,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,SAAgB,wBAAwB,CACtC,KAAa,EACb,MAAc;IAEd,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAA8B,CAAC;IAClG,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG,CAAC,CAAC;IAE1C,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG,MAAK,gBAAgB,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,IAAI,YAAY,IAAI,CAAC,IAAI,OAAO,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,KAAK,CAAA,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC1J,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAChD,CAAC;IAED,OAAO;QACL,YAAY;QACZ,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC;AACJ,CAAC","sourcesContent":["import * as jwt from 'jsonwebtoken';\nimport { createHmac } from 'crypto';\nimport type { AccessiOptions } from '../AccessiModule';\n\n/** Durata predefinita del token di reset. La monouso e garantita anche dal nonce persistito in `UTENTI.KEYREG`. */\nexport const DEFAULT_PASSWORD_RESET_EXPIRES_IN = '1h';\n\ninterface PasswordResetTokenPayload extends jwt.JwtPayload {\n nonce: string;\n typ: 'password-reset';\n}\n\n/** Scopo della chiave derivata: separa le chiavi di access token, reset password e 2FA. */\nexport type AccessiJwtPurpose = 'access' | 'reset' | '2fa';\n\n/**\n * Deriva una chiave JWT per scopo dal segreto configurato (key separation senza nuova configurazione).\n * Un leak della chiave di reset non compromette gli access token e viceversa.\n */\nexport function getAccessiJwtSecret(options: AccessiOptions, purpose: AccessiJwtPurpose = 'access'): string {\n const secret = options?.jwtOptions?.secret || process.env.ACC_JWT_SECRET;\n if (!secret) {\n throw new Error('JWT secret non configurato.');\n }\n\n return createHmac('sha256', secret).update(`accessi:jwt:${purpose}:v1`).digest('hex');\n}\n\n/** Crea un token firmato contenente solo codice utente e nonce; non inserire email o dati personali nelle claim. */\nexport function createPasswordResetToken(\n codiceUtente: number,\n nonce: string,\n secret: string,\n expiresIn: jwt.SignOptions['expiresIn'] = DEFAULT_PASSWORD_RESET_EXPIRES_IN,\n): string {\n if (!codiceUtente || !nonce) {\n throw new Error('Dati insufficienti per generare il reset token.');\n }\n\n return jwt.sign(\n {\n nonce,\n typ: 'password-reset',\n },\n secret,\n {\n subject: `${codiceUtente}`,\n expiresIn,\n },\n );\n}\n\n/** Verifica firma e scadenza, quindi restituisce i dati minimi necessari al reset. */\nexport function verifyPasswordResetToken(\n token: string,\n secret: string,\n): { codiceUtente: number; nonce: string } {\n const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] }) as PasswordResetTokenPayload;\n const codiceUtente = Number(decoded?.sub);\n\n if (decoded?.typ !== 'password-reset' || !Number.isSafeInteger(codiceUtente) || codiceUtente <= 0 || typeof decoded?.nonce !== 'string' || !decoded.nonce) {\n throw new Error('Token di reset non valido.');\n }\n\n return {\n codiceUtente,\n nonce: decoded.nonce,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"passwordResetToken.js","sourceRoot":"","sources":["../../../src/accessi-module/security/passwordResetToken.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmBA,kDAOC;AAGD,4DAqBC;AAGD,4DAeC;AApED,kDAAoC;AACpC,mCAAoC;AAGpC,mHAAmH;AACtG,QAAA,iCAAiC,GAAG,IAAI,CAAC;AAUtD;;;GAGG;AACH,SAAgB,mBAAmB,CAAC,OAAuB,EAAE,UAA6B,QAAQ;;IAChG,MAAM,MAAM,GAAG,CAAA,MAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,UAAU,0CAAE,MAAM,KAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACzE,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;IAED,OAAO,IAAA,mBAAU,EAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,eAAe,OAAO,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACxF,CAAC;AAED,oHAAoH;AACpH,SAAgB,wBAAwB,CACtC,YAAoB,EACpB,KAAa,EACb,MAAc,EACd,YAA0C,yCAAiC;IAE3E,IAAI,CAAC,YAAY,IAAI,CAAC,KAAK,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACrE,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CACb;QACE,KAAK;QACL,GAAG,EAAE,gBAAgB;KACtB,EACD,MAAM,EACN;QACE,OAAO,EAAE,GAAG,YAAY,EAAE;QAC1B,SAAS;KACV,CACF,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,SAAgB,wBAAwB,CACtC,KAAa,EACb,MAAc;IAEd,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAA8B,CAAC;IAClG,MAAM,YAAY,GAAG,MAAM,CAAC,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG,CAAC,CAAC;IAE1C,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,GAAG,MAAK,gBAAgB,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,IAAI,YAAY,IAAI,CAAC,IAAI,OAAO,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,KAAK,CAAA,KAAK,QAAQ,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC1J,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAChD,CAAC;IAED,OAAO;QACL,YAAY;QACZ,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC;AACJ,CAAC","sourcesContent":["import * as jwt from 'jsonwebtoken';\nimport { createHmac } from 'crypto';\nimport type { AccessiOptions } from '../AccessiModule';\n\n/** Durata predefinita del token di reset. La monouso e garantita anche dal nonce persistito in `UTENTI.KEYREG`. */\nexport const DEFAULT_PASSWORD_RESET_EXPIRES_IN = '1h';\n\ninterface PasswordResetTokenPayload extends jwt.JwtPayload {\n nonce: string;\n typ: 'password-reset';\n}\n\n/** Scopo della chiave derivata: separa le chiavi di access token, reset password e 2FA. */\nexport type AccessiJwtPurpose = 'access' | 'refresh' | 'reset' | '2fa';\n\n/**\n * Deriva una chiave JWT per scopo dal segreto configurato (key separation senza nuova configurazione).\n * Un leak della chiave di reset non compromette gli access token e viceversa.\n */\nexport function getAccessiJwtSecret(options: AccessiOptions, purpose: AccessiJwtPurpose = 'access'): string {\n const secret = options?.jwtOptions?.secret || process.env.ACC_JWT_SECRET;\n if (!secret) {\n throw new Error('JWT secret non configurato.');\n }\n\n return createHmac('sha256', secret).update(`accessi:jwt:${purpose}:v1`).digest('hex');\n}\n\n/** Crea un token firmato contenente solo codice utente e nonce; non inserire email o dati personali nelle claim. */\nexport function createPasswordResetToken(\n codiceUtente: number,\n nonce: string,\n secret: string,\n expiresIn: jwt.SignOptions['expiresIn'] = DEFAULT_PASSWORD_RESET_EXPIRES_IN,\n): string {\n if (!codiceUtente || !nonce) {\n throw new Error('Dati insufficienti per generare il reset token.');\n }\n\n return jwt.sign(\n {\n nonce,\n typ: 'password-reset',\n },\n secret,\n {\n subject: `${codiceUtente}`,\n expiresIn,\n },\n );\n}\n\n/** Verifica firma e scadenza, quindi restituisce i dati minimi necessari al reset. */\nexport function verifyPasswordResetToken(\n token: string,\n secret: string,\n): { codiceUtente: number; nonce: string } {\n const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] }) as PasswordResetTokenPayload;\n const codiceUtente = Number(decoded?.sub);\n\n if (decoded?.typ !== 'password-reset' || !Number.isSafeInteger(codiceUtente) || codiceUtente <= 0 || typeof decoded?.nonce !== 'string' || !decoded.nonce) {\n throw new Error('Token di reset non valido.');\n }\n\n return {\n codiceUtente,\n nonce: decoded.nonce,\n };\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Request, Response } from 'express';
|
|
2
2
|
import type { AccessiOptions } from '../AccessiModule';
|
|
3
3
|
import type { RateLimitDecision } from './rateLimitStore';
|
|
4
|
-
export type PublicAuthRateLimitScope = 'login' | 'twoFactorVerify' | 'twoFactorResend' | 'register' | 'passwordResetEmail' | 'passwordResetConfirm' | 'getUserByToken' | 'adminBootstrap' | 'consoleEntry';
|
|
4
|
+
export type PublicAuthRateLimitScope = 'login' | 'twoFactorVerify' | 'twoFactorResend' | 'register' | 'passwordResetEmail' | 'passwordResetConfirm' | 'refresh' | 'getUserByToken' | 'adminBootstrap' | 'consoleEntry';
|
|
5
5
|
/**
|
|
6
6
|
* Applica il rate limit per IP e, opzionalmente, per soggetto.
|
|
7
7
|
*
|
|
@@ -21,6 +21,7 @@ const DEFAULT_PUBLIC_AUTH_RATE_LIMITS = {
|
|
|
21
21
|
register: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },
|
|
22
22
|
passwordResetEmail: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },
|
|
23
23
|
passwordResetConfirm: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },
|
|
24
|
+
refresh: { windowMs: 5 * 60 * 1000, maxAttempts: 60 },
|
|
24
25
|
getUserByToken: { windowMs: 5 * 60 * 1000, maxAttempts: 30 },
|
|
25
26
|
adminBootstrap: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },
|
|
26
27
|
consoleEntry: { windowMs: 5 * 60 * 1000, maxAttempts: 30 },
|
|
@@ -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';\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 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 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"]}
|
|
@@ -6,6 +6,8 @@ export interface RateLimitDecision {
|
|
|
6
6
|
/** Store pluggable per rate limit: memoria per singola istanza, Redis per cluster. */
|
|
7
7
|
export interface RateLimitStore {
|
|
8
8
|
consume(key: string, windowMs: number, maxAttempts: number): Promise<RateLimitDecision>;
|
|
9
|
+
/** Health check opzionale per readiness del servizio. */
|
|
10
|
+
healthCheck?(): Promise<boolean>;
|
|
9
11
|
}
|
|
10
12
|
export interface MemoryRateLimitStore extends RateLimitStore {
|
|
11
13
|
reset(): void;
|
|
@@ -76,6 +76,11 @@ function createMemoryRateLimitStore(maxBuckets = DEFAULT_MAX_BUCKETS) {
|
|
|
76
76
|
reset() {
|
|
77
77
|
buckets.clear();
|
|
78
78
|
},
|
|
79
|
+
healthCheck() {
|
|
80
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
},
|
|
79
84
|
};
|
|
80
85
|
}
|
|
81
86
|
const defaultStore = createMemoryRateLimitStore();
|
|
@@ -124,6 +129,17 @@ function createRedisRateLimitStore(options) {
|
|
|
124
129
|
};
|
|
125
130
|
});
|
|
126
131
|
},
|
|
132
|
+
healthCheck() {
|
|
133
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
134
|
+
try {
|
|
135
|
+
const result = yield options.client.eval("return redis.call('PING')", 0);
|
|
136
|
+
return String(result).toUpperCase() === 'PONG';
|
|
137
|
+
}
|
|
138
|
+
catch (_a) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
},
|
|
127
143
|
};
|
|
128
144
|
}
|
|
129
145
|
//# sourceMappingURL=rateLimitStore.js.map
|