emilsoftware-utilities 2.0.0-dev.67 → 2.0.0-dev.68
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-module/AccessiModule.d.ts +27 -1
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/console.js +12 -6
- package/dist/accessi-module/Console/wiki.js +9 -3
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Controllers/AuthController.js +6 -6
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Controllers/ConsoleEntryController.d.ts +2 -0
- package/dist/accessi-module/Controllers/ConsoleEntryController.js +11 -3
- package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
- package/dist/accessi-module/Controllers/EmailController.js +1 -1
- package/dist/accessi-module/Controllers/EmailController.js.map +1 -1
- package/dist/accessi-module/Controllers/UserController.js +1 -1
- package/dist/accessi-module/Controllers/UserController.js.map +1 -1
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js +8 -16
- package/dist/accessi-module/database-updates/accessiOptionsFromEnv.js.map +1 -1
- package/dist/accessi-module/index.d.ts +9 -1
- package/dist/accessi-module/index.js +34 -4
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/security/consoleEntry.d.ts +56 -11
- package/dist/accessi-module/security/consoleEntry.js +165 -31
- package/dist/accessi-module/security/consoleEntry.js.map +1 -1
- package/dist/accessi-module/security/edgeRateLimit.d.ts +31 -0
- package/dist/accessi-module/security/edgeRateLimit.js +70 -0
- package/dist/accessi-module/security/edgeRateLimit.js.map +1 -0
- package/dist/accessi-module/security/jwks.d.ts +33 -0
- package/dist/accessi-module/security/jwks.js +83 -0
- package/dist/accessi-module/security/jwks.js.map +1 -0
- package/dist/accessi-module/security/publicAuthRateLimit.d.ts +8 -6
- package/dist/accessi-module/security/publicAuthRateLimit.js +54 -78
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
- package/dist/accessi-module/security/rateLimitStore.d.ts +32 -0
- package/dist/accessi-module/security/rateLimitStore.js +129 -0
- package/dist/accessi-module/security/rateLimitStore.js.map +1 -0
- package/dist/accessi-module/security/serviceTokenGuard.d.ts +3 -1
- package/dist/accessi-module/security/serviceTokenGuard.js +11 -6
- package/dist/accessi-module/security/serviceTokenGuard.js.map +1 -1
- package/dist/accessi-module/security/serviceTokenRateLimit.d.ts +5 -4
- package/dist/accessi-module/security/serviceTokenRateLimit.js +33 -29
- package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.68.tgz +0 -0
- package/dist/initEmilsoftwareModule.js +20 -2
- package/dist/initEmilsoftwareModule.js.map +1 -1
- package/docs/gateway-rate-limit.md +98 -0
- package/integration-prompt.md +139 -8
- package/package.json +5 -4
- package/dist/emilsoftware-utilities-2.0.0-dev.67.tgz +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serviceTokenGuard.js","sourceRoot":"","sources":["../../../src/accessi-module/security/serviceTokenGuard.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"serviceTokenGuard.js","sourceRoot":"","sources":["../../../src/accessi-module/security/serviceTokenGuard.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,wDAMC;AA/BD,2CAQwB;AACxB,uCAAyC;AAGzC,6FAAsI;AACtI,6DAAiE;AACjE,mEAAsE;AAEtE,iEAAiE;AACpD,QAAA,6BAA6B,GAAG,8BAA8B,CAAC;AAE5E,2EAA2E;AACpE,MAAM,yBAAyB,GAAG,CAAC,GAAG,MAAgB,EAAE,EAAE,CAAC,IAAA,oBAAW,EAAC,qCAA6B,EAAE,MAAM,CAAC,CAAC;AAAxG,QAAA,yBAAyB,6BAA+E;AAIrH,iGAAiG;AACjG,SAAgB,sBAAsB,CAAC,GAAY;IACjD,MAAM,QAAQ,GAAI,GAAkC,CAAC,mBAAmB,CAAC;IACzE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,8BAAqB,CAAC,mCAAmC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AAEI,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IAC5B,YACmB,mBAAwC,EACxC,SAAoB,EACO,cAA8B;QAFzD,wBAAmB,GAAnB,mBAAmB,CAAqB;QACxC,cAAS,GAAT,SAAS,CAAW;QACO,mBAAc,GAAd,cAAc,CAAgB;IACzE,CAAC;IAEE,WAAW,CAAC,OAAyB;;;YACzC,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAW,CAAC;YAC7D,MAAM,EAAE,GAAG,MAAA,MAAA,OAAO,CAAC,EAAE,mCAAI,MAAA,OAAO,CAAC,MAAM,0CAAE,aAAa,mCAAI,SAAS,CAAC;YACpE,MAAM,IAAA,mDAA2B,EAAC,EAAE,EAAE,MAAA,IAAI,CAAC,cAAc,CAAC,aAAa,0CAAE,cAAc,CAAC,CAAC;YAEzF,MAAM,KAAK,GAAG,IAAA,8CAAyB,EAAC,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC;YAC1E,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,IAAI,8BAAqB,CAAC,6BAA6B,CAAC,CAAC;YACjE,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YACtE,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,8BAAqB,CAAC,mDAAmD,CAAC,CAAC;YACvF,CAAC;YAEA,OAAsC,CAAC,mBAAmB,GAAG,QAAQ,CAAC;YAEvE,MAAM,cAAc,GAAG,MAAA,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAW,qCAA6B,EAAE;gBAC/F,OAAO,CAAC,UAAU,EAAE;gBACpB,OAAO,CAAC,QAAQ,EAAE;aACnB,CAAC,mCAAI,EAAE,CAAC;YACT,MAAM,YAAY,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,IAAA,0CAAoB,EAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;YACnG,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,IAAI,2BAAkB,CAAC,UAAU,YAAY,qCAAqC,CAAC,CAAC;YAC5F,CAAC;YAED,OAAO,IAAI,CAAC;QACd,CAAC;KAAA;CACF,CAAA;AAnCY,8CAAiB;4BAAjB,iBAAiB;IAD7B,IAAA,mBAAU,GAAE;IAKR,WAAA,IAAA,eAAM,EAAC,iBAAiB,CAAC,CAAA;qCAFY,yCAAmB;QAC7B,gBAAS;GAH5B,iBAAiB,CAmC7B","sourcesContent":["import {\n CanActivate,\n ExecutionContext,\n ForbiddenException,\n Inject,\n Injectable,\n SetMetadata,\n UnauthorizedException,\n} from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nimport type { Request } from 'express';\nimport type { AccessiOptions } from '../AccessiModule';\nimport { ServiceTokenService, serviceTokenHasScope, VerifiedServiceToken } from '../Services/ServiceTokenService/ServiceTokenService';\nimport { extractAccessiBearerToken } from './authenticatedToken';\nimport { assertServiceTokenRateLimit } from './serviceTokenRateLimit';\n\n/** Chiave dei metadata usata da `@RequireServiceTokenScopes`. */\nexport const SERVICE_TOKEN_SCOPES_METADATA = 'accessi:service-token-scopes';\n\n/** Richiede che il token di servizio presenti tutti gli scope indicati. */\nexport const RequireServiceTokenScopes = (...scopes: string[]) => SetMetadata(SERVICE_TOKEN_SCOPES_METADATA, scopes);\n\nexport type AccessiServiceTokenRequest = Request & { accessiServiceToken?: VerifiedServiceToken };\n\n/** Restituisce il token di servizio verificato da `ServiceTokenGuard`; lancia 401 se assente. */\nexport function getAccessiServiceToken(req: Request): VerifiedServiceToken {\n const verified = (req as AccessiServiceTokenRequest).accessiServiceToken;\n if (!verified) {\n throw new UnauthorizedException('Token di servizio non verificato.');\n }\n return verified;\n}\n\n/**\n * Autentica le chiamate tecniche macchina-a-macchina tramite `Authorization: Bearer <token>`.\n *\n * Il token e per installazione/servizio, non legato a un utente. Gli scope richiesti si dichiarano\n * con `@RequireServiceTokenScopes('ia')`.\n */\n@Injectable()\nexport class ServiceTokenGuard implements CanActivate {\n constructor(\n private readonly serviceTokenService: ServiceTokenService,\n private readonly reflector: Reflector,\n @Inject('ACCESSI_OPTIONS') private readonly accessiOptions: AccessiOptions,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const request = context.switchToHttp().getRequest<Request>();\n const ip = request.ip ?? request.socket?.remoteAddress ?? 'unknown';\n await assertServiceTokenRateLimit(ip, this.accessiOptions.serviceTokens?.rateLimitStore);\n\n const token = extractAccessiBearerToken(request.headers['authorization']);\n if (!token) {\n throw new UnauthorizedException('Token di servizio mancante.');\n }\n\n const verified = await this.serviceTokenService.verify(token, { ip });\n if (!verified) {\n throw new UnauthorizedException('Token di servizio non valido, scaduto o revocato.');\n }\n\n (request as AccessiServiceTokenRequest).accessiServiceToken = verified;\n\n const requiredScopes = this.reflector.getAllAndOverride<string[]>(SERVICE_TOKEN_SCOPES_METADATA, [\n context.getHandler(),\n context.getClass(),\n ]) ?? [];\n const missingScope = requiredScopes.find((scope) => !serviceTokenHasScope(verified.scopes, scope));\n if (missingScope) {\n throw new ForbiddenException(`Scope '${missingScope}' non autorizzato per questo token.`);\n }\n\n return true;\n }\n}\n"]}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import type { RateLimitStore } from './rateLimitStore';
|
|
2
|
+
export declare function checkServiceTokenRateLimit(identifier: string, store?: RateLimitStore): Promise<{
|
|
2
3
|
allowed: boolean;
|
|
3
4
|
retryAfterSeconds: number;
|
|
4
|
-
}
|
|
5
|
-
/** Svuota i bucket (usato nei test). */
|
|
5
|
+
}>;
|
|
6
|
+
/** Svuota i bucket dello store di default (usato nei test). */
|
|
6
7
|
export declare function resetServiceTokenRateLimit(): void;
|
|
7
8
|
/** Lancia 429 con `Retry-After` quando il limite e superato. */
|
|
8
|
-
export declare function assertServiceTokenRateLimit(identifier: string): void
|
|
9
|
+
export declare function assertServiceTokenRateLimit(identifier: string, store?: RateLimitStore): Promise<void>;
|
|
@@ -1,49 +1,53 @@
|
|
|
1
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
|
+
};
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
12
|
exports.checkServiceTokenRateLimit = checkServiceTokenRateLimit;
|
|
4
13
|
exports.resetServiceTokenRateLimit = resetServiceTokenRateLimit;
|
|
5
14
|
exports.assertServiceTokenRateLimit = assertServiceTokenRateLimit;
|
|
6
15
|
const common_1 = require("@nestjs/common");
|
|
16
|
+
const rateLimitStore_1 = require("./rateLimitStore");
|
|
7
17
|
/**
|
|
8
18
|
* Limite anti-abuso per la verifica dei token di servizio.
|
|
9
19
|
*
|
|
10
20
|
* Il segreto ha 256 bit di entropia (brute force non praticabile), ma il limite evita
|
|
11
|
-
* che un client difettoso o malevolo martelli l'endpoint.
|
|
12
|
-
*
|
|
21
|
+
* che un client difettoso o malevolo martelli l'endpoint. Con `serviceTokens.rateLimitStore`
|
|
22
|
+
* e possibile usare uno store condiviso in cluster.
|
|
13
23
|
*/
|
|
14
24
|
const WINDOW_MS = 60000;
|
|
15
25
|
const MAX_ATTEMPTS = 120;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const bucket = buckets.get(key);
|
|
22
|
-
if (!bucket || bucket.resetAt <= now) {
|
|
23
|
-
if (buckets.size >= MAX_BUCKETS) {
|
|
24
|
-
const oldestKey = buckets.keys().next().value;
|
|
25
|
-
if (oldestKey !== undefined) {
|
|
26
|
-
buckets.delete(oldestKey);
|
|
27
|
-
}
|
|
26
|
+
function checkServiceTokenRateLimit(identifier_1) {
|
|
27
|
+
return __awaiter(this, arguments, void 0, function* (identifier, store = (0, rateLimitStore_1.getDefaultRateLimitStore)()) {
|
|
28
|
+
const key = identifier && identifier.trim() !== '' ? identifier.trim() : 'unknown';
|
|
29
|
+
try {
|
|
30
|
+
return yield store.consume(`service-token|${key}`, WINDOW_MS, MAX_ATTEMPTS);
|
|
28
31
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return { allowed: true, retryAfterSeconds: 0 };
|
|
32
|
+
catch (_a) {
|
|
33
|
+
throw new common_1.HttpException({
|
|
34
|
+
code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',
|
|
35
|
+
message: 'Rate limit non disponibile. Riprova piu tardi.',
|
|
36
|
+
}, common_1.HttpStatus.SERVICE_UNAVAILABLE);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
37
39
|
}
|
|
38
|
-
/** Svuota i bucket (usato nei test). */
|
|
40
|
+
/** Svuota i bucket dello store di default (usato nei test). */
|
|
39
41
|
function resetServiceTokenRateLimit() {
|
|
40
|
-
|
|
42
|
+
(0, rateLimitStore_1.getDefaultRateLimitStore)().reset();
|
|
41
43
|
}
|
|
42
44
|
/** Lancia 429 con `Retry-After` quando il limite e superato. */
|
|
43
|
-
function assertServiceTokenRateLimit(identifier) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
function assertServiceTokenRateLimit(identifier, store) {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
const decision = yield checkServiceTokenRateLimit(identifier, store);
|
|
48
|
+
if (!decision.allowed) {
|
|
49
|
+
throw new common_1.HttpException({ code: 'ACCESSI_SERVICE_TOKEN_RATE_LIMITED', message: 'Troppi tentativi. Riprova piu tardi.' }, common_1.HttpStatus.TOO_MANY_REQUESTS);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
48
52
|
}
|
|
49
53
|
//# sourceMappingURL=serviceTokenRateLimit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serviceTokenRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/serviceTokenRateLimit.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"serviceTokenRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/serviceTokenRateLimit.ts"],"names":[],"mappings":";;;;;;;;;;;AAcA,gEAgBC;AAGD,gEAEC;AAGD,kEAWC;AAjDD,2CAA2D;AAC3D,qDAA4D;AAG5D;;;;;;GAMG;AACH,MAAM,SAAS,GAAG,KAAM,CAAC;AACzB,MAAM,YAAY,GAAG,GAAG,CAAC;AAEzB,SAAsB,0BAA0B;yDAC9C,UAAkB,EAClB,QAAwB,IAAA,yCAAwB,GAAE;QAElD,MAAM,GAAG,GAAG,UAAU,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACnF,IAAI,CAAC;YACH,OAAO,MAAM,KAAK,CAAC,OAAO,CAAC,iBAAiB,GAAG,EAAE,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;QAC9E,CAAC;QAAC,WAAM,CAAC;YACP,MAAM,IAAI,sBAAa,CACrB;gBACE,IAAI,EAAE,gCAAgC;gBACtC,OAAO,EAAE,gDAAgD;aAC1D,EACD,mBAAU,CAAC,mBAAmB,CAC/B,CAAC;QACJ,CAAC;IACH,CAAC;CAAA;AAED,+DAA+D;AAC/D,SAAgB,0BAA0B;IACxC,IAAA,yCAAwB,GAAE,CAAC,KAAK,EAAE,CAAC;AACrC,CAAC;AAED,gEAAgE;AAChE,SAAsB,2BAA2B,CAC/C,UAAkB,EAClB,KAAsB;;QAEtB,MAAM,QAAQ,GAAG,MAAM,0BAA0B,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACrE,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;YACtB,MAAM,IAAI,sBAAa,CACrB,EAAE,IAAI,EAAE,oCAAoC,EAAE,OAAO,EAAE,sCAAsC,EAAE,EAC/F,mBAAU,CAAC,iBAAiB,CAC7B,CAAC;QACJ,CAAC;IACH,CAAC;CAAA","sourcesContent":["import { HttpException, HttpStatus } from '@nestjs/common';\nimport { getDefaultRateLimitStore } from './rateLimitStore';\nimport type { RateLimitStore } from './rateLimitStore';\n\n/**\n * Limite anti-abuso per la verifica dei token di servizio.\n *\n * Il segreto ha 256 bit di entropia (brute force non praticabile), ma il limite evita\n * che un client difettoso o malevolo martelli l'endpoint. Con `serviceTokens.rateLimitStore`\n * e possibile usare uno store condiviso in cluster.\n */\nconst WINDOW_MS = 60_000;\nconst MAX_ATTEMPTS = 120;\n\nexport async function checkServiceTokenRateLimit(\n identifier: string,\n store: RateLimitStore = getDefaultRateLimitStore(),\n): Promise<{ allowed: boolean; retryAfterSeconds: number }> {\n const key = identifier && identifier.trim() !== '' ? identifier.trim() : 'unknown';\n try {\n return await store.consume(`service-token|${key}`, WINDOW_MS, MAX_ATTEMPTS);\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\n/** Svuota i bucket dello store di default (usato nei test). */\nexport function resetServiceTokenRateLimit(): void {\n getDefaultRateLimitStore().reset();\n}\n\n/** Lancia 429 con `Retry-After` quando il limite e superato. */\nexport async function assertServiceTokenRateLimit(\n identifier: string,\n store?: RateLimitStore,\n): Promise<void> {\n const decision = await checkServiceTokenRateLimit(identifier, store);\n if (!decision.allowed) {\n throw new HttpException(\n { code: 'ACCESSI_SERVICE_TOKEN_RATE_LIMITED', message: 'Troppi tentativi. Riprova piu tardi.' },\n HttpStatus.TOO_MANY_REQUESTS,\n );\n }\n}\n"]}
|
|
Binary file
|
|
@@ -24,6 +24,8 @@ const accessiRuntime_1 = require("./accessi-module/accessiRuntime");
|
|
|
24
24
|
const startupLog_1 = require("./accessi-module/startupLog");
|
|
25
25
|
const accessiValidation_1 = require("./accessi-module/security/accessiValidation");
|
|
26
26
|
const AccessiHttpExceptionFilter_1 = require("./accessi-module/security/AccessiHttpExceptionFilter");
|
|
27
|
+
const jwks_1 = require("./accessi-module/security/jwks");
|
|
28
|
+
const edgeRateLimit_1 = require("./accessi-module/security/edgeRateLimit");
|
|
27
29
|
/**
|
|
28
30
|
* Initializes the unified module that combines Accessi and Allegati functionality
|
|
29
31
|
* @param app Express application instance
|
|
@@ -32,7 +34,7 @@ const AccessiHttpExceptionFilter_1 = require("./accessi-module/security/AccessiH
|
|
|
32
34
|
*/
|
|
33
35
|
function initEmilsoftwareModule(app, options) {
|
|
34
36
|
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
-
var _a;
|
|
37
|
+
var _a, _b, _c;
|
|
36
38
|
const logger = new Logger_1.Logger(initEmilsoftwareModule.name);
|
|
37
39
|
if (options === null || options === void 0 ? void 0 : options.accessiOptions) {
|
|
38
40
|
(0, authenticateGen_1.beginAccessiAuthInitialization)();
|
|
@@ -46,15 +48,31 @@ function initEmilsoftwareModule(app, options) {
|
|
|
46
48
|
// Precedenza: EmilsoftwareOptions.openapi > AccessiOptions.openapi > default.
|
|
47
49
|
const resolvedOpenApi = (0, OpenApiConfig_1.resolveAccessiOpenApiOptions)((_a = options === null || options === void 0 ? void 0 : options.accessiOptions) === null || _a === void 0 ? void 0 : _a.openapi, options === null || options === void 0 ? void 0 : options.openapi);
|
|
48
50
|
const nestHostApp = (0, express_1.default)();
|
|
51
|
+
const jwksPath = (options === null || options === void 0 ? void 0 : options.accessiOptions)
|
|
52
|
+
? (0, jwks_1.getAccessiJwksPath)(options.accessiOptions)
|
|
53
|
+
: undefined;
|
|
49
54
|
const isNestRoute = (path) => path === "/api" ||
|
|
50
55
|
path.startsWith("/api/") ||
|
|
51
|
-
(0, OpenApiConfig_1.isOpenApiRequestPath)(path, resolvedOpenApi)
|
|
56
|
+
(0, OpenApiConfig_1.isOpenApiRequestPath)(path, resolvedOpenApi) ||
|
|
57
|
+
(jwksPath !== undefined && path === jwksPath);
|
|
58
|
+
const edgeRateLimit = ((_b = options === null || options === void 0 ? void 0 : options.accessiOptions) === null || _b === void 0 ? void 0 : _b.edgeRateLimit)
|
|
59
|
+
? (0, edgeRateLimit_1.createAccessiEdgeRateLimitMiddleware)(Object.assign(Object.assign({}, options.accessiOptions.edgeRateLimit), { match: (_c = options.accessiOptions.edgeRateLimit.match) !== null && _c !== void 0 ? _c : ((path) => isNestRoute(path)) }))
|
|
60
|
+
: undefined;
|
|
61
|
+
if (edgeRateLimit) {
|
|
62
|
+
app.use(edgeRateLimit);
|
|
63
|
+
}
|
|
52
64
|
app.use((req, res, next) => {
|
|
53
65
|
if (!isNestRoute(req.path)) {
|
|
54
66
|
return next();
|
|
55
67
|
}
|
|
56
68
|
return nestHostApp(req, res, next);
|
|
57
69
|
});
|
|
70
|
+
if (options === null || options === void 0 ? void 0 : options.accessiOptions) {
|
|
71
|
+
(0, jwks_1.setupAccessiJwks)(app, options.accessiOptions);
|
|
72
|
+
if (jwksPath) {
|
|
73
|
+
logger.info(`JWKS accessi disponibile su: ${jwksPath}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
58
76
|
const nestExpressInstance = new platform_express_1.ExpressAdapter(nestHostApp);
|
|
59
77
|
const nestApp = yield core_1.NestFactory.create(EmilsoftwareModule_1.EmilsoftwareModule.forRoot(options), nestExpressInstance, Object.assign({ bufferLogs: (options === null || options === void 0 ? void 0 : options.bufferLogs) !== false }, ((options === null || options === void 0 ? void 0 : options.logger) ? { logger: options.logger } : {})));
|
|
60
78
|
nestApp.enableCors();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initEmilsoftwareModule.js","sourceRoot":"","sources":["../src/initEmilsoftwareModule.ts"],"names":[],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"initEmilsoftwareModule.js","sourceRoot":"","sources":["../src/initEmilsoftwareModule.ts"],"names":[],"mappings":"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","sourcesContent":["import express, { Application, Request } from \"express\";\nimport { NestFactory } from \"@nestjs/core\";\nimport { ExpressAdapter } from \"@nestjs/platform-express\";\nimport type { INestApplication } from \"@nestjs/common\";\nimport type { OpenAPIObject } from \"@nestjs/swagger\";\nimport {\n buildOpenApiExcludePaths,\n isOpenApiRequestPath,\n resolveAccessiOpenApiOptions,\n setupOpenApi\n} from \"./openapi/OpenApiConfig\";\nimport { Logger } from \"./Logger\";\nimport { EmilsoftwareModule, EmilsoftwareOptions } from \"./EmilsoftwareModule\";\nimport {\n beginAccessiAuthInitialization,\n failAccessiAuthInitialization,\n setAccessiAuthService\n} from \"./accessi-module/middleware/authenticateGen\";\nimport { registerAccessiModuleServices } from \"./accessi-module/accessiRuntime\";\nimport { logAccessiStartup } from \"./accessi-module/startupLog\";\nimport { createAccessiValidationPipe } from './accessi-module/security/accessiValidation';\nimport { AccessiHttpExceptionFilter } from './accessi-module/security/AccessiHttpExceptionFilter';\nimport { getAccessiJwksPath, setupAccessiJwks } from './accessi-module/security/jwks';\nimport { createAccessiEdgeRateLimitMiddleware } from './accessi-module/security/edgeRateLimit';\n\n/** Esito del bootstrap del modulo unificato. */\nexport interface EmilsoftwareModuleInitializationResult {\n /** Sotto-app Nest che serve Accessi, Allegati e la documentazione. */\n nestApp: INestApplication;\n /** Documento OpenAPI generato, mergiabile nel documento unico dell'host. */\n document: OpenAPIObject;\n}\n\n/**\n * Initializes the unified module that combines Accessi and Allegati functionality\n * @param app Express application instance\n * @param options Configuration options for both modules\n * @returns Il sotto-app Nest e il documento OpenAPI generato.\n */\nexport async function initEmilsoftwareModule(\n app: Application,\n options: EmilsoftwareOptions\n): Promise<EmilsoftwareModuleInitializationResult> {\n const logger: Logger = new Logger(initEmilsoftwareModule.name);\n if (options?.accessiOptions) {\n beginAccessiAuthInitialization();\n }\n\n try {\n // Isolate Nest on a dedicated Express instance and dispatch only module routes.\n // This keeps pre-existing routes/middlewares on the host app untouched.\n // Prefisso OpenAPI configurabile (default `accessi/openapi`), cosi' piu'\n // documentazioni possono convivere (backend su `/openapi`, moduli sotto\n // `/<modulo>/openapi`).\n // Precedenza: EmilsoftwareOptions.openapi > AccessiOptions.openapi > default.\n const resolvedOpenApi = resolveAccessiOpenApiOptions(\n options?.accessiOptions?.openapi,\n options?.openapi\n );\n\n const nestHostApp = express();\n const jwksPath = options?.accessiOptions\n ? getAccessiJwksPath(options.accessiOptions)\n : undefined;\n const isNestRoute = (path: string) =>\n path === \"/api\" ||\n path.startsWith(\"/api/\") ||\n isOpenApiRequestPath(path, resolvedOpenApi) ||\n (jwksPath !== undefined && path === jwksPath);\n const edgeRateLimit = options?.accessiOptions?.edgeRateLimit\n ? createAccessiEdgeRateLimitMiddleware({\n ...options.accessiOptions.edgeRateLimit,\n match: options.accessiOptions.edgeRateLimit.match ?? ((path) => isNestRoute(path)),\n })\n : undefined;\n if (edgeRateLimit) {\n app.use(edgeRateLimit);\n }\n\n app.use((req: Request, res, next) => {\n if (!isNestRoute(req.path)) {\n return next();\n }\n return nestHostApp(req, res, next);\n });\n\n if (options?.accessiOptions) {\n setupAccessiJwks(app, options.accessiOptions);\n if (jwksPath) {\n logger.info(`JWKS accessi disponibile su: ${jwksPath}`);\n }\n }\n\n const nestExpressInstance = new ExpressAdapter(nestHostApp);\n const nestApp = await NestFactory.create(EmilsoftwareModule.forRoot(options), nestExpressInstance, {\n bufferLogs: options?.bufferLogs !== false,\n ...(options?.logger ? { logger: options.logger } : {})\n });\n\n nestApp.enableCors();\n if (options?.accessiOptions) {\n // Keep the Accessi API contract identical for standalone and unified bootstraps.\n nestApp.useGlobalPipes(createAccessiValidationPipe());\n nestApp.useGlobalFilters(new AccessiHttpExceptionFilter());\n }\n\n nestApp.setGlobalPrefix('api', {\n exclude: buildOpenApiExcludePaths(resolvedOpenApi)\n });\n\n // Documentazione OpenAPI del modulo unificato (Accessi + Allegati).\n const document = setupOpenApi(nestApp, resolvedOpenApi);\n\n await nestApp.init();\n if (options?.accessiOptions) {\n const accessiServices = registerAccessiModuleServices(app, nestApp);\n app.locals.accessiAuthService = accessiServices.authenticateGenService;\n setAccessiAuthService(accessiServices.authenticateGenService);\n logAccessiStartup(options.accessiOptions, resolvedOpenApi);\n }\n\n logger.info(\"Emilsoftware module initialized successfully\");\n\n return { nestApp, document };\n } catch (error) {\n if (options?.accessiOptions) {\n failAccessiAuthInitialization(error);\n }\n logger.error(\"Error initializing Emilsoftware Module:\", error);\n throw error;\n }\n}\n"]}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Gateway rate limit per Accessi Service
|
|
2
|
+
|
|
3
|
+
Il rate limit applicativo di Accessi protegge login, 2FA, reset password, console entry e service token. Il gateway/API gateway resta utile per una protezione per IP, connessioni e bot prima che la richiesta raggiunga Node.
|
|
4
|
+
|
|
5
|
+
Regole consigliate:
|
|
6
|
+
|
|
7
|
+
- `/api/accessi/auth/*`: soglia bassa, burst contenuto
|
|
8
|
+
- `/api/accessi/console/entry/exchange`: soglia bassa
|
|
9
|
+
- `/api/accessi/*`: soglia media
|
|
10
|
+
- `/accessi/openapi*` e `/accessi/.well-known/jwks.json`: soglia alta o cache CDN
|
|
11
|
+
|
|
12
|
+
## Nginx
|
|
13
|
+
|
|
14
|
+
```nginx
|
|
15
|
+
limit_req_zone $binary_remote_addr zone=accessi_auth:10m rate=2r/s;
|
|
16
|
+
limit_req_zone $binary_remote_addr zone=accessi_api:10m rate=10r/s;
|
|
17
|
+
limit_conn_zone $binary_remote_addr zone=accessi_conn:10m;
|
|
18
|
+
|
|
19
|
+
server {
|
|
20
|
+
# Se Accessi e dietro un proxy/load balancer, configura real_ip
|
|
21
|
+
# prima di usare $binary_remote_addr.
|
|
22
|
+
|
|
23
|
+
location /api/accessi/auth/ {
|
|
24
|
+
limit_req zone=accessi_auth burst=10 nodelay;
|
|
25
|
+
limit_conn accessi_conn 20;
|
|
26
|
+
proxy_pass http://accessi_service;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
location /api/accessi/ {
|
|
30
|
+
limit_req zone=accessi_api burst=50 nodelay;
|
|
31
|
+
limit_conn accessi_conn 20;
|
|
32
|
+
proxy_pass http://accessi_service;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
location /accessi/ {
|
|
36
|
+
limit_req zone=accessi_api burst=50 nodelay;
|
|
37
|
+
proxy_pass http://accessi_service;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Traefik
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
http:
|
|
46
|
+
middlewares:
|
|
47
|
+
accessi-auth-ratelimit:
|
|
48
|
+
rateLimit:
|
|
49
|
+
average: 2
|
|
50
|
+
burst: 10
|
|
51
|
+
period: 1s
|
|
52
|
+
accessi-api-ratelimit:
|
|
53
|
+
rateLimit:
|
|
54
|
+
average: 10
|
|
55
|
+
burst: 50
|
|
56
|
+
period: 1s
|
|
57
|
+
routers:
|
|
58
|
+
accessi-auth:
|
|
59
|
+
rule: PathPrefix(`/api/accessi/auth/`)
|
|
60
|
+
middlewares: [accessi-auth-ratelimit]
|
|
61
|
+
service: accessi
|
|
62
|
+
accessi-api:
|
|
63
|
+
rule: PathPrefix(`/api/accessi/`) || PathPrefix(`/accessi/`)
|
|
64
|
+
middlewares: [accessi-api-ratelimit]
|
|
65
|
+
service: accessi
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Cloudflare
|
|
69
|
+
|
|
70
|
+
Regole di rate limiting:
|
|
71
|
+
|
|
72
|
+
- path `/api/accessi/auth/*`: 10 richieste/minuto per IP, azione `managed challenge`
|
|
73
|
+
- path `/api/accessi/console/entry/exchange`: 30 richieste/5 minuti per IP
|
|
74
|
+
- path `/api/accessi/*`: 300 richieste/minuto per IP, azione `block`
|
|
75
|
+
- path `/accessi/.well-known/jwks.json`: cache CDN, limite alto
|
|
76
|
+
|
|
77
|
+
Aggiungere bot fight mode/WAF secondo il piano disponibile.
|
|
78
|
+
|
|
79
|
+
## AWS WAF / API Gateway
|
|
80
|
+
|
|
81
|
+
- rate-based rule: 300 richieste/5 minuti per IP
|
|
82
|
+
- scope-down statement: `URI starts with /api/accessi`
|
|
83
|
+
- regola più severa su `/api/accessi/auth/*`
|
|
84
|
+
- AWS Shield/WAF Bot Control se disponibile
|
|
85
|
+
|
|
86
|
+
## Rapporto con `edgeRateLimit` della libreria
|
|
87
|
+
|
|
88
|
+
`edgeRateLimit` e un ultimo livello applicativo:
|
|
89
|
+
|
|
90
|
+
- usa lo stesso `RateLimitStore` Redis dei rate limit per account;
|
|
91
|
+
- copre Accessi anche se il gateway non ha regole specifiche;
|
|
92
|
+
- non sostituisce WAF, bot protection, limiti di connessione o protezione DDoS.
|
|
93
|
+
|
|
94
|
+
In produzione usare entrambi:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
Client -> CDN/WAF -> API gateway -> edgeRateLimit Accessi -> rate limit per account
|
|
98
|
+
```
|
package/integration-prompt.md
CHANGED
|
@@ -150,6 +150,127 @@ const merged = mergeOpenApiDocuments(hostDocument, accessiDocument, {
|
|
|
150
150
|
});
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
### JWKS e rotazione chiavi
|
|
154
|
+
|
|
155
|
+
Con RS256/ES256 Accessi pubblica automaticamente:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
GET /accessi/.well-known/jwks.json
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Configurazione:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
jwtOptions: {
|
|
165
|
+
algorithm: "RS256",
|
|
166
|
+
privateKey: process.env.ACCESSI_JWT_PRIVATE_KEY,
|
|
167
|
+
publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY,
|
|
168
|
+
keyId: "accessi-2026-02",
|
|
169
|
+
jwksPath: "accessi/.well-known/jwks.json",
|
|
170
|
+
previousPublicKeys: [
|
|
171
|
+
{
|
|
172
|
+
publicKey: process.env.ACCESSI_JWT_PUBLIC_KEY_2026_01!,
|
|
173
|
+
keyId: "accessi-2026-01",
|
|
174
|
+
algorithm: "RS256",
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Rotazione:
|
|
181
|
+
|
|
182
|
+
1. generare la nuova coppia
|
|
183
|
+
2. pubblicare la nuova chiave mantenendo la precedente in `previousPublicKeys`
|
|
184
|
+
3. firmare con la nuova `keyId`
|
|
185
|
+
4. attendere la scadenza massima dei token + il TTL cache della JWKS (default 5 minuti)
|
|
186
|
+
5. rimuovere la chiave precedente
|
|
187
|
+
|
|
188
|
+
Per un'app Nest esistente si può usare `setupAccessiJwks(app.getHttpAdapter().getInstance(), options)`.
|
|
189
|
+
|
|
190
|
+
### Store ticket console
|
|
191
|
+
|
|
192
|
+
Il default è in memoria, adatto a una sola istanza. In multi-istanza configurare uno store condiviso:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { createRedisConsoleEntryStore } from "emilsoftware-utilities/accessi/server";
|
|
196
|
+
|
|
197
|
+
consoleEntry: {
|
|
198
|
+
ttlMs: 60_000,
|
|
199
|
+
store: createRedisConsoleEntryStore({
|
|
200
|
+
client: redis, // ioredis o client compatibile con eval(script, numKeys, ...args)
|
|
201
|
+
encryptionKey: process.env.ACCESSI_CONSOLE_ENTRY_KEY!,
|
|
202
|
+
keyPrefix: "accessi:console-entry:",
|
|
203
|
+
}),
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Lo store Redis:
|
|
208
|
+
|
|
209
|
+
- cifra il JWT con AES-256-GCM
|
|
210
|
+
- usa `SET ... PX ... NX` con TTL
|
|
211
|
+
- consuma con `GET` + `DEL` atomici in Lua
|
|
212
|
+
- rende il ticket monouso anche tra istanze diverse
|
|
213
|
+
|
|
214
|
+
Se lo store condiviso non è raggiungibile, la console deve fallire chiusa.
|
|
215
|
+
|
|
216
|
+
### Rate limit condiviso
|
|
217
|
+
|
|
218
|
+
Con più istanze anche i rate limit devono usare uno store condiviso:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { createRedisRateLimitStore } from "emilsoftware-utilities/accessi/server";
|
|
222
|
+
|
|
223
|
+
const rateLimitStore = createRedisRateLimitStore({
|
|
224
|
+
client: redis, // ioredis o client compatibile con eval(script, numKeys, ...args)
|
|
225
|
+
keyPrefix: "accessi:rate:",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
publicAuthRateLimit: {
|
|
229
|
+
enabled: true,
|
|
230
|
+
store: rateLimitStore,
|
|
231
|
+
login: { windowMs: 15 * 60_000, maxAttempts: 10 },
|
|
232
|
+
twoFactorVerify: { windowMs: 15 * 60_000, maxAttempts: 20 },
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
serviceTokens: {
|
|
236
|
+
enabled: true,
|
|
237
|
+
rateLimitStore: rateLimitStore,
|
|
238
|
+
},
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Lo store Redis usa `INCR` + `PEXPIRE` atomici:
|
|
242
|
+
|
|
243
|
+
- conta tentativi per IP e per soggetto (email, challenge, ticket, tokenId);
|
|
244
|
+
- il limite è globale tra le istanze;
|
|
245
|
+
- non viene azzerato da un restart;
|
|
246
|
+
- se Redis non è raggiungibile il controllo fallisce chiuso: l’endpoint risponde con errore, non autorizza.
|
|
247
|
+
|
|
248
|
+
Default: store in memoria. Va bene per una singola istanza o per ambienti di sviluppo.
|
|
249
|
+
|
|
250
|
+
### Edge rate limit per IP
|
|
251
|
+
|
|
252
|
+
Per una soglia grossolana davanti alle rotte Accessi:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
edgeRateLimit: {
|
|
256
|
+
enabled: true,
|
|
257
|
+
windowMs: 60_000,
|
|
258
|
+
maxAttempts: 300,
|
|
259
|
+
store: rateLimitStore, // stesso store Redis, opzionale
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Viene applicato prima del dispatcher Accessi e copre `/api/accessi/*`, OpenAPI e JWKS. In caso di store non disponibile risponde `503 ACCESSI_RATE_LIMIT_UNAVAILABLE`; quando la soglia è superata risponde `429 ACCESSI_EDGE_RATE_LIMITED` con `Retry-After`.
|
|
264
|
+
|
|
265
|
+
Il gateway/API gateway resta consigliato per:
|
|
266
|
+
|
|
267
|
+
- limiti di connessione e burst;
|
|
268
|
+
- bot protection e WAF;
|
|
269
|
+
- rate limit per IP a livello di rete;
|
|
270
|
+
- blocco geografico o reputazionale.
|
|
271
|
+
|
|
272
|
+
Configurazioni di riferimento: `docs/gateway-rate-limit.md`.
|
|
273
|
+
|
|
153
274
|
---
|
|
154
275
|
|
|
155
276
|
## 4. Backend fratello — Express
|
|
@@ -190,7 +311,9 @@ Dopo il middleware:
|
|
|
190
311
|
- **HS256**: usare `jwt.serverSecret` con `ACCESSI_JWT_SECRET`. Il resource server deriva la chiave access con la stessa KDF del server.
|
|
191
312
|
- **RS256/ES256**: usare `jwt.publicKey` PEM oppure `jwt.jwks.url`. Consigliato in multi-servizio.
|
|
192
313
|
- Il server Accessi supporta `jwtOptions.algorithm`, `privateKey`, `publicKey`, `keyId`.
|
|
193
|
-
- Accessi
|
|
314
|
+
- Accessi espone la JWKS su `jwtOptions.jwksPath` (default `accessi/.well-known/jwks.json`) quando usa RS/ES.
|
|
315
|
+
- La JWKS include la chiave corrente e le `previousPublicKeys`, per la rotazione senza downtime.
|
|
316
|
+
- Configurare il resource server con `jwks: { url: "https://accessi.example.com/accessi/.well-known/jwks.json" }`.
|
|
194
317
|
|
|
195
318
|
### Errori
|
|
196
319
|
|
|
@@ -404,6 +527,9 @@ ACCESSI_JWT_ALGORITHM=RS256
|
|
|
404
527
|
ACCESSI_JWT_PRIVATE_KEY=
|
|
405
528
|
ACCESSI_JWT_PUBLIC_KEY=
|
|
406
529
|
ACCESSI_JWT_KEY_ID=
|
|
530
|
+
ACCESSI_JWKS_PATH=accessi/.well-known/jwks.json
|
|
531
|
+
ACCESSI_CONSOLE_ENTRY_TTL_MS=60000
|
|
532
|
+
ACCESSI_CONSOLE_ENTRY_KEY=
|
|
407
533
|
SMTP_HOST=
|
|
408
534
|
SMTP_PORT=587
|
|
409
535
|
SMTP_USER=
|
|
@@ -428,6 +554,7 @@ Frontend:
|
|
|
428
554
|
```env
|
|
429
555
|
ACCESSI_API_BASE_URL=https://accessi.example.com
|
|
430
556
|
ACCESSI_CONSOLE_URL=https://accessi.example.com/api/accessi/console
|
|
557
|
+
ACCESSI_JWKS_URL=https://accessi.example.com/accessi/.well-known/jwks.json
|
|
431
558
|
```
|
|
432
559
|
|
|
433
560
|
---
|
|
@@ -443,16 +570,20 @@ Checklist:
|
|
|
443
570
|
5. Spostare la console una sola volta.
|
|
444
571
|
6. Passare i backend fratelli a `resource-server`.
|
|
445
572
|
7. Configurare un service token con `accessi:introspect`.
|
|
446
|
-
8.
|
|
573
|
+
8. Pubblicare la JWKS e configurare i resource server su `jwks.url`.
|
|
574
|
+
9. In multi-istanza configurare `consoleEntry.store`, `publicAuthRateLimit.store` e `serviceTokens.rateLimitStore` con Redis.
|
|
575
|
+
10. Testare:
|
|
447
576
|
- login
|
|
448
577
|
- token valido
|
|
449
578
|
- token scaduto
|
|
450
579
|
- utente disabilitato
|
|
451
580
|
- revoca grants
|
|
452
581
|
- Accessi Service spento (fail closed)
|
|
453
|
-
- console entry ticket
|
|
454
|
-
|
|
455
|
-
|
|
582
|
+
- console entry ticket su istanze diverse
|
|
583
|
+
- rate limit condiviso tra istanze
|
|
584
|
+
- rotazione JWKS con vecchia chiave ancora valida
|
|
585
|
+
11. Aggiornare CORS se i frontend cambiano dominio.
|
|
586
|
+
12. Aggiornare monitoraggio e log senza includere token.
|
|
456
587
|
|
|
457
588
|
---
|
|
458
589
|
|
|
@@ -474,8 +605,8 @@ Checklist:
|
|
|
474
605
|
|
|
475
606
|
## 14. Limiti attuali da conoscere
|
|
476
607
|
|
|
477
|
-
- I ticket di ingresso console
|
|
478
|
-
- I rate limit
|
|
479
|
-
- Accessi non espone ancora un endpoint JWKS pronto: il resource server sa verificare una JWKS, ma la chiave pubblica va pubblicata dall'host.
|
|
608
|
+
- I ticket di ingresso console usano lo store in memoria per default: in multi-istanza configurare `consoleEntry.store` con Redis. Lo store Redis e cifrato e monouso, ma richiede un client Redis compatibile.
|
|
609
|
+
- I rate limit usano lo store in memoria per default: in multi-istanza configurare `publicAuthRateLimit.store` e `serviceTokens.rateLimitStore` con Redis. Il gateway resta comunque consigliato per i limiti grossolani per IP.
|
|
480
610
|
- Il client generato e completo (include endpoint amministrativi): non distribuirlo a frontend non amministrativi senza valutare gli endpoint esposti. Per un client pubblico ridotto, generare un sottoinsieme per tag con Orval.
|
|
481
611
|
- I DTO in `accessi/contracts/dtos` dipendono da Nest/class-validator: non sono pensati per bundle frontend minimali.
|
|
612
|
+
- La JWKS viene pubblicata solo con algoritmi asimmetrici: con HS256 non esiste una chiave pubblica da distribuire.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emilsoftware-utilities",
|
|
3
|
-
"version": "2.0.0-dev.
|
|
3
|
+
"version": "2.0.0-dev.68",
|
|
4
4
|
"description": "Utilities for EmilSoftware",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -101,8 +101,8 @@
|
|
|
101
101
|
"typecheck": "tsc --noEmit",
|
|
102
102
|
"lint": "node scripts/lint.cjs",
|
|
103
103
|
"test": "npm run typecheck && npm run lint && npm run test:unit",
|
|
104
|
-
"test:unit": "node --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs",
|
|
105
|
-
"test:coverage": "node --experimental-test-coverage --test-coverage-lines=50 --test-coverage-functions=60 --test-coverage-branches=60 --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs",
|
|
104
|
+
"test:unit": "node --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs",
|
|
105
|
+
"test:coverage": "node --experimental-test-coverage --test-coverage-lines=50 --test-coverage-functions=60 --test-coverage-branches=60 --test test/angular-components.cjs test/menus-catalog.mjs test/menu-scopes.cjs test/accessi-regressions.cjs test/password-reset-regressions.cjs test/two-factor.cjs test/accessi-schema.cjs test/utilities-regressions.cjs test/service-token-regressions.cjs test/accessi-email-config.cjs test/accessi-catalog-scripts.cjs test/openapi-config.cjs test/resource-server.cjs test/accessi-security-standards.cjs",
|
|
106
106
|
"test:accessi": "npm run test:unit",
|
|
107
107
|
"test:all": "npm test && npm run test:integration",
|
|
108
108
|
"test:integration": "node test/integration/run-firebird.cjs 5",
|
|
@@ -183,7 +183,8 @@
|
|
|
183
183
|
},
|
|
184
184
|
"files": [
|
|
185
185
|
"dist",
|
|
186
|
-
"integration-prompt.md"
|
|
186
|
+
"integration-prompt.md",
|
|
187
|
+
"docs"
|
|
187
188
|
],
|
|
188
189
|
"config": {
|
|
189
190
|
"commitizen": {
|
|
Binary file
|