emilsoftware-utilities 2.0.0-dev.67 → 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 +53 -2
- 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 +21 -7
- 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 +16 -3
- 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 +90 -8
- 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/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 +8 -16
- 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 +17 -1
- package/dist/accessi-module/index.js +71 -5
- 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 +58 -11
- package/dist/accessi-module/security/consoleEntry.js +181 -31
- package/dist/accessi-module/security/consoleEntry.js.map +1 -1
- package/dist/accessi-module/security/edgeRateLimit.d.ts +33 -0
- package/dist/accessi-module/security/edgeRateLimit.js +81 -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/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 +9 -7
- package/dist/accessi-module/security/publicAuthRateLimit.js +55 -78
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
- package/dist/accessi-module/security/rateLimitStore.d.ts +34 -0
- package/dist/accessi-module/security/rateLimitStore.js +145 -0
- package/dist/accessi-module/security/rateLimitStore.js.map +1 -0
- 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/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/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 +39 -3
- 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/gateway-rate-limit.md +98 -0
- package/docs/standalone-service.md +176 -0
- package/integration-prompt.md +299 -8
- package/package.json +11 -5
- package/dist/emilsoftware-utilities-2.0.0-dev.67.tgz +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publicAuthRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/publicAuthRateLimit.ts"],"names":[],"mappings":"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","sourcesContent":["import { HttpStatus } from '@nestjs/common';\nimport { Request, Response } from 'express';\nimport { RestUtilities } from '../../Utilities';\nimport type { AccessiOptions, PublicAuthRateLimitRuleOptions } from '../AccessiModule';\n\nexport type PublicAuthRateLimitScope =\n | 'login'\n | 'twoFactorVerify'\n | 'twoFactorResend'\n | 'register'\n | 'passwordResetEmail'\n | 'passwordResetConfirm'\n | 'getUserByToken'\n | 'adminBootstrap'\n | 'consoleEntry';\n\ntype RateLimitBucket = {\n count: number;\n resetAt: number;\n lastSeenAt: number;\n};\n\ntype RateLimitDecision = {\n allowed: boolean;\n retryAfterSeconds: number;\n};\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 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\nconst RATE_LIMIT_BUCKETS = new Map<string, RateLimitBucket>();\nconst MAX_TRACKED_BUCKETS = 20000;\nconst MAX_WINDOW_MS = Math.max(\n ...Object.values(DEFAULT_PUBLIC_AUTH_RATE_LIMITS).map((rule) => rule.windowMs),\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\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\nfunction pruneRateLimitBuckets(now: number): void {\n if (RATE_LIMIT_BUCKETS.size < MAX_TRACKED_BUCKETS) {\n return;\n }\n\n for (const [key, bucket] of RATE_LIMIT_BUCKETS.entries()) {\n if (bucket.resetAt <= now || bucket.lastSeenAt <= now - MAX_WINDOW_MS * 2) {\n RATE_LIMIT_BUCKETS.delete(key);\n }\n\n if (RATE_LIMIT_BUCKETS.size < MAX_TRACKED_BUCKETS) {\n return;\n }\n }\n}\n\n/** Mantiene il numero di bucket sotto la soglia, scartando i meno recenti. */\nfunction enforceBucketCap(): void {\n while (RATE_LIMIT_BUCKETS.size >= MAX_TRACKED_BUCKETS) {\n let oldestKey: string | undefined;\n let oldestSeen = Number.POSITIVE_INFINITY;\n\n for (const [key, bucket] of RATE_LIMIT_BUCKETS.entries()) {\n if (bucket.lastSeenAt < oldestSeen) {\n oldestSeen = bucket.lastSeenAt;\n oldestKey = key;\n }\n }\n\n if (oldestKey === undefined) {\n return;\n }\n\n RATE_LIMIT_BUCKETS.delete(oldestKey);\n }\n}\n\nfunction consumeBucket(\n bucketKey: string,\n rule: PublicAuthRateLimitRuleOptions,\n now: number,\n): number | null {\n const existingBucket = RATE_LIMIT_BUCKETS.get(bucketKey);\n if (!existingBucket || existingBucket.resetAt <= now) {\n enforceBucketCap();\n RATE_LIMIT_BUCKETS.set(bucketKey, {\n count: 1,\n resetAt: now + rule.windowMs,\n lastSeenAt: now,\n });\n return null;\n }\n\n existingBucket.lastSeenAt = now;\n if (existingBucket.count >= rule.maxAttempts) {\n return Math.max(1, Math.ceil((existingBucket.resetAt - now) / 1000));\n }\n\n existingBucket.count += 1;\n return null;\n}\n\nexport function checkPublicAuthRateLimit(\n options: AccessiOptions,\n scope: PublicAuthRateLimitScope,\n req: Request,\n identifiers: string[] = [],\n): RateLimitDecision {\n if (options.publicAuthRateLimit?.enabled === false) {\n return { allowed: true, retryAfterSeconds: 0 };\n }\n\n const rule = getRule(options, scope);\n const now = Date.now();\n pruneRateLimitBuckets(now);\n\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 ipRetryAfterSeconds = consumeBucket(`${scope}|ip|${ipAddress}`, rule, now);\n if (ipRetryAfterSeconds) {\n return { allowed: false, retryAfterSeconds: ipRetryAfterSeconds };\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 bucketRetryAfterSeconds = consumeBucket(\n `${scope}|ip-subject|${ipAddress}|${normalizedIdentifier}`,\n rule,\n now,\n );\n if (bucketRetryAfterSeconds) {\n retryAfterSeconds = Math.max(retryAfterSeconds, bucketRetryAfterSeconds);\n }\n }\n\n if (retryAfterSeconds > 0) {\n return {\n allowed: false,\n retryAfterSeconds,\n };\n }\n\n return { allowed: true, retryAfterSeconds: 0 };\n}\n\nexport function sendPublicAuthRateLimitExceeded(\n res: Response,\n retryAfterSeconds: number,\n): Response {\n res.setHeader('Retry-After', String(retryAfterSeconds));\n return RestUtilities.sendErrorMessage(\n res,\n 'Troppi tentativi. Riprova piu tardi.',\n 'PublicAuthRateLimit',\n HttpStatus.TOO_MANY_REQUESTS,\n );\n}\n"]}
|
|
1
|
+
{"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"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Esito di un controllo di rate limit. */
|
|
2
|
+
export interface RateLimitDecision {
|
|
3
|
+
allowed: boolean;
|
|
4
|
+
retryAfterSeconds: number;
|
|
5
|
+
}
|
|
6
|
+
/** Store pluggable per rate limit: memoria per singola istanza, Redis per cluster. */
|
|
7
|
+
export interface RateLimitStore {
|
|
8
|
+
consume(key: string, windowMs: number, maxAttempts: number): Promise<RateLimitDecision>;
|
|
9
|
+
/** Health check opzionale per readiness del servizio. */
|
|
10
|
+
healthCheck?(): Promise<boolean>;
|
|
11
|
+
}
|
|
12
|
+
export interface MemoryRateLimitStore extends RateLimitStore {
|
|
13
|
+
reset(): void;
|
|
14
|
+
}
|
|
15
|
+
/** Store in memoria con finestra fissa e limite di bucket. */
|
|
16
|
+
export declare function createMemoryRateLimitStore(maxBuckets?: number): MemoryRateLimitStore;
|
|
17
|
+
/** Store di default in memoria, usato quando non viene configurato uno store condiviso. */
|
|
18
|
+
export declare function getDefaultRateLimitStore(): MemoryRateLimitStore;
|
|
19
|
+
/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */
|
|
20
|
+
export interface RateLimitRedisClient {
|
|
21
|
+
eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
export interface RedisRateLimitStoreOptions {
|
|
24
|
+
client: RateLimitRedisClient;
|
|
25
|
+
/** Default: `accessi:rate:`. */
|
|
26
|
+
keyPrefix?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Store Redis per rate limit distribuito.
|
|
30
|
+
*
|
|
31
|
+
* Usa `INCR` + `PEXPIRE` atomici. Adatto a login, reset password, 2FA,
|
|
32
|
+
* console entry e service token in deployment multi-istanza.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createRedisRateLimitStore(options: RedisRateLimitStoreOptions): RateLimitStore;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createMemoryRateLimitStore = createMemoryRateLimitStore;
|
|
13
|
+
exports.getDefaultRateLimitStore = getDefaultRateLimitStore;
|
|
14
|
+
exports.createRedisRateLimitStore = createRedisRateLimitStore;
|
|
15
|
+
const DEFAULT_MAX_BUCKETS = 20000;
|
|
16
|
+
function normalizeLimits(windowMs, maxAttempts) {
|
|
17
|
+
return {
|
|
18
|
+
windowMs: Math.max(1, Math.trunc(windowMs)),
|
|
19
|
+
maxAttempts: Math.max(1, Math.trunc(maxAttempts)),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Store in memoria con finestra fissa e limite di bucket. */
|
|
23
|
+
function createMemoryRateLimitStore(maxBuckets = DEFAULT_MAX_BUCKETS) {
|
|
24
|
+
const buckets = new Map();
|
|
25
|
+
const safeMaxBuckets = Math.max(1, Math.trunc(maxBuckets));
|
|
26
|
+
const prune = (now) => {
|
|
27
|
+
if (buckets.size < safeMaxBuckets)
|
|
28
|
+
return;
|
|
29
|
+
for (const [key, bucket] of buckets) {
|
|
30
|
+
if (bucket.resetAt <= now)
|
|
31
|
+
buckets.delete(key);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const enforceCap = () => {
|
|
35
|
+
while (buckets.size >= safeMaxBuckets) {
|
|
36
|
+
let oldestKey;
|
|
37
|
+
let oldestSeen = Number.POSITIVE_INFINITY;
|
|
38
|
+
for (const [key, bucket] of buckets) {
|
|
39
|
+
if (bucket.lastSeenAt < oldestSeen) {
|
|
40
|
+
oldestSeen = bucket.lastSeenAt;
|
|
41
|
+
oldestKey = key;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (oldestKey === undefined)
|
|
45
|
+
return;
|
|
46
|
+
buckets.delete(oldestKey);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
consume(key, windowMs, maxAttempts) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
const limits = normalizeLimits(windowMs, maxAttempts);
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
prune(now);
|
|
55
|
+
const existing = buckets.get(key);
|
|
56
|
+
if (!existing || existing.resetAt <= now) {
|
|
57
|
+
enforceCap();
|
|
58
|
+
buckets.set(key, {
|
|
59
|
+
count: 1,
|
|
60
|
+
resetAt: now + limits.windowMs,
|
|
61
|
+
lastSeenAt: now,
|
|
62
|
+
});
|
|
63
|
+
return { allowed: true, retryAfterSeconds: 0 };
|
|
64
|
+
}
|
|
65
|
+
existing.lastSeenAt = now;
|
|
66
|
+
if (existing.count >= limits.maxAttempts) {
|
|
67
|
+
return {
|
|
68
|
+
allowed: false,
|
|
69
|
+
retryAfterSeconds: Math.max(1, Math.ceil((existing.resetAt - now) / 1000)),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
existing.count += 1;
|
|
73
|
+
return { allowed: true, retryAfterSeconds: 0 };
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
reset() {
|
|
77
|
+
buckets.clear();
|
|
78
|
+
},
|
|
79
|
+
healthCheck() {
|
|
80
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const defaultStore = createMemoryRateLimitStore();
|
|
87
|
+
/** Store di default in memoria, usato quando non viene configurato uno store condiviso. */
|
|
88
|
+
function getDefaultRateLimitStore() {
|
|
89
|
+
return defaultStore;
|
|
90
|
+
}
|
|
91
|
+
const REDIS_SCRIPT = `
|
|
92
|
+
local current = redis.call('INCR', KEYS[1])
|
|
93
|
+
if current == 1 then
|
|
94
|
+
redis.call('PEXPIRE', KEYS[1], ARGV[1])
|
|
95
|
+
end
|
|
96
|
+
local ttl = redis.call('PTTL', KEYS[1])
|
|
97
|
+
if current > tonumber(ARGV[2]) then
|
|
98
|
+
return {0, ttl}
|
|
99
|
+
end
|
|
100
|
+
return {1, ttl}
|
|
101
|
+
`;
|
|
102
|
+
/**
|
|
103
|
+
* Store Redis per rate limit distribuito.
|
|
104
|
+
*
|
|
105
|
+
* Usa `INCR` + `PEXPIRE` atomici. Adatto a login, reset password, 2FA,
|
|
106
|
+
* console entry e service token in deployment multi-istanza.
|
|
107
|
+
*/
|
|
108
|
+
function createRedisRateLimitStore(options) {
|
|
109
|
+
var _a;
|
|
110
|
+
if (!(options === null || options === void 0 ? void 0 : options.client) || typeof options.client.eval !== 'function') {
|
|
111
|
+
throw new Error('Redis rate limit store: client.eval non configurato.');
|
|
112
|
+
}
|
|
113
|
+
const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:rate:').replace(/:+$/, ':');
|
|
114
|
+
return {
|
|
115
|
+
consume(key, windowMs, maxAttempts) {
|
|
116
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
117
|
+
const limits = normalizeLimits(windowMs, maxAttempts);
|
|
118
|
+
const raw = yield options.client.eval(REDIS_SCRIPT, 1, `${keyPrefix}${key}`, limits.windowMs, limits.maxAttempts);
|
|
119
|
+
if (!Array.isArray(raw) || raw.length < 2) {
|
|
120
|
+
throw new Error('Redis rate limit store: risposta non valida.');
|
|
121
|
+
}
|
|
122
|
+
const allowed = Number(raw[0]) === 1;
|
|
123
|
+
const ttlMs = Number(raw[1]);
|
|
124
|
+
return {
|
|
125
|
+
allowed,
|
|
126
|
+
retryAfterSeconds: allowed
|
|
127
|
+
? 0
|
|
128
|
+
: Math.max(1, Number.isFinite(ttlMs) && ttlMs > 0 ? Math.ceil(ttlMs / 1000) : 1),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
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
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=rateLimitStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimitStore.js","sourceRoot":"","sources":["../../../src/accessi-module/security/rateLimitStore.ts"],"names":[],"mappings":";;;;;;;;;;;AAiCA,gEA+DC;AAKD,4DAEC;AA+BD,8DA0CC;AAzJD,MAAM,mBAAmB,GAAG,KAAM,CAAC;AAEnC,SAAS,eAAe,CAAC,QAAgB,EAAE,WAAmB;IAC5D,OAAO;QACL,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC3C,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;KAClD,CAAC;AACJ,CAAC;AAED,8DAA8D;AAC9D,SAAgB,0BAA0B,CACxC,aAAqB,mBAAmB;IAExC,MAAM,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;IACnD,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAE3D,MAAM,KAAK,GAAG,CAAC,GAAW,EAAQ,EAAE;QAClC,IAAI,OAAO,CAAC,IAAI,GAAG,cAAc;YAAE,OAAO;QAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACpC,IAAI,MAAM,CAAC,OAAO,IAAI,GAAG;gBAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,UAAU,GAAG,GAAS,EAAE;QAC5B,OAAO,OAAO,CAAC,IAAI,IAAI,cAAc,EAAE,CAAC;YACtC,IAAI,SAA6B,CAAC;YAClC,IAAI,UAAU,GAAG,MAAM,CAAC,iBAAiB,CAAC;YAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;gBACpC,IAAI,MAAM,CAAC,UAAU,GAAG,UAAU,EAAE,CAAC;oBACnC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;oBAC/B,SAAS,GAAG,GAAG,CAAC;gBAClB,CAAC;YACH,CAAC;YACD,IAAI,SAAS,KAAK,SAAS;gBAAE,OAAO;YACpC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACC,OAAO,CAAC,GAAW,EAAE,QAAgB,EAAE,WAAmB;;gBAC9D,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;gBACtD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBACvB,KAAK,CAAC,GAAG,CAAC,CAAC;gBAEX,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAClC,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,OAAO,IAAI,GAAG,EAAE,CAAC;oBACzC,UAAU,EAAE,CAAC;oBACb,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE;wBACf,KAAK,EAAE,CAAC;wBACR,OAAO,EAAE,GAAG,GAAG,MAAM,CAAC,QAAQ;wBAC9B,UAAU,EAAE,GAAG;qBAChB,CAAC,CAAC;oBACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;gBACjD,CAAC;gBAED,QAAQ,CAAC,UAAU,GAAG,GAAG,CAAC;gBAC1B,IAAI,QAAQ,CAAC,KAAK,IAAI,MAAM,CAAC,WAAW,EAAE,CAAC;oBACzC,OAAO;wBACL,OAAO,EAAE,KAAK;wBACd,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;qBAC3E,CAAC;gBACJ,CAAC;gBAED,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC;gBACpB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,EAAE,CAAC;YACjD,CAAC;SAAA;QACD,KAAK;YACH,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;QACK,WAAW;;gBACf,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;KACF,CAAC;AACJ,CAAC;AAED,MAAM,YAAY,GAAG,0BAA0B,EAAE,CAAC;AAElD,2FAA2F;AAC3F,SAAgB,wBAAwB;IACtC,OAAO,YAAY,CAAC;AACtB,CAAC;AAaD,MAAM,YAAY,GAAG;;;;;;;;;;CAUpB,CAAC;AAEF;;;;;GAKG;AACH,SAAgB,yBAAyB,CACvC,OAAmC;;IAEnC,IAAI,CAAC,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,MAAM,CAAA,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,MAAA,OAAO,CAAC,SAAS,mCAAI,eAAe,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAE7E,OAAO;QACC,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,WAAW;;gBACtC,MAAM,MAAM,GAAG,eAAe,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;gBACtD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CACnC,YAAY,EACZ,CAAC,EACD,GAAG,SAAS,GAAG,GAAG,EAAE,EACpB,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,WAAW,CACnB,CAAC;gBAEF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC1C,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;gBAClE,CAAC;gBAED,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;gBACrC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC7B,OAAO;oBACL,OAAO;oBACP,iBAAiB,EAAE,OAAO;wBACxB,CAAC,CAAC,CAAC;wBACH,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;iBACnF,CAAC;YACJ,CAAC;SAAA;QACK,WAAW;;gBACf,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC;oBACzE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;gBACjD,CAAC;gBAAC,WAAM,CAAC;oBACP,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;SAAA;KACF,CAAC;AACJ,CAAC","sourcesContent":["/** Esito di un controllo di rate limit. */\nexport interface RateLimitDecision {\n allowed: boolean;\n retryAfterSeconds: number;\n}\n\n/** Store pluggable per rate limit: memoria per singola istanza, Redis per cluster. */\nexport interface RateLimitStore {\n consume(key: string, windowMs: number, maxAttempts: number): Promise<RateLimitDecision>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface MemoryRateLimitStore extends RateLimitStore {\n reset(): void;\n}\n\ninterface RateLimitBucket {\n count: number;\n resetAt: number;\n lastSeenAt: number;\n}\n\nconst DEFAULT_MAX_BUCKETS = 20_000;\n\nfunction normalizeLimits(windowMs: number, maxAttempts: number): { windowMs: number; maxAttempts: number } {\n return {\n windowMs: Math.max(1, Math.trunc(windowMs)),\n maxAttempts: Math.max(1, Math.trunc(maxAttempts)),\n };\n}\n\n/** Store in memoria con finestra fissa e limite di bucket. */\nexport function createMemoryRateLimitStore(\n maxBuckets: number = DEFAULT_MAX_BUCKETS,\n): MemoryRateLimitStore {\n const buckets = new Map<string, RateLimitBucket>();\n const safeMaxBuckets = Math.max(1, Math.trunc(maxBuckets));\n\n const prune = (now: number): void => {\n if (buckets.size < safeMaxBuckets) return;\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) buckets.delete(key);\n }\n };\n\n const enforceCap = (): void => {\n while (buckets.size >= safeMaxBuckets) {\n let oldestKey: string | undefined;\n let oldestSeen = Number.POSITIVE_INFINITY;\n for (const [key, bucket] of buckets) {\n if (bucket.lastSeenAt < oldestSeen) {\n oldestSeen = bucket.lastSeenAt;\n oldestKey = key;\n }\n }\n if (oldestKey === undefined) return;\n buckets.delete(oldestKey);\n }\n };\n\n return {\n async consume(key: string, windowMs: number, maxAttempts: number): Promise<RateLimitDecision> {\n const limits = normalizeLimits(windowMs, maxAttempts);\n const now = Date.now();\n prune(now);\n\n const existing = buckets.get(key);\n if (!existing || existing.resetAt <= now) {\n enforceCap();\n buckets.set(key, {\n count: 1,\n resetAt: now + limits.windowMs,\n lastSeenAt: now,\n });\n return { allowed: true, retryAfterSeconds: 0 };\n }\n\n existing.lastSeenAt = now;\n if (existing.count >= limits.maxAttempts) {\n return {\n allowed: false,\n retryAfterSeconds: Math.max(1, Math.ceil((existing.resetAt - now) / 1000)),\n };\n }\n\n existing.count += 1;\n return { allowed: true, retryAfterSeconds: 0 };\n },\n reset() {\n buckets.clear();\n },\n async healthCheck() {\n return true;\n },\n };\n}\n\nconst defaultStore = createMemoryRateLimitStore();\n\n/** Store di default in memoria, usato quando non viene configurato uno store condiviso. */\nexport function getDefaultRateLimitStore(): MemoryRateLimitStore {\n return defaultStore;\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface RateLimitRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisRateLimitStoreOptions {\n client: RateLimitRedisClient;\n /** Default: `accessi:rate:`. */\n keyPrefix?: string;\n}\n\nconst REDIS_SCRIPT = `\nlocal current = redis.call('INCR', KEYS[1])\nif current == 1 then\n redis.call('PEXPIRE', KEYS[1], ARGV[1])\nend\nlocal ttl = redis.call('PTTL', KEYS[1])\nif current > tonumber(ARGV[2]) then\n return {0, ttl}\nend\nreturn {1, ttl}\n`;\n\n/**\n * Store Redis per rate limit distribuito.\n *\n * Usa `INCR` + `PEXPIRE` atomici. Adatto a login, reset password, 2FA,\n * console entry e service token in deployment multi-istanza.\n */\nexport function createRedisRateLimitStore(\n options: RedisRateLimitStoreOptions,\n): RateLimitStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis rate limit store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:rate:').replace(/:+$/, ':');\n\n return {\n async consume(key, windowMs, maxAttempts) {\n const limits = normalizeLimits(windowMs, maxAttempts);\n const raw = await options.client.eval(\n REDIS_SCRIPT,\n 1,\n `${keyPrefix}${key}`,\n limits.windowMs,\n limits.maxAttempts,\n );\n\n if (!Array.isArray(raw) || raw.length < 2) {\n throw new Error('Redis rate limit store: risposta non valida.');\n }\n\n const allowed = Number(raw[0]) === 1;\n const ttlMs = Number(raw[1]);\n return {\n allowed,\n retryAfterSeconds: allowed\n ? 0\n : Math.max(1, Number.isFinite(ttlMs) && ttlMs > 0 ? Math.ceil(ttlMs / 1000) : 1),\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"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { AccessiOptions } from '../AccessiModule';
|
|
2
|
+
/**
|
|
3
|
+
* Store di rotazione refresh token.
|
|
4
|
+
*
|
|
5
|
+
* `consume` e atomico: restituisce true una sola volta per `jti`, poi il token
|
|
6
|
+
* e considerato usato fino alla sua scadenza. In multi-istanza usare Redis.
|
|
7
|
+
*/
|
|
8
|
+
export interface RefreshTokenRotationStore {
|
|
9
|
+
consume(jti: string, expiresAtMs: number): Promise<boolean>;
|
|
10
|
+
/** Health check opzionale per readiness del servizio. */
|
|
11
|
+
healthCheck?(): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
export interface RefreshTokenRotationOptions {
|
|
14
|
+
/** Default: disabilitata se non viene fornito uno store. */
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
/** Store condiviso (es. Redis) per il monouso tra istanze. */
|
|
17
|
+
store?: RefreshTokenRotationStore;
|
|
18
|
+
}
|
|
19
|
+
export interface MemoryRefreshTokenRotationStore extends RefreshTokenRotationStore {
|
|
20
|
+
clear(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createMemoryRefreshTokenRotationStore(): MemoryRefreshTokenRotationStore;
|
|
23
|
+
export interface RefreshTokenRotationRedisClient {
|
|
24
|
+
eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
|
|
25
|
+
}
|
|
26
|
+
export interface RedisRefreshTokenRotationStoreOptions {
|
|
27
|
+
client: RefreshTokenRotationRedisClient;
|
|
28
|
+
/** Default: `accessi:refresh-used:`. */
|
|
29
|
+
keyPrefix?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Store Redis per refresh token monouso.
|
|
33
|
+
*
|
|
34
|
+
* Usa `SET ... PX ... NX` atomico: due refresh concorrenti dello stesso token
|
|
35
|
+
* producono un solo successo.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createRedisRefreshTokenRotationStore(options: RedisRefreshTokenRotationStoreOptions): RefreshTokenRotationStore;
|
|
38
|
+
export declare function getRefreshTokenRotationStore(options: AccessiOptions): RefreshTokenRotationStore | undefined;
|
|
39
|
+
/** Solo per test: azzera lo store in memoria di default. */
|
|
40
|
+
export declare function clearDefaultRefreshTokenRotations(): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createMemoryRefreshTokenRotationStore = createMemoryRefreshTokenRotationStore;
|
|
13
|
+
exports.createRedisRefreshTokenRotationStore = createRedisRefreshTokenRotationStore;
|
|
14
|
+
exports.getRefreshTokenRotationStore = getRefreshTokenRotationStore;
|
|
15
|
+
exports.clearDefaultRefreshTokenRotations = clearDefaultRefreshTokenRotations;
|
|
16
|
+
const CONSUME_SCRIPT = `
|
|
17
|
+
local ttl = tonumber(ARGV[2])
|
|
18
|
+
if not ttl or ttl <= 0 then return nil end
|
|
19
|
+
return redis.call('SET', KEYS[1], '1', 'PX', ttl, 'NX')
|
|
20
|
+
`;
|
|
21
|
+
function createMemoryRefreshTokenRotationStore() {
|
|
22
|
+
const used = new Map();
|
|
23
|
+
return {
|
|
24
|
+
consume(jti, expiresAtMs) {
|
|
25
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const existing = used.get(jti);
|
|
28
|
+
if (existing !== undefined && existing > now) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
used.set(jti, expiresAtMs);
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
healthCheck() {
|
|
36
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
clear() {
|
|
41
|
+
used.clear();
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Store Redis per refresh token monouso.
|
|
47
|
+
*
|
|
48
|
+
* Usa `SET ... PX ... NX` atomico: due refresh concorrenti dello stesso token
|
|
49
|
+
* producono un solo successo.
|
|
50
|
+
*/
|
|
51
|
+
function createRedisRefreshTokenRotationStore(options) {
|
|
52
|
+
var _a;
|
|
53
|
+
if (!(options === null || options === void 0 ? void 0 : options.client) || typeof options.client.eval !== 'function') {
|
|
54
|
+
throw new Error('Redis refresh rotation store: client.eval non configurato.');
|
|
55
|
+
}
|
|
56
|
+
const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:refresh-used:').replace(/:+$/, ':');
|
|
57
|
+
return {
|
|
58
|
+
consume(jti, expiresAtMs) {
|
|
59
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
60
|
+
const ttlMs = Math.max(1, Math.trunc(expiresAtMs - Date.now()));
|
|
61
|
+
const result = yield options.client.eval(CONSUME_SCRIPT, 1, `${keyPrefix}${jti}`, ttlMs);
|
|
62
|
+
return result !== null && result !== undefined && result !== false;
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
healthCheck() {
|
|
66
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
try {
|
|
68
|
+
const result = yield options.client.eval("return redis.call('PING')", 0);
|
|
69
|
+
return String(result).toUpperCase() === 'PONG';
|
|
70
|
+
}
|
|
71
|
+
catch (_a) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const defaultMemoryStore = createMemoryRefreshTokenRotationStore();
|
|
79
|
+
function getRefreshTokenRotationStore(options) {
|
|
80
|
+
var _a, _b, _c;
|
|
81
|
+
if (((_a = options.refreshTokenRotation) === null || _a === void 0 ? void 0 : _a.enabled) === false)
|
|
82
|
+
return undefined;
|
|
83
|
+
if ((_b = options.refreshTokenRotation) === null || _b === void 0 ? void 0 : _b.store)
|
|
84
|
+
return options.refreshTokenRotation.store;
|
|
85
|
+
return ((_c = options.refreshTokenRotation) === null || _c === void 0 ? void 0 : _c.enabled) === true ? defaultMemoryStore : undefined;
|
|
86
|
+
}
|
|
87
|
+
/** Solo per test: azzera lo store in memoria di default. */
|
|
88
|
+
function clearDefaultRefreshTokenRotations() {
|
|
89
|
+
defaultMemoryStore.clear();
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=refreshTokenRotation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refreshTokenRotation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/refreshTokenRotation.ts"],"names":[],"mappings":";;;;;;;;;;;AA+BA,sFAmBC;AAkBD,oFA6BC;AAID,oEAMC;AAGD,8EAEC;AAvFD,MAAM,cAAc,GAAG;;;;CAItB,CAAC;AAEF,SAAgB,qCAAqC;IACnD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,OAAO;QACC,OAAO,CAAC,GAAG,EAAE,WAAW;;gBAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAC/B,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,GAAG,GAAG,EAAE,CAAC;oBAC7C,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;gBAC3B,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;QACK,WAAW;;gBACf,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;QACD,KAAK;YACH,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;KACF,CAAC;AACJ,CAAC;AAYD;;;;;GAKG;AACH,SAAgB,oCAAoC,CAClD,OAA8C;;IAE9C,IAAI,CAAC,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,MAAM,CAAA,IAAI,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,SAAS,GAAG,CAAC,MAAA,OAAO,CAAC,SAAS,mCAAI,uBAAuB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAErF,OAAO;QACC,OAAO,CAAC,GAAG,EAAE,WAAW;;gBAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBAChE,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CACtC,cAAc,EACd,CAAC,EACD,GAAG,SAAS,GAAG,GAAG,EAAE,EACpB,KAAK,CACN,CAAC;gBACF,OAAO,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,KAAK,CAAC;YACrE,CAAC;SAAA;QACK,WAAW;;gBACf,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC;oBACzE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;gBACjD,CAAC;gBAAC,WAAM,CAAC;oBACP,OAAO,KAAK,CAAC;gBACf,CAAC;YACH,CAAC;SAAA;KACF,CAAC;AACJ,CAAC;AAED,MAAM,kBAAkB,GAAG,qCAAqC,EAAE,CAAC;AAEnE,SAAgB,4BAA4B,CAC1C,OAAuB;;IAEvB,IAAI,CAAA,MAAA,OAAO,CAAC,oBAAoB,0CAAE,OAAO,MAAK,KAAK;QAAE,OAAO,SAAS,CAAC;IACtE,IAAI,MAAA,OAAO,CAAC,oBAAoB,0CAAE,KAAK;QAAE,OAAO,OAAO,CAAC,oBAAoB,CAAC,KAAK,CAAC;IACnF,OAAO,CAAA,MAAA,OAAO,CAAC,oBAAoB,0CAAE,OAAO,MAAK,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,SAAS,CAAC;AACzF,CAAC;AAED,4DAA4D;AAC5D,SAAgB,iCAAiC;IAC/C,kBAAkB,CAAC,KAAK,EAAE,CAAC;AAC7B,CAAC","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\n\n/**\n * Store di rotazione refresh token.\n *\n * `consume` e atomico: restituisce true una sola volta per `jti`, poi il token\n * e considerato usato fino alla sua scadenza. In multi-istanza usare Redis.\n */\nexport interface RefreshTokenRotationStore {\n consume(jti: string, expiresAtMs: number): Promise<boolean>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface RefreshTokenRotationOptions {\n /** Default: disabilitata se non viene fornito uno store. */\n enabled?: boolean;\n /** Store condiviso (es. Redis) per il monouso tra istanze. */\n store?: RefreshTokenRotationStore;\n}\n\nexport interface MemoryRefreshTokenRotationStore extends RefreshTokenRotationStore {\n clear(): void;\n}\n\nconst CONSUME_SCRIPT = `\nlocal ttl = tonumber(ARGV[2])\nif not ttl or ttl <= 0 then return nil end\nreturn redis.call('SET', KEYS[1], '1', 'PX', ttl, 'NX')\n`;\n\nexport function createMemoryRefreshTokenRotationStore(): MemoryRefreshTokenRotationStore {\n const used = new Map<string, number>();\n return {\n async consume(jti, expiresAtMs) {\n const now = Date.now();\n const existing = used.get(jti);\n if (existing !== undefined && existing > now) {\n return false;\n }\n used.set(jti, expiresAtMs);\n return true;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n used.clear();\n },\n };\n}\n\nexport interface RefreshTokenRotationRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisRefreshTokenRotationStoreOptions {\n client: RefreshTokenRotationRedisClient;\n /** Default: `accessi:refresh-used:`. */\n keyPrefix?: string;\n}\n\n/**\n * Store Redis per refresh token monouso.\n *\n * Usa `SET ... PX ... NX` atomico: due refresh concorrenti dello stesso token\n * producono un solo successo.\n */\nexport function createRedisRefreshTokenRotationStore(\n options: RedisRefreshTokenRotationStoreOptions,\n): RefreshTokenRotationStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis refresh rotation store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:refresh-used:').replace(/:+$/, ':');\n\n return {\n async consume(jti, expiresAtMs) {\n const ttlMs = Math.max(1, Math.trunc(expiresAtMs - Date.now()));\n const result = await options.client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${jti}`,\n ttlMs,\n );\n return result !== null && result !== undefined && result !== false;\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\nconst defaultMemoryStore = createMemoryRefreshTokenRotationStore();\n\nexport function getRefreshTokenRotationStore(\n options: AccessiOptions,\n): RefreshTokenRotationStore | undefined {\n if (options.refreshTokenRotation?.enabled === false) return undefined;\n if (options.refreshTokenRotation?.store) return options.refreshTokenRotation.store;\n return options.refreshTokenRotation?.enabled === true ? defaultMemoryStore : undefined;\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultRefreshTokenRotations(): void {\n defaultMemoryStore.clear();\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CanActivate, ExecutionContext } from '@nestjs/common';
|
|
2
2
|
import { Reflector } from '@nestjs/core';
|
|
3
3
|
import type { Request } from 'express';
|
|
4
|
+
import type { AccessiOptions } from '../AccessiModule';
|
|
4
5
|
import { ServiceTokenService, VerifiedServiceToken } from '../Services/ServiceTokenService/ServiceTokenService';
|
|
5
6
|
/** Chiave dei metadata usata da `@RequireServiceTokenScopes`. */
|
|
6
7
|
export declare const SERVICE_TOKEN_SCOPES_METADATA = "accessi:service-token-scopes";
|
|
@@ -20,6 +21,7 @@ export declare function getAccessiServiceToken(req: Request): VerifiedServiceTok
|
|
|
20
21
|
export declare class ServiceTokenGuard implements CanActivate {
|
|
21
22
|
private readonly serviceTokenService;
|
|
22
23
|
private readonly reflector;
|
|
23
|
-
|
|
24
|
+
private readonly accessiOptions;
|
|
25
|
+
constructor(serviceTokenService: ServiceTokenService, reflector: Reflector, accessiOptions: AccessiOptions);
|
|
24
26
|
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
25
27
|
}
|
|
@@ -8,6 +8,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
11
14
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
12
15
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
13
16
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -45,16 +48,17 @@ function getAccessiServiceToken(req) {
|
|
|
45
48
|
* con `@RequireServiceTokenScopes('ia')`.
|
|
46
49
|
*/
|
|
47
50
|
let ServiceTokenGuard = class ServiceTokenGuard {
|
|
48
|
-
constructor(serviceTokenService, reflector) {
|
|
51
|
+
constructor(serviceTokenService, reflector, accessiOptions) {
|
|
49
52
|
this.serviceTokenService = serviceTokenService;
|
|
50
53
|
this.reflector = reflector;
|
|
54
|
+
this.accessiOptions = accessiOptions;
|
|
51
55
|
}
|
|
52
56
|
canActivate(context) {
|
|
53
57
|
return __awaiter(this, void 0, void 0, function* () {
|
|
54
|
-
var _a, _b, _c, _d;
|
|
58
|
+
var _a, _b, _c, _d, _e;
|
|
55
59
|
const request = context.switchToHttp().getRequest();
|
|
56
60
|
const ip = (_c = (_a = request.ip) !== null && _a !== void 0 ? _a : (_b = request.socket) === null || _b === void 0 ? void 0 : _b.remoteAddress) !== null && _c !== void 0 ? _c : 'unknown';
|
|
57
|
-
(0, serviceTokenRateLimit_1.assertServiceTokenRateLimit)(ip);
|
|
61
|
+
yield (0, serviceTokenRateLimit_1.assertServiceTokenRateLimit)(ip, (_d = this.accessiOptions.serviceTokens) === null || _d === void 0 ? void 0 : _d.rateLimitStore);
|
|
58
62
|
const token = (0, authenticatedToken_1.extractAccessiBearerToken)(request.headers['authorization']);
|
|
59
63
|
if (!token) {
|
|
60
64
|
throw new common_1.UnauthorizedException('Token di servizio mancante.');
|
|
@@ -64,10 +68,10 @@ let ServiceTokenGuard = class ServiceTokenGuard {
|
|
|
64
68
|
throw new common_1.UnauthorizedException('Token di servizio non valido, scaduto o revocato.');
|
|
65
69
|
}
|
|
66
70
|
request.accessiServiceToken = verified;
|
|
67
|
-
const requiredScopes = (
|
|
71
|
+
const requiredScopes = (_e = this.reflector.getAllAndOverride(exports.SERVICE_TOKEN_SCOPES_METADATA, [
|
|
68
72
|
context.getHandler(),
|
|
69
73
|
context.getClass(),
|
|
70
|
-
])) !== null &&
|
|
74
|
+
])) !== null && _e !== void 0 ? _e : [];
|
|
71
75
|
const missingScope = requiredScopes.find((scope) => !(0, ServiceTokenService_1.serviceTokenHasScope)(verified.scopes, scope));
|
|
72
76
|
if (missingScope) {
|
|
73
77
|
throw new common_1.ForbiddenException(`Scope '${missingScope}' non autorizzato per questo token.`);
|
|
@@ -79,7 +83,8 @@ let ServiceTokenGuard = class ServiceTokenGuard {
|
|
|
79
83
|
exports.ServiceTokenGuard = ServiceTokenGuard;
|
|
80
84
|
exports.ServiceTokenGuard = ServiceTokenGuard = __decorate([
|
|
81
85
|
(0, common_1.Injectable)(),
|
|
86
|
+
__param(2, (0, common_1.Inject)('ACCESSI_OPTIONS')),
|
|
82
87
|
__metadata("design:paramtypes", [ServiceTokenService_1.ServiceTokenService,
|
|
83
|
-
core_1.Reflector])
|
|
88
|
+
core_1.Reflector, Object])
|
|
84
89
|
], ServiceTokenGuard);
|
|
85
90
|
//# sourceMappingURL=serviceTokenGuard.js.map
|
|
@@ -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"]}
|