emilsoftware-utilities 2.0.0-dev.77 → 2.0.0-dev.78
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/token.js +2 -0
- package/dist/accessi/resource-server/token.js.map +1 -1
- package/dist/accessi/server/standalone.js +2 -1
- package/dist/accessi/server/standalone.js.map +1 -1
- package/dist/accessi-module/AccessiModule.d.ts +3 -0
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/console.js +1 -0
- package/dist/accessi-module/Console/wiki.js +1 -0
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Controllers/AccessiIntrospectionController.js +9 -0
- package/dist/accessi-module/Controllers/AccessiIntrospectionController.js.map +1 -1
- package/dist/accessi-module/Controllers/AuthController.js +67 -8
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Controllers/ConsoleEntryController.js +9 -0
- package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
- package/dist/accessi-module/Services/AuthService/AuthService.js +8 -2
- package/dist/accessi-module/Services/AuthService/AuthService.js.map +1 -1
- package/dist/accessi-module/index.d.ts +4 -0
- package/dist/accessi-module/index.js +15 -1
- package/dist/accessi-module/index.js.map +1 -1
- package/dist/accessi-module/observability/metrics.d.ts +79 -0
- package/dist/accessi-module/observability/metrics.js +216 -0
- package/dist/accessi-module/observability/metrics.js.map +1 -0
- package/dist/accessi-module/observability/redisMetrics.d.ts +12 -0
- package/dist/accessi-module/observability/redisMetrics.js +44 -0
- package/dist/accessi-module/observability/redisMetrics.js.map +1 -0
- package/dist/accessi-module/security/accessTokenRevocation.js +5 -3
- package/dist/accessi-module/security/accessTokenRevocation.js.map +1 -1
- package/dist/accessi-module/security/consoleEntry.js +5 -3
- package/dist/accessi-module/security/consoleEntry.js.map +1 -1
- package/dist/accessi-module/security/edgeRateLimit.js +3 -0
- package/dist/accessi-module/security/edgeRateLimit.js.map +1 -1
- package/dist/accessi-module/security/jwks.js +3 -0
- package/dist/accessi-module/security/jwks.js.map +1 -1
- package/dist/accessi-module/security/jwtTokenOptions.js +2 -0
- package/dist/accessi-module/security/jwtTokenOptions.js.map +1 -1
- package/dist/accessi-module/security/publicAuthRateLimit.js +3 -0
- package/dist/accessi-module/security/publicAuthRateLimit.js.map +1 -1
- package/dist/accessi-module/security/rateLimitStore.js +4 -2
- package/dist/accessi-module/security/rateLimitStore.js.map +1 -1
- package/dist/accessi-module/security/refreshTokenRotation.js +7 -5
- package/dist/accessi-module/security/refreshTokenRotation.js.map +1 -1
- package/dist/accessi-module/security/serviceTokenRateLimit.js +3 -0
- package/dist/accessi-module/security/serviceTokenRateLimit.js.map +1 -1
- package/dist/accessi-module/security/sessionRevocation.js +7 -3
- package/dist/accessi-module/security/sessionRevocation.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.78.tgz +0 -0
- package/docs/metrics.md +153 -0
- package/docs/redis-load-testing.md +88 -0
- package/package.json +4 -3
- package/dist/emilsoftware-utilities-2.0.0-dev.77.tgz +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwks.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwks.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"jwks.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwks.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, createPublicKey } from \"crypto\";\nimport type { JsonWebKey } from \"crypto\";\nimport type { Request, Response } from \"express\";\nimport type { AccessiOptions } from \"../AccessiModule\";\nimport { recordAccessiMetric } from '../observability/metrics';\n\n/** Chiave pubblica in formato JWK, pronta per una risposta JWKS. */\nexport type AccessiJwksKey = JsonWebKey & {\n kid: string;\n alg: string;\n use: \"sig\";\n};\n\nexport interface AccessiJwksDocument {\n keys: AccessiJwksKey[];\n}\n\nconst DEFAULT_JWKS_PATH = \"accessi/.well-known/jwks.json\";\n\nfunction normalizePath(value: string): string {\n return `/${value.replace(/^\\/+|\\/+$/g, \"\")}`;\n}\n\nfunction fingerprintPublicKey(publicKey: string | Buffer): string {\n const der = createPublicKey(publicKey).export({ type: \"spki\", format: \"der\" });\n return createHash(\"sha256\").update(der).digest(\"base64url\").slice(0, 16);\n}\n\nfunction toJwksKey(\n publicKey: string | Buffer,\n keyId: string | undefined,\n algorithm: string,\n): AccessiJwksKey {\n const jwk = createPublicKey(publicKey).export({ format: \"jwk\" }) as JsonWebKey;\n return {\n ...jwk,\n kid: keyId ?? fingerprintPublicKey(publicKey),\n alg: algorithm,\n use: \"sig\",\n };\n}\n\n/** Path pubblico della JWKS, oppure `undefined` quando si usa HS256. */\nexport function getAccessiJwksPath(options: AccessiOptions): string | undefined {\n const algorithm = options.jwtOptions.algorithm ?? \"HS256\";\n if (algorithm.startsWith(\"HS\")) return undefined;\n return normalizePath(options.jwtOptions.jwksPath ?? DEFAULT_JWKS_PATH);\n}\n\n/**\n * Costruisce la JWKS pubblica con chiave corrente e chiavi precedenti.\n *\n * Restituisce `null` per HS256 o quando non e configurata una chiave\n * pubblica/privata asimmetrica.\n */\nexport function buildAccessiJwks(options: AccessiOptions): AccessiJwksDocument | null {\n const algorithm = options.jwtOptions.algorithm ?? \"HS256\";\n if (algorithm.startsWith(\"HS\")) return null;\n\n const currentPublicKey = options.jwtOptions.publicKey ?? options.jwtOptions.privateKey;\n if (!currentPublicKey) return null;\n\n const keys: AccessiJwksKey[] = [\n toJwksKey(currentPublicKey, options.jwtOptions.keyId, algorithm),\n ];\n\n for (const previous of options.jwtOptions.previousPublicKeys ?? []) {\n const previousAlgorithm = previous.algorithm ?? algorithm;\n keys.push(toJwksKey(previous.publicKey, previous.keyId, previousAlgorithm));\n }\n\n // Deduplica per kid: la chiave corrente vince sulle precedenti.\n const unique = new Map<string, AccessiJwksKey>();\n for (const key of keys) {\n if (!unique.has(key.kid)) unique.set(key.kid, key);\n }\n\n return { keys: [...unique.values()] };\n}\n\n/** Handler Express per la JWKS. Risponde 404 se la configurazione e HS256. */\nexport function createAccessiJwksHandler(\n options: AccessiOptions,\n): (req: Request, res: Response) => Response | void {\n return (_req: Request, res: Response): Response | void => {\n const document = buildAccessiJwks(options);\n if (!document) {\n recordAccessiMetric('accessi_jwks_requests_total', 1, { result: 'unavailable' });\n return res.status(404).json({\n severity: \"error\",\n status: 404,\n statusCode: 2,\n code: \"ACCESSI_JWKS_NOT_AVAILABLE\",\n error: \"ACCESSI_JWKS_NOT_AVAILABLE\",\n message: \"JWKS non disponibile: Accessi usa HS256 o non ha una chiave asimmetrica.\",\n });\n }\n\n res.setHeader(\"Content-Type\", \"application/json\");\n res.setHeader(\"Cache-Control\", \"public, max-age=300\");\n recordAccessiMetric('accessi_jwks_requests_total', 1, { result: 'ok' });\n return res.status(200).send(document);\n };\n}\n\n/**\n * Monta la route JWKS sull'Express host.\n *\n * Per un'app Nest esistente passare `app.getHttpAdapter().getInstance()`.\n * Restituisce il path montato, oppure `undefined` se HS256.\n */\nexport interface AccessiJwksExpressApp {\n get(path: string, handler: (req: Request, res: Response) => unknown): unknown;\n}\n\nexport function setupAccessiJwks(\n app: AccessiJwksExpressApp,\n options: AccessiOptions,\n): string | undefined {\n const path = getAccessiJwksPath(options);\n if (!path) return undefined;\n app.get(path, createAccessiJwksHandler(options));\n return path;\n}\n"]}
|
|
@@ -41,6 +41,7 @@ exports.getAccessiRefreshTokenSigningConfig = getAccessiRefreshTokenSigningConfi
|
|
|
41
41
|
exports.getAccessiRefreshTokenVerificationConfig = getAccessiRefreshTokenVerificationConfig;
|
|
42
42
|
const jwt = __importStar(require("jsonwebtoken"));
|
|
43
43
|
const passwordResetToken_1 = require("./passwordResetToken");
|
|
44
|
+
const metrics_1 = require("../observability/metrics");
|
|
44
45
|
function resolveAlgorithm(options) {
|
|
45
46
|
var _a;
|
|
46
47
|
return (_a = options.jwtOptions.algorithm) !== null && _a !== void 0 ? _a : "HS256";
|
|
@@ -128,6 +129,7 @@ function resolveAccessiVerificationKeys(config, token) {
|
|
|
128
129
|
if (key)
|
|
129
130
|
return [key];
|
|
130
131
|
(_b = config.onUnknownKeyId) === null || _b === void 0 ? void 0 : _b.call(config, Object.assign(Object.assign({ keyId: header.keyId }, (header.algorithm ? { algorithm: header.algorithm } : {})), { purpose: config.purpose }));
|
|
132
|
+
(0, metrics_1.recordAccessiMetric)('accessi_jwt_unknown_key_id_total', 1, Object.assign({ purpose: config.purpose }, (header.algorithm ? { algorithm: header.algorithm } : {})));
|
|
131
133
|
return [];
|
|
132
134
|
}
|
|
133
135
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwtTokenOptions.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwtTokenOptions.ts"],"names":[],"mappings":"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","sourcesContent":["import * as jwt from \"jsonwebtoken\";\nimport type { AccessiOptions } from \"../AccessiModule\";\nimport { getAccessiJwtSecret } from \"./passwordResetToken\";\nimport type { AccessiJwtPurpose } from \"./passwordResetToken\";\n\n/** Algoritmi supportati per gli access token Accessi. */\nexport type AccessiAccessTokenAlgorithm =\n | \"HS256\"\n | \"HS384\"\n | \"HS512\"\n | \"RS256\"\n | \"RS384\"\n | \"RS512\"\n | \"ES256\"\n | \"ES384\"\n | \"ES512\";\n\n/**\n * Evento emesso quando un token presenta un `kid` che non appartiene né alla\n * chiave corrente né alle chiavi precedenti configurate.\n *\n * Serve per collegare un alert operativo (log, metrica, webhook) senza\n * accoppiare il modulo a uno specifico sistema di monitoraggio.\n */\nexport interface AccessiUnknownKeyIdEvent {\n keyId: string;\n algorithm?: string;\n /** `access` o `refresh`, per distinguere i token coinvolti. */\n purpose: AccessiJwtPurpose;\n}\n\n/** Configurazione di verifica con chiave corrente, chiavi precedenti e alert. */\nexport interface AccessiJwtVerificationConfig {\n /** Chiave corrente. Resta il campo principale per i consumer esistenti. */\n key: jwt.Secret;\n verifyOptions: jwt.VerifyOptions;\n /** Chiavi indicizzate per `kid`: la corrente vince sulle precedenti. */\n keysByKeyId: ReadonlyMap<string, jwt.Secret>;\n /** Chiavi provate per i token legacy privi di `kid`, corrente per prima. */\n fallbackKeys: readonly jwt.Secret[];\n purpose: AccessiJwtPurpose;\n onUnknownKeyId?: (event: AccessiUnknownKeyIdEvent) => void;\n}\n\nfunction resolveAlgorithm(options: AccessiOptions): AccessiAccessTokenAlgorithm {\n return options.jwtOptions.algorithm ?? \"HS256\";\n}\n\n/** Configurazione di firma: chiave, algoritmo e claim standard. */\nfunction getTokenSigningConfig(\n options: AccessiOptions,\n purpose: AccessiJwtPurpose,\n): {\n key: jwt.Secret;\n algorithm: AccessiAccessTokenAlgorithm;\n signOptions: Pick<jwt.SignOptions, \"issuer\" | \"audience\" | \"keyid\">;\n} {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options, purpose)\n : options.jwtOptions.privateKey;\n\n if (!key) {\n throw new Error(`jwtOptions.privateKey e obbligatoria per l'algoritmo ${algorithm}.`);\n }\n\n return {\n key,\n algorithm,\n signOptions: {\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.SignOptions[\"audience\"] }\n : {}),\n ...(options.jwtOptions.keyId ? { keyid: options.jwtOptions.keyId } : {}),\n },\n };\n}\n\n/**\n * Configurazione di verifica.\n *\n * Con algoritmi asimmetrici accetta anche le chiavi precedenti: un token\n * firmato prima di una rotazione resta valido finché la sua chiave pubblica\n * compare in `previousPublicKeys`. I token legacy senza `kid` vengono provati\n * su tutte le chiavi candidate, la corrente per prima.\n */\nfunction getTokenVerificationConfig(\n options: AccessiOptions,\n purpose: AccessiJwtPurpose,\n): AccessiJwtVerificationConfig {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options, purpose)\n : (options.jwtOptions.publicKey ?? options.jwtOptions.privateKey);\n\n if (!key) {\n throw new Error(\n `jwtOptions.publicKey e obbligatoria per verificare token ${algorithm}.`,\n );\n }\n\n const algorithms = new Set<string>([algorithm]);\n const keysByKeyId = new Map<string, jwt.Secret>();\n const fallbackKeys: jwt.Secret[] = [key];\n\n if (options.jwtOptions.keyId) {\n keysByKeyId.set(options.jwtOptions.keyId, key);\n }\n\n if (!algorithm.startsWith(\"HS\")) {\n for (const previous of options.jwtOptions.previousPublicKeys ?? []) {\n const previousAlgorithm = previous.algorithm ?? algorithm;\n algorithms.add(previousAlgorithm);\n fallbackKeys.push(previous.publicKey);\n if (previous.keyId && !keysByKeyId.has(previous.keyId)) {\n keysByKeyId.set(previous.keyId, previous.publicKey);\n }\n }\n }\n\n return {\n key,\n verifyOptions: {\n algorithms: [...algorithms] as jwt.Algorithm[],\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.VerifyOptions[\"audience\"] }\n : {}),\n },\n keysByKeyId,\n fallbackKeys,\n purpose,\n ...(options.jwtOptions.onUnknownKeyId\n ? { onUnknownKeyId: options.jwtOptions.onUnknownKeyId }\n : {}),\n };\n}\n\nfunction readTokenHeader(token: string): { keyId?: string; algorithm?: string } {\n const decoded = jwt.decode(token, { complete: true });\n if (!decoded || typeof decoded === \"string\" || !decoded.header) return {};\n const header = decoded.header as { kid?: unknown; alg?: unknown };\n return {\n ...(typeof header.kid === \"string\" && header.kid !== \"\" ? { keyId: header.kid } : {}),\n ...(typeof header.alg === \"string\" ? { algorithm: header.alg } : {}),\n };\n}\n\n/**\n * Chiavi da provare per un token.\n *\n * Un `kid` noto seleziona una sola chiave: non si ricade sulle altre, così un\n * token con intestazione manomessa non viene verificato per tentativi.\n */\nexport function resolveAccessiVerificationKeys(\n config: AccessiJwtVerificationConfig,\n token: string,\n): readonly jwt.Secret[] {\n const header = readTokenHeader(token);\n // Con HMAC il kid non partecipa alla crittografia: un kid cambiato non deve\n // invalidare token firmati con lo stesso segreto.\n const hmacOnly = (config.verifyOptions.algorithms ?? []).every((algorithm) =>\n String(algorithm).startsWith(\"HS\"),\n );\n if (!header.keyId || hmacOnly) return config.fallbackKeys;\n\n const key = config.keysByKeyId.get(header.keyId);\n if (key) return [key];\n\n config.onUnknownKeyId?.({\n keyId: header.keyId,\n ...(header.algorithm ? { algorithm: header.algorithm } : {}),\n purpose: config.purpose,\n });\n return [];\n}\n\n/**\n * Verifica un JWT Accessi con la chiave corretta (corrente o precedente).\n *\n * Lancia l'errore di `jsonwebtoken` quando nessuna chiave candidata verifica il\n * token; per un `kid` sconosciuto lancia un errore dedicato dopo aver emesso\n * l'evento di alert.\n */\nexport function verifyAccessiJwt(token: string, config: AccessiJwtVerificationConfig): unknown {\n const keys = resolveAccessiVerificationKeys(config, token);\n if (keys.length === 0) {\n const { keyId } = readTokenHeader(token);\n throw new Error(`Chiave JWT sconosciuta per kid \"${keyId ?? \"assente\"}\".`);\n }\n\n let lastError: unknown;\n for (const key of keys) {\n try {\n return jwt.verify(token, key, config.verifyOptions);\n } catch (error) {\n lastError = error;\n }\n }\n\n throw lastError instanceof Error ? lastError : new Error(\"Token JWT non valido.\");\n}\n\nexport function getAccessiAccessTokenSigningConfig(options: AccessiOptions) {\n return getTokenSigningConfig(options, \"access\");\n}\n\nexport function getAccessiAccessTokenVerificationConfig(options: AccessiOptions) {\n return getTokenVerificationConfig(options, \"access\");\n}\n\nexport function getAccessiRefreshTokenSigningConfig(options: AccessiOptions) {\n return getTokenSigningConfig(options, \"refresh\");\n}\n\nexport function getAccessiRefreshTokenVerificationConfig(options: AccessiOptions) {\n return getTokenVerificationConfig(options, \"refresh\");\n}\n"]}
|
|
1
|
+
{"version":3,"file":"jwtTokenOptions.js","sourceRoot":"","sources":["../../../src/accessi-module/security/jwtTokenOptions.ts"],"names":[],"mappings":"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","sourcesContent":["import * as jwt from \"jsonwebtoken\";\nimport type { AccessiOptions } from \"../AccessiModule\";\nimport { getAccessiJwtSecret } from \"./passwordResetToken\";\nimport type { AccessiJwtPurpose } from \"./passwordResetToken\";\nimport { recordAccessiMetric } from \"../observability/metrics\";\n\n/** Algoritmi supportati per gli access token Accessi. */\nexport type AccessiAccessTokenAlgorithm =\n | \"HS256\"\n | \"HS384\"\n | \"HS512\"\n | \"RS256\"\n | \"RS384\"\n | \"RS512\"\n | \"ES256\"\n | \"ES384\"\n | \"ES512\";\n\n/**\n * Evento emesso quando un token presenta un `kid` che non appartiene né alla\n * chiave corrente né alle chiavi precedenti configurate.\n *\n * Serve per collegare un alert operativo (log, metrica, webhook) senza\n * accoppiare il modulo a uno specifico sistema di monitoraggio.\n */\nexport interface AccessiUnknownKeyIdEvent {\n keyId: string;\n algorithm?: string;\n /** `access` o `refresh`, per distinguere i token coinvolti. */\n purpose: AccessiJwtPurpose;\n}\n\n/** Configurazione di verifica con chiave corrente, chiavi precedenti e alert. */\nexport interface AccessiJwtVerificationConfig {\n /** Chiave corrente. Resta il campo principale per i consumer esistenti. */\n key: jwt.Secret;\n verifyOptions: jwt.VerifyOptions;\n /** Chiavi indicizzate per `kid`: la corrente vince sulle precedenti. */\n keysByKeyId: ReadonlyMap<string, jwt.Secret>;\n /** Chiavi provate per i token legacy privi di `kid`, corrente per prima. */\n fallbackKeys: readonly jwt.Secret[];\n purpose: AccessiJwtPurpose;\n onUnknownKeyId?: (event: AccessiUnknownKeyIdEvent) => void;\n}\n\nfunction resolveAlgorithm(options: AccessiOptions): AccessiAccessTokenAlgorithm {\n return options.jwtOptions.algorithm ?? \"HS256\";\n}\n\n/** Configurazione di firma: chiave, algoritmo e claim standard. */\nfunction getTokenSigningConfig(\n options: AccessiOptions,\n purpose: AccessiJwtPurpose,\n): {\n key: jwt.Secret;\n algorithm: AccessiAccessTokenAlgorithm;\n signOptions: Pick<jwt.SignOptions, \"issuer\" | \"audience\" | \"keyid\">;\n} {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options, purpose)\n : options.jwtOptions.privateKey;\n\n if (!key) {\n throw new Error(`jwtOptions.privateKey e obbligatoria per l'algoritmo ${algorithm}.`);\n }\n\n return {\n key,\n algorithm,\n signOptions: {\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.SignOptions[\"audience\"] }\n : {}),\n ...(options.jwtOptions.keyId ? { keyid: options.jwtOptions.keyId } : {}),\n },\n };\n}\n\n/**\n * Configurazione di verifica.\n *\n * Con algoritmi asimmetrici accetta anche le chiavi precedenti: un token\n * firmato prima di una rotazione resta valido finché la sua chiave pubblica\n * compare in `previousPublicKeys`. I token legacy senza `kid` vengono provati\n * su tutte le chiavi candidate, la corrente per prima.\n */\nfunction getTokenVerificationConfig(\n options: AccessiOptions,\n purpose: AccessiJwtPurpose,\n): AccessiJwtVerificationConfig {\n const algorithm = resolveAlgorithm(options);\n const key = algorithm.startsWith(\"HS\")\n ? getAccessiJwtSecret(options, purpose)\n : (options.jwtOptions.publicKey ?? options.jwtOptions.privateKey);\n\n if (!key) {\n throw new Error(\n `jwtOptions.publicKey e obbligatoria per verificare token ${algorithm}.`,\n );\n }\n\n const algorithms = new Set<string>([algorithm]);\n const keysByKeyId = new Map<string, jwt.Secret>();\n const fallbackKeys: jwt.Secret[] = [key];\n\n if (options.jwtOptions.keyId) {\n keysByKeyId.set(options.jwtOptions.keyId, key);\n }\n\n if (!algorithm.startsWith(\"HS\")) {\n for (const previous of options.jwtOptions.previousPublicKeys ?? []) {\n const previousAlgorithm = previous.algorithm ?? algorithm;\n algorithms.add(previousAlgorithm);\n fallbackKeys.push(previous.publicKey);\n if (previous.keyId && !keysByKeyId.has(previous.keyId)) {\n keysByKeyId.set(previous.keyId, previous.publicKey);\n }\n }\n }\n\n return {\n key,\n verifyOptions: {\n algorithms: [...algorithms] as jwt.Algorithm[],\n ...(options.jwtOptions.issuer ? { issuer: options.jwtOptions.issuer } : {}),\n ...(options.jwtOptions.audience\n ? { audience: options.jwtOptions.audience as jwt.VerifyOptions[\"audience\"] }\n : {}),\n },\n keysByKeyId,\n fallbackKeys,\n purpose,\n ...(options.jwtOptions.onUnknownKeyId\n ? { onUnknownKeyId: options.jwtOptions.onUnknownKeyId }\n : {}),\n };\n}\n\nfunction readTokenHeader(token: string): { keyId?: string; algorithm?: string } {\n const decoded = jwt.decode(token, { complete: true });\n if (!decoded || typeof decoded === \"string\" || !decoded.header) return {};\n const header = decoded.header as { kid?: unknown; alg?: unknown };\n return {\n ...(typeof header.kid === \"string\" && header.kid !== \"\" ? { keyId: header.kid } : {}),\n ...(typeof header.alg === \"string\" ? { algorithm: header.alg } : {}),\n };\n}\n\n/**\n * Chiavi da provare per un token.\n *\n * Un `kid` noto seleziona una sola chiave: non si ricade sulle altre, così un\n * token con intestazione manomessa non viene verificato per tentativi.\n */\nexport function resolveAccessiVerificationKeys(\n config: AccessiJwtVerificationConfig,\n token: string,\n): readonly jwt.Secret[] {\n const header = readTokenHeader(token);\n // Con HMAC il kid non partecipa alla crittografia: un kid cambiato non deve\n // invalidare token firmati con lo stesso segreto.\n const hmacOnly = (config.verifyOptions.algorithms ?? []).every((algorithm) =>\n String(algorithm).startsWith(\"HS\"),\n );\n if (!header.keyId || hmacOnly) return config.fallbackKeys;\n\n const key = config.keysByKeyId.get(header.keyId);\n if (key) return [key];\n\n config.onUnknownKeyId?.({\n keyId: header.keyId,\n ...(header.algorithm ? { algorithm: header.algorithm } : {}),\n purpose: config.purpose,\n });\n recordAccessiMetric('accessi_jwt_unknown_key_id_total', 1, {\n purpose: config.purpose,\n ...(header.algorithm ? { algorithm: header.algorithm } : {}),\n });\n return [];\n}\n\n/**\n * Verifica un JWT Accessi con la chiave corretta (corrente o precedente).\n *\n * Lancia l'errore di `jsonwebtoken` quando nessuna chiave candidata verifica il\n * token; per un `kid` sconosciuto lancia un errore dedicato dopo aver emesso\n * l'evento di alert.\n */\nexport function verifyAccessiJwt(token: string, config: AccessiJwtVerificationConfig): unknown {\n const keys = resolveAccessiVerificationKeys(config, token);\n if (keys.length === 0) {\n const { keyId } = readTokenHeader(token);\n throw new Error(`Chiave JWT sconosciuta per kid \"${keyId ?? \"assente\"}\".`);\n }\n\n let lastError: unknown;\n for (const key of keys) {\n try {\n return jwt.verify(token, key, config.verifyOptions);\n } catch (error) {\n lastError = error;\n }\n }\n\n throw lastError instanceof Error ? lastError : new Error(\"Token JWT non valido.\");\n}\n\nexport function getAccessiAccessTokenSigningConfig(options: AccessiOptions) {\n return getTokenSigningConfig(options, \"access\");\n}\n\nexport function getAccessiAccessTokenVerificationConfig(options: AccessiOptions) {\n return getTokenVerificationConfig(options, \"access\");\n}\n\nexport function getAccessiRefreshTokenSigningConfig(options: AccessiOptions) {\n return getTokenSigningConfig(options, \"refresh\");\n}\n\nexport function getAccessiRefreshTokenVerificationConfig(options: AccessiOptions) {\n return getTokenVerificationConfig(options, \"refresh\");\n}\n"]}
|
|
@@ -14,6 +14,7 @@ exports.sendPublicAuthRateLimitExceeded = sendPublicAuthRateLimitExceeded;
|
|
|
14
14
|
const common_1 = require("@nestjs/common");
|
|
15
15
|
const Utilities_1 = require("../../Utilities");
|
|
16
16
|
const rateLimitStore_1 = require("./rateLimitStore");
|
|
17
|
+
const metrics_1 = require("../observability/metrics");
|
|
17
18
|
const DEFAULT_PUBLIC_AUTH_RATE_LIMITS = {
|
|
18
19
|
login: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },
|
|
19
20
|
twoFactorVerify: { windowMs: 15 * 60 * 1000, maxAttempts: 20 },
|
|
@@ -80,6 +81,7 @@ function checkPublicAuthRateLimit(options_1, scope_1, req_1) {
|
|
|
80
81
|
// evitando crescita di memoria con identificatori sempre diversi.
|
|
81
82
|
const ipDecision = yield consumeRateLimit(store, `${scope}|ip|${ipAddress}`, rule);
|
|
82
83
|
if (!ipDecision.allowed) {
|
|
84
|
+
(0, metrics_1.countAccessiMetric)(options, 'rate_limit_blocked_total', { scope, layer: 'public' });
|
|
83
85
|
return ipDecision;
|
|
84
86
|
}
|
|
85
87
|
let retryAfterSeconds = 0;
|
|
@@ -94,6 +96,7 @@ function checkPublicAuthRateLimit(options_1, scope_1, req_1) {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
if (retryAfterSeconds > 0) {
|
|
99
|
+
(0, metrics_1.countAccessiMetric)(options, 'rate_limit_blocked_total', { scope, layer: 'public' });
|
|
97
100
|
return { allowed: false, retryAfterSeconds };
|
|
98
101
|
}
|
|
99
102
|
return { allowed: true, retryAfterSeconds: 0 };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publicAuthRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/publicAuthRateLimit.ts"],"names":[],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"publicAuthRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/publicAuthRateLimit.ts"],"names":[],"mappings":"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","sourcesContent":["import { HttpException, HttpStatus } from '@nestjs/common';\nimport { Request, Response } from 'express';\nimport { RestUtilities } from '../../Utilities';\nimport type { AccessiOptions, PublicAuthRateLimitRuleOptions } from '../AccessiModule';\nimport { getDefaultRateLimitStore } from './rateLimitStore';\nimport type { RateLimitDecision, RateLimitStore } from './rateLimitStore';\nimport { countAccessiMetric } from '../observability/metrics';\n\nexport type PublicAuthRateLimitScope =\n | 'login'\n | 'twoFactorVerify'\n | 'twoFactorResend'\n | 'register'\n | 'passwordResetEmail'\n | 'passwordResetConfirm'\n | 'refresh'\n | 'getUserByToken'\n | 'adminBootstrap'\n | 'consoleEntry';\n\nconst DEFAULT_PUBLIC_AUTH_RATE_LIMITS: Record<\n PublicAuthRateLimitScope,\n PublicAuthRateLimitRuleOptions\n> = {\n login: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },\n twoFactorVerify: { windowMs: 15 * 60 * 1000, maxAttempts: 20 },\n twoFactorResend: { windowMs: 15 * 60 * 1000, maxAttempts: 5 },\n register: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },\n passwordResetEmail: { windowMs: 60 * 60 * 1000, maxAttempts: 5 },\n passwordResetConfirm: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },\n refresh: { windowMs: 5 * 60 * 1000, maxAttempts: 60 },\n getUserByToken: { windowMs: 5 * 60 * 1000, maxAttempts: 30 },\n adminBootstrap: { windowMs: 15 * 60 * 1000, maxAttempts: 10 },\n consoleEntry: { windowMs: 5 * 60 * 1000, maxAttempts: 30 },\n};\n\nfunction getRule(\n options: AccessiOptions,\n scope: PublicAuthRateLimitScope,\n): PublicAuthRateLimitRuleOptions {\n const configuredRule = options.publicAuthRateLimit?.[scope];\n\n if (\n configuredRule &&\n Number.isFinite(configuredRule.windowMs) &&\n configuredRule.windowMs > 0 &&\n Number.isFinite(configuredRule.maxAttempts) &&\n configuredRule.maxAttempts > 0\n ) {\n return {\n windowMs: Math.trunc(configuredRule.windowMs),\n maxAttempts: Math.trunc(configuredRule.maxAttempts),\n };\n }\n\n return DEFAULT_PUBLIC_AUTH_RATE_LIMITS[scope];\n}\n\nasync function consumeRateLimit(\n store: RateLimitStore,\n key: string,\n rule: PublicAuthRateLimitRuleOptions,\n): Promise<RateLimitDecision> {\n try {\n return await store.consume(key, rule.windowMs, rule.maxAttempts);\n } catch {\n throw new HttpException(\n {\n code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n message: 'Rate limit non disponibile. Riprova piu tardi.',\n },\n HttpStatus.SERVICE_UNAVAILABLE,\n );\n }\n}\n\nfunction normalizeIdentifier(identifier: string): string | null {\n if (typeof identifier !== 'string') {\n return null;\n }\n\n const normalized = identifier.trim().toLowerCase();\n return normalized === '' ? null : normalized;\n}\n\n/**\n * Applica il rate limit per IP e, opzionalmente, per soggetto.\n *\n * Con `publicAuthRateLimit.store` e possibile usare uno store Redis condiviso;\n * senza store usa la memoria del processo.\n */\nexport async function checkPublicAuthRateLimit(\n options: AccessiOptions,\n scope: PublicAuthRateLimitScope,\n req: Request,\n identifiers: string[] = [],\n): Promise<RateLimitDecision> {\n if (options.publicAuthRateLimit?.enabled === false) {\n return { allowed: true, retryAfterSeconds: 0 };\n }\n\n const rule = getRule(options, scope);\n const store = options.publicAuthRateLimit?.store ?? getDefaultRateLimitStore();\n const ipAddress =\n normalizeIdentifier(req.ip ?? req.socket?.remoteAddress ?? 'unknown') ?? 'unknown';\n\n // Consuma prima il bucket per IP: se gia bloccato non crea bucket per soggetto,\n // evitando crescita di memoria con identificatori sempre diversi.\n const ipDecision = await consumeRateLimit(store, `${scope}|ip|${ipAddress}`, rule);\n if (!ipDecision.allowed) {\n countAccessiMetric(options, 'rate_limit_blocked_total', { scope, layer: 'public' });\n return ipDecision;\n }\n\n let retryAfterSeconds = 0;\n for (const identifier of identifiers) {\n const normalizedIdentifier = normalizeIdentifier(identifier);\n if (!normalizedIdentifier) {\n continue;\n }\n\n const subjectDecision = await consumeRateLimit(\n store,\n `${scope}|ip-subject|${ipAddress}|${normalizedIdentifier}`,\n rule,\n );\n if (!subjectDecision.allowed) {\n retryAfterSeconds = Math.max(retryAfterSeconds, subjectDecision.retryAfterSeconds);\n }\n }\n\n if (retryAfterSeconds > 0) {\n countAccessiMetric(options, 'rate_limit_blocked_total', { scope, layer: 'public' });\n 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"]}
|
|
@@ -88,6 +88,7 @@ const defaultStore = createMemoryRateLimitStore();
|
|
|
88
88
|
function getDefaultRateLimitStore() {
|
|
89
89
|
return defaultStore;
|
|
90
90
|
}
|
|
91
|
+
const redisMetrics_1 = require("../observability/redisMetrics");
|
|
91
92
|
const REDIS_SCRIPT = `
|
|
92
93
|
local current = redis.call('INCR', KEYS[1])
|
|
93
94
|
if current == 1 then
|
|
@@ -111,11 +112,12 @@ function createRedisRateLimitStore(options) {
|
|
|
111
112
|
throw new Error('Redis rate limit store: client.eval non configurato.');
|
|
112
113
|
}
|
|
113
114
|
const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:rate:').replace(/:+$/, ':');
|
|
115
|
+
const client = (0, redisMetrics_1.instrumentAccessiRedisClient)('rate_limit', options.client);
|
|
114
116
|
return {
|
|
115
117
|
consume(key, windowMs, maxAttempts) {
|
|
116
118
|
return __awaiter(this, void 0, void 0, function* () {
|
|
117
119
|
const limits = normalizeLimits(windowMs, maxAttempts);
|
|
118
|
-
const raw = yield
|
|
120
|
+
const raw = yield client.eval(REDIS_SCRIPT, 1, `${keyPrefix}${key}`, limits.windowMs, limits.maxAttempts);
|
|
119
121
|
if (!Array.isArray(raw) || raw.length < 2) {
|
|
120
122
|
throw new Error('Redis rate limit store: risposta non valida.');
|
|
121
123
|
}
|
|
@@ -132,7 +134,7 @@ function createRedisRateLimitStore(options) {
|
|
|
132
134
|
healthCheck() {
|
|
133
135
|
return __awaiter(this, void 0, void 0, function* () {
|
|
134
136
|
try {
|
|
135
|
-
const result = yield
|
|
137
|
+
const result = yield client.eval("return redis.call('PING')", 0);
|
|
136
138
|
return String(result).toUpperCase() === 'PONG';
|
|
137
139
|
}
|
|
138
140
|
catch (_a) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimitStore.js","sourceRoot":"","sources":["../../../src/accessi-module/security/rateLimitStore.ts"],"names":[],"mappings":";;;;;;;;;;;AAiCA,gEA+DC;AAKD,4DAEC;
|
|
1
|
+
{"version":3,"file":"rateLimitStore.js","sourceRoot":"","sources":["../../../src/accessi-module/security/rateLimitStore.ts"],"names":[],"mappings":"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","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\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\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 const client = instrumentAccessiRedisClient('rate_limit', options.client);\n\n return {\n async consume(key, windowMs, maxAttempts) {\n const limits = normalizeLimits(windowMs, maxAttempts);\n const raw = await 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 client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n"]}
|
|
@@ -16,6 +16,7 @@ exports.isRefreshTokenFamilyRevoked = isRefreshTokenFamilyRevoked;
|
|
|
16
16
|
exports.revokeRefreshTokenFamily = revokeRefreshTokenFamily;
|
|
17
17
|
exports.consumeRefreshToken = consumeRefreshToken;
|
|
18
18
|
exports.clearDefaultRefreshTokenRotations = clearDefaultRefreshTokenRotations;
|
|
19
|
+
const redisMetrics_1 = require("../observability/redisMetrics");
|
|
19
20
|
const CONSUME_SCRIPT = `
|
|
20
21
|
local ttl = tonumber(ARGV[2])
|
|
21
22
|
if not ttl or ttl <= 0 then return nil end
|
|
@@ -109,37 +110,38 @@ function createRedisRefreshTokenRotationStore(options) {
|
|
|
109
110
|
}
|
|
110
111
|
const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:refresh-used:').replace(/:+$/, ':');
|
|
111
112
|
const familyKeyPrefix = ((_b = options.familyKeyPrefix) !== null && _b !== void 0 ? _b : 'accessi:refresh-family-revoked:').replace(/:+$/, ':');
|
|
113
|
+
const client = (0, redisMetrics_1.instrumentAccessiRedisClient)('refresh_token_rotation', options.client);
|
|
112
114
|
return {
|
|
113
115
|
consume(jti, expiresAtMs) {
|
|
114
116
|
return __awaiter(this, void 0, void 0, function* () {
|
|
115
117
|
const ttlMs = Math.max(1, Math.trunc(expiresAtMs - Date.now()));
|
|
116
|
-
const result = yield
|
|
118
|
+
const result = yield client.eval(CONSUME_SCRIPT, 1, `${keyPrefix}${jti}`, Date.now(), ttlMs);
|
|
117
119
|
return parseConsumeResult(result).consumed;
|
|
118
120
|
});
|
|
119
121
|
},
|
|
120
122
|
consumeWithMetadata(jti, expiresAtMs) {
|
|
121
123
|
return __awaiter(this, void 0, void 0, function* () {
|
|
122
124
|
const ttlMs = Math.max(1, Math.trunc(expiresAtMs - Date.now()));
|
|
123
|
-
const result = yield
|
|
125
|
+
const result = yield client.eval(CONSUME_SCRIPT, 1, `${keyPrefix}${jti}`, Date.now(), ttlMs);
|
|
124
126
|
return parseConsumeResult(result);
|
|
125
127
|
});
|
|
126
128
|
},
|
|
127
129
|
revokeFamily(familyId, expiresAtMs) {
|
|
128
130
|
return __awaiter(this, void 0, void 0, function* () {
|
|
129
131
|
const ttlMs = Math.max(0, Math.trunc(expiresAtMs - Date.now()));
|
|
130
|
-
yield
|
|
132
|
+
yield client.eval(REVOKE_FAMILY_SCRIPT, 1, `${familyKeyPrefix}${familyId}`, ttlMs);
|
|
131
133
|
});
|
|
132
134
|
},
|
|
133
135
|
isFamilyRevoked(familyId) {
|
|
134
136
|
return __awaiter(this, void 0, void 0, function* () {
|
|
135
|
-
const result = yield
|
|
137
|
+
const result = yield client.eval(IS_FAMILY_REVOKED_SCRIPT, 1, `${familyKeyPrefix}${familyId}`);
|
|
136
138
|
return Number(result) > 0;
|
|
137
139
|
});
|
|
138
140
|
},
|
|
139
141
|
healthCheck() {
|
|
140
142
|
return __awaiter(this, void 0, void 0, function* () {
|
|
141
143
|
try {
|
|
142
|
-
const result = yield
|
|
144
|
+
const result = yield client.eval("return redis.call('PING')", 0);
|
|
143
145
|
return String(result).toUpperCase() === 'PONG';
|
|
144
146
|
}
|
|
145
147
|
catch (_a) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refreshTokenRotation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/refreshTokenRotation.ts"],"names":[],"mappings":"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","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 /**\n * Come `consume`, ma restituisce anche quando il token era gia stato\n * consumato: serve a distinguere un retry concorrente da un possibile furto.\n */\n consumeWithMetadata?(jti: string, expiresAtMs: number): Promise<RefreshTokenConsumeResult>;\n /** Revoca l'intera famiglia di refresh token (stesso login). */\n revokeFamily?(familyId: string, expiresAtMs: number): Promise<void>;\n /** True se la famiglia e stata revocata per riuso sospetto. */\n isFamilyRevoked?(familyId: string): Promise<boolean>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\n/** Esito del consumo atomico di un refresh token. */\nexport interface RefreshTokenConsumeResult {\n consumed: boolean;\n /** Quando il token era gia stato consumato, il timestamp del primo consumo. */\n consumedAtMs?: number;\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 * Finestra entro cui il riuso di un token appena consumato e trattato come\n * retry concorrente (nessuna revoca famiglia). Default: 10000 ms.\n */\n reuseGraceMs?: number;\n /**\n * Se true, dopo un riuso rilevato revoca anche tutte le sessioni utente.\n * Default: false, per non disconnettere dispositivi legittimi.\n */\n revokeUserSessionsOnReuse?: boolean;\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\nlocal stored = redis.call('GET', KEYS[1])\nif not stored then\n redis.call('SET', KEYS[1], ARGV[1], 'PX', ttl)\n return {1, 0}\nend\nreturn {0, tonumber(stored) or 0}\n`;\nconst REVOKE_FAMILY_SCRIPT =\n \"if tonumber(ARGV[1]) > 0 then return redis.call('SET', KEYS[1], '1', 'PX', ARGV[1]) else return redis.call('SET', KEYS[1], '1') end\";\nconst IS_FAMILY_REVOKED_SCRIPT = \"return redis.call('EXISTS', KEYS[1])\";\n\nfunction parseConsumeResult(result: unknown): RefreshTokenConsumeResult {\n if (Array.isArray(result)) {\n const consumed = Number(result[0]) === 1 || result[0] === true;\n const consumedAtMs = Number(result[1]);\n return {\n consumed,\n ...(Number.isFinite(consumedAtMs) && consumedAtMs > 0 ? { consumedAtMs } : {}),\n };\n }\n // Client minimali (o store precedenti) rispondono 'OK' / null.\n return { consumed: result !== null && result !== undefined && result !== false };\n}\n\nexport function createMemoryRefreshTokenRotationStore(): MemoryRefreshTokenRotationStore {\n const used = new Map<string, { expiresAtMs: number; consumedAtMs: number }>();\n const revokedFamilies = new Map<string, number>();\n\n const prune = (now: number) => {\n for (const [jti, entry] of used) {\n if (entry.expiresAtMs <= now) used.delete(jti);\n }\n for (const [familyId, expiresAt] of revokedFamilies) {\n if (expiresAt <= now) revokedFamilies.delete(familyId);\n }\n };\n\n const consumeWithMetadata = async (\n jti: string,\n expiresAtMs: number,\n ): Promise<RefreshTokenConsumeResult> => {\n const now = Date.now();\n prune(now);\n const existing = used.get(jti);\n if (existing && existing.expiresAtMs > now) {\n return { consumed: false, consumedAtMs: existing.consumedAtMs };\n }\n used.set(jti, { expiresAtMs, consumedAtMs: now });\n return { consumed: true };\n };\n\n return {\n async consume(jti, expiresAtMs) {\n return (await consumeWithMetadata(jti, expiresAtMs)).consumed;\n },\n consumeWithMetadata,\n async revokeFamily(familyId, expiresAtMs) {\n revokedFamilies.set(familyId, Math.max(expiresAtMs, Date.now() + 1));\n },\n async isFamilyRevoked(familyId) {\n const expiresAt = revokedFamilies.get(familyId);\n if (expiresAt === undefined) return false;\n if (expiresAt <= Date.now()) {\n revokedFamilies.delete(familyId);\n return false;\n }\n return true;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n used.clear();\n revokedFamilies.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 /** Default: `accessi:refresh-family-revoked:`. */\n familyKeyPrefix?: string;\n}\n\n/**\n * Store Redis per refresh token monouso.\n *\n * Il consumo e atomico: due refresh concorrenti dello stesso token producono\n * un solo successo. La famiglia viene revocata con una chiave separata che\n * scade insieme ai token che ne fanno parte.\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 const familyKeyPrefix = (options.familyKeyPrefix ?? 'accessi:refresh-family-revoked:').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 Date.now(),\n ttlMs,\n );\n return parseConsumeResult(result).consumed;\n },\n\n async consumeWithMetadata(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 Date.now(),\n ttlMs,\n );\n return parseConsumeResult(result);\n },\n\n async revokeFamily(familyId, expiresAtMs) {\n const ttlMs = Math.max(0, Math.trunc(expiresAtMs - Date.now()));\n await options.client.eval(REVOKE_FAMILY_SCRIPT, 1, `${familyKeyPrefix}${familyId}`, ttlMs);\n },\n\n async isFamilyRevoked(familyId) {\n const result = await options.client.eval(IS_FAMILY_REVOKED_SCRIPT, 1, `${familyKeyPrefix}${familyId}`);\n return Number(result) > 0;\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\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/**\n * True se la famiglia di refresh token e stata revocata. Ritorna false se lo\n * store non supporta le famiglie (implementazioni legacy).\n */\nexport async function isRefreshTokenFamilyRevoked(\n options: AccessiOptions,\n familyId: unknown,\n): Promise<boolean> {\n if (typeof familyId !== 'string' || familyId.trim() === '') return false;\n const store = getRefreshTokenRotationStore(options);\n if (!store?.isFamilyRevoked) return false;\n return store.isFamilyRevoked(familyId);\n}\n\n/**\n * Revoca la famiglia di refresh token fino alla scadenza indicata. Ritorna\n * false se lo store non supporta le famiglie.\n */\nexport async function revokeRefreshTokenFamily(\n options: AccessiOptions,\n familyId: string,\n expiresAtMs: number,\n): Promise<boolean> {\n const store = getRefreshTokenRotationStore(options);\n if (!store?.revokeFamily) return false;\n await store.revokeFamily(familyId, expiresAtMs);\n return true;\n}\n\n/** Consumo atomico con metadati, se lo store li supporta. */\nexport async function consumeRefreshToken(\n options: AccessiOptions,\n jti: string,\n expiresAtMs: number,\n): Promise<RefreshTokenConsumeResult | undefined> {\n const store = getRefreshTokenRotationStore(options);\n if (!store) return undefined;\n if (store.consumeWithMetadata) {\n return store.consumeWithMetadata(jti, expiresAtMs);\n }\n return { consumed: await store.consume(jti, expiresAtMs) };\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultRefreshTokenRotations(): void {\n defaultMemoryStore.clear();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"refreshTokenRotation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/refreshTokenRotation.ts"],"names":[],"mappings":";;;;;;;;;;;AA+EA,sFAoDC;AAqBD,oFAuDC;AAID,oEAMC;AAMD,kEAQC;AAMD,4DASC;AAGD,kDAWC;AAGD,8EAEC;AAxQD,gEAA6E;AAmD7E,MAAM,cAAc,GAAG;;;;;;;;;CAStB,CAAC;AACF,MAAM,oBAAoB,GACxB,qIAAqI,CAAC;AACxI,MAAM,wBAAwB,GAAG,sCAAsC,CAAC;AAExE,SAAS,kBAAkB,CAAC,MAAe;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;QAC/D,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACvC,uBACE,QAAQ,IACL,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAC9E;IACJ,CAAC;IACD,+DAA+D;IAC/D,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;AACnF,CAAC;AAED,SAAgB,qCAAqC;IACnD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAyD,CAAC;IAC9E,MAAM,eAAe,GAAG,IAAI,GAAG,EAAkB,CAAC;IAElD,MAAM,KAAK,GAAG,CAAC,GAAW,EAAE,EAAE;QAC5B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC;YAChC,IAAI,KAAK,CAAC,WAAW,IAAI,GAAG;gBAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC;QACD,KAAK,MAAM,CAAC,QAAQ,EAAE,SAAS,CAAC,IAAI,eAAe,EAAE,CAAC;YACpD,IAAI,SAAS,IAAI,GAAG;gBAAE,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACzD,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,mBAAmB,GAAG,CAC1B,GAAW,EACX,WAAmB,EACiB,EAAE;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,CAAC,GAAG,CAAC,CAAC;QACX,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,QAAQ,IAAI,QAAQ,CAAC,WAAW,GAAG,GAAG,EAAE,CAAC;YAC3C,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,CAAC,YAAY,EAAE,CAAC;QAClE,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;QAClD,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC,CAAA,CAAC;IAEF,OAAO;QACC,OAAO,CAAC,GAAG,EAAE,WAAW;;gBAC5B,OAAO,CAAC,MAAM,mBAAmB,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC;YAChE,CAAC;SAAA;QACD,mBAAmB;QACb,YAAY,CAAC,QAAQ,EAAE,WAAW;;gBACtC,eAAe,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC;YACvE,CAAC;SAAA;QACK,eAAe,CAAC,QAAQ;;gBAC5B,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBAChD,IAAI,SAAS,KAAK,SAAS;oBAAE,OAAO,KAAK,CAAC;gBAC1C,IAAI,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;oBAC5B,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;oBACjC,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;QACK,WAAW;;gBACf,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;QACD,KAAK;YACH,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,eAAe,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC;AAcD;;;;;;GAMG;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;IACrF,MAAM,eAAe,GAAG,CAAC,MAAA,OAAO,CAAC,eAAe,mCAAI,iCAAiC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC3G,MAAM,MAAM,GAAG,IAAA,2CAA4B,EAAC,wBAAwB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAEtF,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,MAAM,CAAC,IAAI,CAC9B,cAAc,EACd,CAAC,EACD,GAAG,SAAS,GAAG,GAAG,EAAE,EACpB,IAAI,CAAC,GAAG,EAAE,EACV,KAAK,CACN,CAAC;gBACF,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC;YAC7C,CAAC;SAAA;QAEK,mBAAmB,CAAC,GAAG,EAAE,WAAW;;gBACxC,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,MAAM,CAAC,IAAI,CAC9B,cAAc,EACd,CAAC,EACD,GAAG,SAAS,GAAG,GAAG,EAAE,EACpB,IAAI,CAAC,GAAG,EAAE,EACV,KAAK,CACN,CAAC;gBACF,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;YACpC,CAAC;SAAA;QAEK,YAAY,CAAC,QAAQ,EAAE,WAAW;;gBACtC,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,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC,EAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,EAAE,KAAK,CAAC,CAAC;YACrF,CAAC;SAAA;QAEK,eAAe,CAAC,QAAQ;;gBAC5B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,wBAAwB,EAAE,CAAC,EAAE,GAAG,eAAe,GAAG,QAAQ,EAAE,CAAC,CAAC;gBAC/F,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC5B,CAAC;SAAA;QAEK,WAAW;;gBACf,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC;oBACjE,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;;;GAGG;AACH,SAAsB,2BAA2B,CAC/C,OAAuB,EACvB,QAAiB;;QAEjB,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,KAAK,CAAC;QACzE,MAAM,KAAK,GAAG,4BAA4B,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,eAAe,CAAA;YAAE,OAAO,KAAK,CAAC;QAC1C,OAAO,KAAK,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;IACzC,CAAC;CAAA;AAED;;;GAGG;AACH,SAAsB,wBAAwB,CAC5C,OAAuB,EACvB,QAAgB,EAChB,WAAmB;;QAEnB,MAAM,KAAK,GAAG,4BAA4B,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,CAAA,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,YAAY,CAAA;YAAE,OAAO,KAAK,CAAC;QACvC,MAAM,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;CAAA;AAED,6DAA6D;AAC7D,SAAsB,mBAAmB,CACvC,OAAuB,EACvB,GAAW,EACX,WAAmB;;QAEnB,MAAM,KAAK,GAAG,4BAA4B,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC7B,IAAI,KAAK,CAAC,mBAAmB,EAAE,CAAC;YAC9B,OAAO,KAAK,CAAC,mBAAmB,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QACrD,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,WAAW,CAAC,EAAE,CAAC;IAC7D,CAAC;CAAA;AAED,4DAA4D;AAC5D,SAAgB,iCAAiC;IAC/C,kBAAkB,CAAC,KAAK,EAAE,CAAC;AAC7B,CAAC","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\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 /**\n * Come `consume`, ma restituisce anche quando il token era gia stato\n * consumato: serve a distinguere un retry concorrente da un possibile furto.\n */\n consumeWithMetadata?(jti: string, expiresAtMs: number): Promise<RefreshTokenConsumeResult>;\n /** Revoca l'intera famiglia di refresh token (stesso login). */\n revokeFamily?(familyId: string, expiresAtMs: number): Promise<void>;\n /** True se la famiglia e stata revocata per riuso sospetto. */\n isFamilyRevoked?(familyId: string): Promise<boolean>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\n/** Esito del consumo atomico di un refresh token. */\nexport interface RefreshTokenConsumeResult {\n consumed: boolean;\n /** Quando il token era gia stato consumato, il timestamp del primo consumo. */\n consumedAtMs?: number;\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 * Finestra entro cui il riuso di un token appena consumato e trattato come\n * retry concorrente (nessuna revoca famiglia). Default: 10000 ms.\n */\n reuseGraceMs?: number;\n /**\n * Se true, dopo un riuso rilevato revoca anche tutte le sessioni utente.\n * Default: false, per non disconnettere dispositivi legittimi.\n */\n revokeUserSessionsOnReuse?: boolean;\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\nlocal stored = redis.call('GET', KEYS[1])\nif not stored then\n redis.call('SET', KEYS[1], ARGV[1], 'PX', ttl)\n return {1, 0}\nend\nreturn {0, tonumber(stored) or 0}\n`;\nconst REVOKE_FAMILY_SCRIPT =\n \"if tonumber(ARGV[1]) > 0 then return redis.call('SET', KEYS[1], '1', 'PX', ARGV[1]) else return redis.call('SET', KEYS[1], '1') end\";\nconst IS_FAMILY_REVOKED_SCRIPT = \"return redis.call('EXISTS', KEYS[1])\";\n\nfunction parseConsumeResult(result: unknown): RefreshTokenConsumeResult {\n if (Array.isArray(result)) {\n const consumed = Number(result[0]) === 1 || result[0] === true;\n const consumedAtMs = Number(result[1]);\n return {\n consumed,\n ...(Number.isFinite(consumedAtMs) && consumedAtMs > 0 ? { consumedAtMs } : {}),\n };\n }\n // Client minimali (o store precedenti) rispondono 'OK' / null.\n return { consumed: result !== null && result !== undefined && result !== false };\n}\n\nexport function createMemoryRefreshTokenRotationStore(): MemoryRefreshTokenRotationStore {\n const used = new Map<string, { expiresAtMs: number; consumedAtMs: number }>();\n const revokedFamilies = new Map<string, number>();\n\n const prune = (now: number) => {\n for (const [jti, entry] of used) {\n if (entry.expiresAtMs <= now) used.delete(jti);\n }\n for (const [familyId, expiresAt] of revokedFamilies) {\n if (expiresAt <= now) revokedFamilies.delete(familyId);\n }\n };\n\n const consumeWithMetadata = async (\n jti: string,\n expiresAtMs: number,\n ): Promise<RefreshTokenConsumeResult> => {\n const now = Date.now();\n prune(now);\n const existing = used.get(jti);\n if (existing && existing.expiresAtMs > now) {\n return { consumed: false, consumedAtMs: existing.consumedAtMs };\n }\n used.set(jti, { expiresAtMs, consumedAtMs: now });\n return { consumed: true };\n };\n\n return {\n async consume(jti, expiresAtMs) {\n return (await consumeWithMetadata(jti, expiresAtMs)).consumed;\n },\n consumeWithMetadata,\n async revokeFamily(familyId, expiresAtMs) {\n revokedFamilies.set(familyId, Math.max(expiresAtMs, Date.now() + 1));\n },\n async isFamilyRevoked(familyId) {\n const expiresAt = revokedFamilies.get(familyId);\n if (expiresAt === undefined) return false;\n if (expiresAt <= Date.now()) {\n revokedFamilies.delete(familyId);\n return false;\n }\n return true;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n used.clear();\n revokedFamilies.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 /** Default: `accessi:refresh-family-revoked:`. */\n familyKeyPrefix?: string;\n}\n\n/**\n * Store Redis per refresh token monouso.\n *\n * Il consumo e atomico: due refresh concorrenti dello stesso token producono\n * un solo successo. La famiglia viene revocata con una chiave separata che\n * scade insieme ai token che ne fanno parte.\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 const familyKeyPrefix = (options.familyKeyPrefix ?? 'accessi:refresh-family-revoked:').replace(/:+$/, ':');\n const client = instrumentAccessiRedisClient('refresh_token_rotation', options.client);\n\n return {\n async consume(jti, expiresAtMs) {\n const ttlMs = Math.max(1, Math.trunc(expiresAtMs - Date.now()));\n const result = await client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${jti}`,\n Date.now(),\n ttlMs,\n );\n return parseConsumeResult(result).consumed;\n },\n\n async consumeWithMetadata(jti, expiresAtMs) {\n const ttlMs = Math.max(1, Math.trunc(expiresAtMs - Date.now()));\n const result = await client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${jti}`,\n Date.now(),\n ttlMs,\n );\n return parseConsumeResult(result);\n },\n\n async revokeFamily(familyId, expiresAtMs) {\n const ttlMs = Math.max(0, Math.trunc(expiresAtMs - Date.now()));\n await client.eval(REVOKE_FAMILY_SCRIPT, 1, `${familyKeyPrefix}${familyId}`, ttlMs);\n },\n\n async isFamilyRevoked(familyId) {\n const result = await client.eval(IS_FAMILY_REVOKED_SCRIPT, 1, `${familyKeyPrefix}${familyId}`);\n return Number(result) > 0;\n },\n\n async healthCheck() {\n try {\n const result = await client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n\nconst defaultMemoryStore = 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/**\n * True se la famiglia di refresh token e stata revocata. Ritorna false se lo\n * store non supporta le famiglie (implementazioni legacy).\n */\nexport async function isRefreshTokenFamilyRevoked(\n options: AccessiOptions,\n familyId: unknown,\n): Promise<boolean> {\n if (typeof familyId !== 'string' || familyId.trim() === '') return false;\n const store = getRefreshTokenRotationStore(options);\n if (!store?.isFamilyRevoked) return false;\n return store.isFamilyRevoked(familyId);\n}\n\n/**\n * Revoca la famiglia di refresh token fino alla scadenza indicata. Ritorna\n * false se lo store non supporta le famiglie.\n */\nexport async function revokeRefreshTokenFamily(\n options: AccessiOptions,\n familyId: string,\n expiresAtMs: number,\n): Promise<boolean> {\n const store = getRefreshTokenRotationStore(options);\n if (!store?.revokeFamily) return false;\n await store.revokeFamily(familyId, expiresAtMs);\n return true;\n}\n\n/** Consumo atomico con metadati, se lo store li supporta. */\nexport async function consumeRefreshToken(\n options: AccessiOptions,\n jti: string,\n expiresAtMs: number,\n): Promise<RefreshTokenConsumeResult | undefined> {\n const store = getRefreshTokenRotationStore(options);\n if (!store) return undefined;\n if (store.consumeWithMetadata) {\n return store.consumeWithMetadata(jti, expiresAtMs);\n }\n return { consumed: await store.consume(jti, expiresAtMs) };\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultRefreshTokenRotations(): void {\n defaultMemoryStore.clear();\n}\n"]}
|
|
@@ -14,6 +14,7 @@ exports.resetServiceTokenRateLimit = resetServiceTokenRateLimit;
|
|
|
14
14
|
exports.assertServiceTokenRateLimit = assertServiceTokenRateLimit;
|
|
15
15
|
const common_1 = require("@nestjs/common");
|
|
16
16
|
const rateLimitStore_1 = require("./rateLimitStore");
|
|
17
|
+
const metrics_1 = require("../observability/metrics");
|
|
17
18
|
/**
|
|
18
19
|
* Limite anti-abuso per la verifica dei token di servizio.
|
|
19
20
|
*
|
|
@@ -30,6 +31,7 @@ function checkServiceTokenRateLimit(identifier_1) {
|
|
|
30
31
|
return yield store.consume(`service-token|${key}`, WINDOW_MS, MAX_ATTEMPTS);
|
|
31
32
|
}
|
|
32
33
|
catch (_a) {
|
|
34
|
+
(0, metrics_1.recordAccessiMetric)('accessi_store_errors_total', 1, { store: 'service_token_rate_limit' });
|
|
33
35
|
throw new common_1.HttpException({
|
|
34
36
|
code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',
|
|
35
37
|
message: 'Rate limit non disponibile. Riprova piu tardi.',
|
|
@@ -46,6 +48,7 @@ function assertServiceTokenRateLimit(identifier, store) {
|
|
|
46
48
|
return __awaiter(this, void 0, void 0, function* () {
|
|
47
49
|
const decision = yield checkServiceTokenRateLimit(identifier, store);
|
|
48
50
|
if (!decision.allowed) {
|
|
51
|
+
(0, metrics_1.recordAccessiMetric)('accessi_rate_limit_blocked_total', 1, { layer: 'service_token' });
|
|
49
52
|
throw new common_1.HttpException({ code: 'ACCESSI_SERVICE_TOKEN_RATE_LIMITED', message: 'Troppi tentativi. Riprova piu tardi.' }, common_1.HttpStatus.TOO_MANY_REQUESTS);
|
|
50
53
|
}
|
|
51
54
|
});
|
|
@@ -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":";;;;;;;;;;;AAeA,gEAiBC;AAGD,gEAEC;AAGD,kEAYC;AApDD,2CAA2D;AAC3D,qDAA4D;AAE5D,sDAA+D;AAE/D;;;;;;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,IAAA,6BAAmB,EAAC,4BAA4B,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;YAC5F,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,IAAA,6BAAmB,EAAC,kCAAkC,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;YACvF,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';\nimport { recordAccessiMetric } from '../observability/metrics';\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 recordAccessiMetric('accessi_store_errors_total', 1, { store: 'service_token_rate_limit' });\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 recordAccessiMetric('accessi_rate_limit_blocked_total', 1, { layer: 'service_token' });\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"]}
|
|
@@ -17,7 +17,9 @@ exports.isAccessiTokenRevoked = isAccessiTokenRevoked;
|
|
|
17
17
|
exports.revokeAccessiUserSessions = revokeAccessiUserSessions;
|
|
18
18
|
exports.revokeAccessiUserSessionsIfConfigured = revokeAccessiUserSessionsIfConfigured;
|
|
19
19
|
exports.clearDefaultSessionRevocations = clearDefaultSessionRevocations;
|
|
20
|
+
const redisMetrics_1 = require("../observability/redisMetrics");
|
|
20
21
|
const audit_1 = require("./audit");
|
|
22
|
+
const metrics_1 = require("../observability/metrics");
|
|
21
23
|
const accessTokenRevocation_1 = require("./accessTokenRevocation");
|
|
22
24
|
const refreshTokenRotation_1 = require("./refreshTokenRotation");
|
|
23
25
|
const REVOKE_USER_SCRIPT = "if tonumber(ARGV[2]) > 0 then return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) else return redis.call('SET', KEYS[1], ARGV[1]) end";
|
|
@@ -59,15 +61,16 @@ function createRedisSessionRevocationStore(options) {
|
|
|
59
61
|
}
|
|
60
62
|
const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:revoked-user:').replace(/:+$/, ':');
|
|
61
63
|
const ttlMs = Math.max(0, Math.trunc((_b = options.ttlMs) !== null && _b !== void 0 ? _b : 365 * 24 * 60 * 60 * 1000));
|
|
64
|
+
const client = (0, redisMetrics_1.instrumentAccessiRedisClient)('session_revocation', options.client);
|
|
62
65
|
return {
|
|
63
66
|
revokeUser(codiceUtente, revokedAtMs) {
|
|
64
67
|
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
-
yield
|
|
68
|
+
yield client.eval(REVOKE_USER_SCRIPT, 1, `${keyPrefix}${codiceUtente}`, String(revokedAtMs), ttlMs);
|
|
66
69
|
});
|
|
67
70
|
},
|
|
68
71
|
getUserRevocation(codiceUtente) {
|
|
69
72
|
return __awaiter(this, void 0, void 0, function* () {
|
|
70
|
-
const value = yield
|
|
73
|
+
const value = yield client.eval(GET_USER_SCRIPT, 1, `${keyPrefix}${codiceUtente}`);
|
|
71
74
|
if (typeof value !== 'string' && typeof value !== 'number')
|
|
72
75
|
return null;
|
|
73
76
|
const timestamp = Number(value);
|
|
@@ -77,7 +80,7 @@ function createRedisSessionRevocationStore(options) {
|
|
|
77
80
|
healthCheck() {
|
|
78
81
|
return __awaiter(this, void 0, void 0, function* () {
|
|
79
82
|
try {
|
|
80
|
-
const result = yield
|
|
83
|
+
const result = yield client.eval("return redis.call('PING')", 0);
|
|
81
84
|
return String(result).toUpperCase() === 'PONG';
|
|
82
85
|
}
|
|
83
86
|
catch (_a) {
|
|
@@ -148,6 +151,7 @@ function revokeAccessiUserSessions(options_1, codiceUtente_1) {
|
|
|
148
151
|
}
|
|
149
152
|
yield store.revokeUser(codiceUtente, revokedAtMs);
|
|
150
153
|
(0, audit_1.auditAccessi)('accessi_sessions_revoked', { codiceUtente, revokedAtMs, reason });
|
|
154
|
+
(0, metrics_1.countAccessiMetric)(options, 'sessions_revoked_total', { reason });
|
|
151
155
|
});
|
|
152
156
|
}
|
|
153
157
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionRevocation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/sessionRevocation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\nimport { auditAccessi } from './audit';\nimport { isAccessiAccessTokenRevoked } from './accessTokenRevocation';\nimport { isRefreshTokenFamilyRevoked } from './refreshTokenRotation';\n\n/**\n * Revoca delle sessioni per utente.\n *\n * La revoca e un timestamp: un token emesso prima del cutoff viene rifiutato.\n * I token emessi dopo il cutoff restano validi. I token includono `iatMs`\n * (millisecondi) per rendere la revoca deterministica anche quando il cambio\n * password e il login avvengono nello stesso secondo; per i token legacy senza\n * `iatMs` si usa `iat` in secondi.\n */\nexport interface SessionRevocationStore {\n revokeUser(codiceUtente: number, revokedAtMs: number): Promise<void>;\n getUserRevocation(codiceUtente: number): Promise<number | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface SessionRevocationOptions {\n /** Default: disabilitata se non viene fornito uno store. */\n enabled?: boolean;\n /** Store condiviso (es. Redis) per invalidare le sessioni tra istanze. */\n store?: SessionRevocationStore;\n /** TTL delle chiavi Redis. Default: 365 giorni. */\n ttlMs?: number;\n}\n\nexport interface MemorySessionRevocationStore extends SessionRevocationStore {\n clear(): void;\n}\n\nconst REVOKE_USER_SCRIPT =\n \"if tonumber(ARGV[2]) > 0 then return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) else return redis.call('SET', KEYS[1], ARGV[1]) end\";\nconst GET_USER_SCRIPT = \"return redis.call('GET', KEYS[1])\";\n\nexport function createMemorySessionRevocationStore(): MemorySessionRevocationStore {\n const revokedAt = new Map<number, number>();\n return {\n async revokeUser(codiceUtente, timestamp) {\n revokedAt.set(codiceUtente, timestamp);\n },\n async getUserRevocation(codiceUtente) {\n return revokedAt.get(codiceUtente) ?? null;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n revokedAt.clear();\n },\n };\n}\n\nexport interface SessionRevocationRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisSessionRevocationStoreOptions {\n client: SessionRevocationRedisClient;\n /** Default: `accessi:revoked-user:`. */\n keyPrefix?: string;\n /** Default: 365 giorni. */\n ttlMs?: number;\n}\n\n/**\n * Store Redis per la revoca sessioni.\n *\n * `revokeUser` scrive un timestamp con TTL; `getUserRevocation` lo legge.\n * Il TTL deve essere maggiore della durata massima dei token.\n */\nexport function createRedisSessionRevocationStore(\n options: RedisSessionRevocationStoreOptions,\n): SessionRevocationStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis session revocation store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:revoked-user:').replace(/:+$/, ':');\n const ttlMs = Math.max(0, Math.trunc(options.ttlMs ?? 365 * 24 * 60 * 60 * 1000));\n\n return {\n async revokeUser(codiceUtente, revokedAtMs) {\n await options.client.eval(\n REVOKE_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n String(revokedAtMs),\n ttlMs,\n );\n },\n\n async getUserRevocation(codiceUtente) {\n const value = await options.client.eval(\n GET_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n );\n if (typeof value !== 'string' && typeof value !== 'number') return null;\n const timestamp = Number(value);\n return Number.isFinite(timestamp) ? timestamp : null;\n },\n\n async healthCheck() {\n try {\n const result = await 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 = createMemorySessionRevocationStore();\n\n/** Store di default in memoria, usato quando `sessionRevocation.store` non e configurato ma la revoca e abilitata. */\nexport function getSessionRevocationStore(options: AccessiOptions): SessionRevocationStore | undefined {\n if (options.sessionRevocation?.enabled === false) return undefined;\n if (options.sessionRevocation?.store) return options.sessionRevocation.store;\n return options.sessionRevocation?.enabled === true ? defaultMemoryStore : undefined;\n}\n\n/**\n * Issued-at in millisecondi. I token emessi da questa versione includono `iatMs`;\n * per quelli legacy si ricade su `iat` (secondi) per restare compatibili.\n */\nexport function resolveAccessiTokenIssuedAtMs(payload: Record<string, unknown>): number | null {\n const issuedAtMs = Number(payload.iatMs);\n if (Number.isFinite(issuedAtMs) && issuedAtMs > 0) return issuedAtMs;\n\n const issuedAt = Number(payload.iat);\n if (!Number.isFinite(issuedAt)) return null;\n return issuedAt * 1000;\n}\n\n/**\n * True se il token e stato revocato: famiglia refresh revocata, `jti` revocato\n * oppure sessione utente revocata prima dell'emissione del token.\n */\nexport async function isAccessiTokenRevoked(\n options: AccessiOptions,\n payload: Record<string, unknown>,\n codiceUtente: number,\n): Promise<boolean> {\n if (await isRefreshTokenFamilyRevoked(options, payload.fid)) return true;\n if (await isAccessiAccessTokenRevoked(options, payload)) return true;\n\n const store = getSessionRevocationStore(options);\n if (!store) return false;\n\n const issuedAtMs = resolveAccessiTokenIssuedAtMs(payload);\n if (issuedAtMs === null) return true;\n\n const revokedAt = await store.getUserRevocation(codiceUtente);\n if (revokedAt === null) return false;\n\n // I token emessi da questa versione hanno `iatMs`: un token emesso nello\n // stesso millisecondo della revoca e considerato revocato. Per i token\n // legacy (solo `iat`, secondi) si mantiene il confronto per secondo.\n const preciseIssuedAtMs = Number(payload.iatMs);\n if (Number.isFinite(preciseIssuedAtMs) && preciseIssuedAtMs > 0) {\n return issuedAtMs <= revokedAt;\n }\n return issuedAtMs < Math.floor(revokedAt / 1000) * 1000;\n}\n\n/** Revoca tutte le sessioni dell'utente. Richiede uno store configurato. */\nexport async function revokeAccessiUserSessions(\n options: AccessiOptions,\n codiceUtente: number,\n revokedAtMs: number = Date.now(),\n reason: string = 'manual',\n): Promise<void> {\n const store = getSessionRevocationStore(options);\n if (!store) {\n throw new Error('Session revocation store non configurato.');\n }\n await store.revokeUser(codiceUtente, revokedAtMs);\n auditAccessi('accessi_sessions_revoked', { codiceUtente, revokedAtMs, reason });\n}\n\n/**\n * Variante per i flussi applicativi (cambio password, blocco, cancellazione):\n * se la revoca non e configurata non fallisce, perche quei flussi devono\n * continuare a funzionare anche senza store condiviso.\n */\nexport async function revokeAccessiUserSessionsIfConfigured(\n options: AccessiOptions,\n codiceUtente: number,\n reason: string = 'manual',\n revokedAtMs: number = Date.now(),\n): Promise<boolean> {\n if (!getSessionRevocationStore(options)) return false;\n await revokeAccessiUserSessions(options, codiceUtente, revokedAtMs, reason);\n return true;\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultSessionRevocations(): void {\n defaultMemoryStore.clear();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sessionRevocation.js","sourceRoot":"","sources":["../../../src/accessi-module/security/sessionRevocation.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AccessiOptions } from '../AccessiModule';\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\nimport { auditAccessi } from './audit';\nimport { countAccessiMetric } from '../observability/metrics';\nimport { isAccessiAccessTokenRevoked } from './accessTokenRevocation';\nimport { isRefreshTokenFamilyRevoked } from './refreshTokenRotation';\n\n/**\n * Revoca delle sessioni per utente.\n *\n * La revoca e un timestamp: un token emesso prima del cutoff viene rifiutato.\n * I token emessi dopo il cutoff restano validi. I token includono `iatMs`\n * (millisecondi) per rendere la revoca deterministica anche quando il cambio\n * password e il login avvengono nello stesso secondo; per i token legacy senza\n * `iatMs` si usa `iat` in secondi.\n */\nexport interface SessionRevocationStore {\n revokeUser(codiceUtente: number, revokedAtMs: number): Promise<void>;\n getUserRevocation(codiceUtente: number): Promise<number | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nexport interface SessionRevocationOptions {\n /** Default: disabilitata se non viene fornito uno store. */\n enabled?: boolean;\n /** Store condiviso (es. Redis) per invalidare le sessioni tra istanze. */\n store?: SessionRevocationStore;\n /** TTL delle chiavi Redis. Default: 365 giorni. */\n ttlMs?: number;\n}\n\nexport interface MemorySessionRevocationStore extends SessionRevocationStore {\n clear(): void;\n}\n\nconst REVOKE_USER_SCRIPT =\n \"if tonumber(ARGV[2]) > 0 then return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2]) else return redis.call('SET', KEYS[1], ARGV[1]) end\";\nconst GET_USER_SCRIPT = \"return redis.call('GET', KEYS[1])\";\n\nexport function createMemorySessionRevocationStore(): MemorySessionRevocationStore {\n const revokedAt = new Map<number, number>();\n return {\n async revokeUser(codiceUtente, timestamp) {\n revokedAt.set(codiceUtente, timestamp);\n },\n async getUserRevocation(codiceUtente) {\n return revokedAt.get(codiceUtente) ?? null;\n },\n async healthCheck() {\n return true;\n },\n clear() {\n revokedAt.clear();\n },\n };\n}\n\nexport interface SessionRevocationRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisSessionRevocationStoreOptions {\n client: SessionRevocationRedisClient;\n /** Default: `accessi:revoked-user:`. */\n keyPrefix?: string;\n /** Default: 365 giorni. */\n ttlMs?: number;\n}\n\n/**\n * Store Redis per la revoca sessioni.\n *\n * `revokeUser` scrive un timestamp con TTL; `getUserRevocation` lo legge.\n * Il TTL deve essere maggiore della durata massima dei token.\n */\nexport function createRedisSessionRevocationStore(\n options: RedisSessionRevocationStoreOptions,\n): SessionRevocationStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis session revocation store: client.eval non configurato.');\n }\n\n const keyPrefix = (options.keyPrefix ?? 'accessi:revoked-user:').replace(/:+$/, ':');\n const ttlMs = Math.max(0, Math.trunc(options.ttlMs ?? 365 * 24 * 60 * 60 * 1000));\n const client = instrumentAccessiRedisClient('session_revocation', options.client);\n\n return {\n async revokeUser(codiceUtente, revokedAtMs) {\n await client.eval(\n REVOKE_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n String(revokedAtMs),\n ttlMs,\n );\n },\n\n async getUserRevocation(codiceUtente) {\n const value = await client.eval(\n GET_USER_SCRIPT,\n 1,\n `${keyPrefix}${codiceUtente}`,\n );\n if (typeof value !== 'string' && typeof value !== 'number') return null;\n const timestamp = Number(value);\n return Number.isFinite(timestamp) ? timestamp : null;\n },\n\n async healthCheck() {\n try {\n const result = await client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n\nconst defaultMemoryStore = createMemorySessionRevocationStore();\n\n/** Store di default in memoria, usato quando `sessionRevocation.store` non e configurato ma la revoca e abilitata. */\nexport function getSessionRevocationStore(options: AccessiOptions): SessionRevocationStore | undefined {\n if (options.sessionRevocation?.enabled === false) return undefined;\n if (options.sessionRevocation?.store) return options.sessionRevocation.store;\n return options.sessionRevocation?.enabled === true ? defaultMemoryStore : undefined;\n}\n\n/**\n * Issued-at in millisecondi. I token emessi da questa versione includono `iatMs`;\n * per quelli legacy si ricade su `iat` (secondi) per restare compatibili.\n */\nexport function resolveAccessiTokenIssuedAtMs(payload: Record<string, unknown>): number | null {\n const issuedAtMs = Number(payload.iatMs);\n if (Number.isFinite(issuedAtMs) && issuedAtMs > 0) return issuedAtMs;\n\n const issuedAt = Number(payload.iat);\n if (!Number.isFinite(issuedAt)) return null;\n return issuedAt * 1000;\n}\n\n/**\n * True se il token e stato revocato: famiglia refresh revocata, `jti` revocato\n * oppure sessione utente revocata prima dell'emissione del token.\n */\nexport async function isAccessiTokenRevoked(\n options: AccessiOptions,\n payload: Record<string, unknown>,\n codiceUtente: number,\n): Promise<boolean> {\n if (await isRefreshTokenFamilyRevoked(options, payload.fid)) return true;\n if (await isAccessiAccessTokenRevoked(options, payload)) return true;\n\n const store = getSessionRevocationStore(options);\n if (!store) return false;\n\n const issuedAtMs = resolveAccessiTokenIssuedAtMs(payload);\n if (issuedAtMs === null) return true;\n\n const revokedAt = await store.getUserRevocation(codiceUtente);\n if (revokedAt === null) return false;\n\n // I token emessi da questa versione hanno `iatMs`: un token emesso nello\n // stesso millisecondo della revoca e considerato revocato. Per i token\n // legacy (solo `iat`, secondi) si mantiene il confronto per secondo.\n const preciseIssuedAtMs = Number(payload.iatMs);\n if (Number.isFinite(preciseIssuedAtMs) && preciseIssuedAtMs > 0) {\n return issuedAtMs <= revokedAt;\n }\n return issuedAtMs < Math.floor(revokedAt / 1000) * 1000;\n}\n\n/** Revoca tutte le sessioni dell'utente. Richiede uno store configurato. */\nexport async function revokeAccessiUserSessions(\n options: AccessiOptions,\n codiceUtente: number,\n revokedAtMs: number = Date.now(),\n reason: string = 'manual',\n): Promise<void> {\n const store = getSessionRevocationStore(options);\n if (!store) {\n throw new Error('Session revocation store non configurato.');\n }\n await store.revokeUser(codiceUtente, revokedAtMs);\n auditAccessi('accessi_sessions_revoked', { codiceUtente, revokedAtMs, reason });\n countAccessiMetric(options, 'sessions_revoked_total', { reason });\n}\n\n/**\n * Variante per i flussi applicativi (cambio password, blocco, cancellazione):\n * se la revoca non e configurata non fallisce, perche quei flussi devono\n * continuare a funzionare anche senza store condiviso.\n */\nexport async function revokeAccessiUserSessionsIfConfigured(\n options: AccessiOptions,\n codiceUtente: number,\n reason: string = 'manual',\n revokedAtMs: number = Date.now(),\n): Promise<boolean> {\n if (!getSessionRevocationStore(options)) return false;\n await revokeAccessiUserSessions(options, codiceUtente, revokedAtMs, reason);\n return true;\n}\n\n/** Solo per test: azzera lo store in memoria di default. */\nexport function clearDefaultSessionRevocations(): void {\n defaultMemoryStore.clear();\n}\n"]}
|
|
Binary file
|