emilsoftware-utilities 2.0.0-dev.76 → 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 +9 -0
- package/dist/accessi-module/AccessiModule.js.map +1 -1
- package/dist/accessi-module/Console/accessiFetch.d.ts +7 -0
- package/dist/accessi-module/Console/accessiFetch.js +45 -7
- package/dist/accessi-module/Console/accessiFetch.js.map +1 -1
- package/dist/accessi-module/Console/console.js +83 -22
- package/dist/accessi-module/Console/console.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/getUserByTokenRequest.d.ts +2 -2
- package/dist/accessi-module/Console/generated/model/getUserByTokenRequest.js.map +1 -1
- package/dist/accessi-module/Console/generated/model/refreshTokenRequest.d.ts +2 -2
- package/dist/accessi-module/Console/generated/model/refreshTokenRequest.js.map +1 -1
- package/dist/accessi-module/Console/wiki.js +2 -0
- package/dist/accessi-module/Console/wiki.js.map +1 -1
- package/dist/accessi-module/Controllers/AccessiConsoleController.js +10 -2
- package/dist/accessi-module/Controllers/AccessiConsoleController.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 +102 -15
- package/dist/accessi-module/Controllers/AuthController.js.map +1 -1
- package/dist/accessi-module/Controllers/ConsoleEntryController.js +17 -0
- package/dist/accessi-module/Controllers/ConsoleEntryController.js.map +1 -1
- package/dist/accessi-module/Dtos/GetUserByTokenRequest.d.ts +1 -1
- package/dist/accessi-module/Dtos/GetUserByTokenRequest.js +3 -2
- package/dist/accessi-module/Dtos/GetUserByTokenRequest.js.map +1 -1
- package/dist/accessi-module/Dtos/RefreshTokenDtos.d.ts +1 -1
- package/dist/accessi-module/Dtos/RefreshTokenDtos.js +4 -1
- package/dist/accessi-module/Dtos/RefreshTokenDtos.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 +6 -0
- package/dist/accessi-module/index.js +31 -2
- 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/authCookies.d.ts +104 -0
- package/dist/accessi-module/security/authCookies.js +240 -0
- package/dist/accessi-module/security/authCookies.js.map +1 -0
- 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/accessi-module/security/validateOptions.js +34 -0
- package/dist/accessi-module/security/validateOptions.js.map +1 -1
- package/dist/emilsoftware-utilities-2.0.0-dev.78.tgz +0 -0
- package/dist/initEmilsoftwareModule.js +7 -0
- package/dist/initEmilsoftwareModule.js.map +1 -1
- package/docs/auth-cookies.md +123 -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.76.tgz +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
2
|
+
import type { AccessiOptions } from '../AccessiModule';
|
|
3
|
+
/** Modalita di autenticazione via cookie httpOnly. */
|
|
4
|
+
export interface AccessiAuthCookieOptions {
|
|
5
|
+
/** Default `true`: la sola presenza dell'oggetto abilita la modalita cookie. */
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
/** `Secure` sui cookie. Default `true`; disattivare solo in sviluppo locale HTTP. */
|
|
8
|
+
secure?: boolean;
|
|
9
|
+
/** Default `lax`. Per SPA su altro dominio serve `none` insieme a `secure: true`. */
|
|
10
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
11
|
+
/** Dominio dei cookie, opzionale. */
|
|
12
|
+
domain?: string;
|
|
13
|
+
/** Nome del cookie con l'access token. Default `accessi_access`. */
|
|
14
|
+
accessTokenName?: string;
|
|
15
|
+
/** Nome del cookie con il refresh token. Default `accessi_refresh`. */
|
|
16
|
+
refreshTokenName?: string;
|
|
17
|
+
/** Nome del cookie CSRF leggibile dal client. Default `accessi_csrf`. */
|
|
18
|
+
csrfTokenName?: string;
|
|
19
|
+
/** Path del cookie access. Default `/api/accessi`. */
|
|
20
|
+
accessPath?: string;
|
|
21
|
+
/** Path del cookie refresh. Default `/api/accessi/auth`. */
|
|
22
|
+
refreshPath?: string;
|
|
23
|
+
/** Path del cookie CSRF. Default `/`. */
|
|
24
|
+
csrfPath?: string;
|
|
25
|
+
/** Header CSRF richiesto. Default `x-csrf-token`. */
|
|
26
|
+
csrfHeaderName?: string;
|
|
27
|
+
/** `double-submit` (default) oppure `off` per host che gestiscono CSRF altrove. */
|
|
28
|
+
csrf?: 'double-submit' | 'off';
|
|
29
|
+
/**
|
|
30
|
+
* Restituisce comunque i token nel body JSON. Default `false`: in modalita
|
|
31
|
+
* cookie i token restano solo negli header httpOnly.
|
|
32
|
+
*/
|
|
33
|
+
exposeTokens?: boolean;
|
|
34
|
+
/** Legge il refresh token dal cookie quando il body non lo contiene. Default `true`. */
|
|
35
|
+
refreshFromCookie?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export type AccessiAuthCookieSetting = AccessiAuthCookieOptions;
|
|
38
|
+
export interface ResolvedAccessiAuthCookieOptions {
|
|
39
|
+
secure: boolean;
|
|
40
|
+
sameSite: 'strict' | 'lax' | 'none';
|
|
41
|
+
domain?: string;
|
|
42
|
+
accessTokenName: string;
|
|
43
|
+
refreshTokenName: string;
|
|
44
|
+
csrfTokenName: string;
|
|
45
|
+
accessPath: string;
|
|
46
|
+
refreshPath: string;
|
|
47
|
+
csrfPath: string;
|
|
48
|
+
csrfHeaderName: string;
|
|
49
|
+
csrf: 'double-submit' | 'off';
|
|
50
|
+
exposeTokens: boolean;
|
|
51
|
+
refreshFromCookie: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface AccessiAuthTokenPair {
|
|
54
|
+
access?: string;
|
|
55
|
+
refresh?: string;
|
|
56
|
+
}
|
|
57
|
+
/** Risolve la configurazione dei cookie; `undefined` quando la modalita e disattivata. */
|
|
58
|
+
export declare function resolveAccessiAuthCookieOptions(options: Pick<AccessiOptions, 'authCookie'>): ResolvedAccessiAuthCookieOptions | undefined;
|
|
59
|
+
/** Legge i cookie senza aggiungere la dipendenza `cookie-parser`. */
|
|
60
|
+
export declare function parseAccessiCookieHeader(header: string | undefined): Record<string, string>;
|
|
61
|
+
/** True per richieste che modificano stato (dove serve il CSRF). */
|
|
62
|
+
export declare function isAccessiUnsafeMethod(method: string): boolean;
|
|
63
|
+
/** True per gli endpoint pubblici esenti da CSRF. */
|
|
64
|
+
export declare function isAccessiCsrfExemptPath(path: string): boolean;
|
|
65
|
+
/** Imposta i cookie access, refresh e (se assente) CSRF. */
|
|
66
|
+
export declare function setAccessiAuthCookies(res: Pick<Response, 'append'>, config: ResolvedAccessiAuthCookieOptions, tokens: AccessiAuthTokenPair): void;
|
|
67
|
+
/** Cancella i cookie di sessione (logout/revoca). */
|
|
68
|
+
export declare function clearAccessiAuthCookies(res: Pick<Response, 'append'>, config: ResolvedAccessiAuthCookieOptions): void;
|
|
69
|
+
export interface AccessiAuthTokenValue {
|
|
70
|
+
value?: string;
|
|
71
|
+
}
|
|
72
|
+
/** Forma comune dei risultati di auth: stringa o `TokenResult` di Accessi. */
|
|
73
|
+
export interface AccessiAuthResultLike {
|
|
74
|
+
token?: string | AccessiAuthTokenValue;
|
|
75
|
+
refreshToken?: string | AccessiAuthTokenValue;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Prepara la risposta di login/refresh: imposta i cookie e rimuove i token dal
|
|
79
|
+
* body quando la modalita cookie non prevede `exposeTokens`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function applyAccessiAuthCookies<T extends AccessiAuthResultLike>(res: Pick<Response, 'append'>, config: ResolvedAccessiAuthCookieOptions | undefined, result: T): T;
|
|
82
|
+
/** Rimuove i token dal `LoginResult` quando la modalita cookie li nasconde. */
|
|
83
|
+
export declare function hideAccessiLoginTokens(config: ResolvedAccessiAuthCookieOptions | undefined, result: {
|
|
84
|
+
token?: unknown;
|
|
85
|
+
refreshToken?: unknown;
|
|
86
|
+
}): void;
|
|
87
|
+
/** Legge l'access token dal cookie (per test e per host senza middleware). */
|
|
88
|
+
export declare function getAccessiAccessTokenFromCookie(header: string | undefined, config: ResolvedAccessiAuthCookieOptions): string | undefined;
|
|
89
|
+
/** Legge il refresh token dal cookie. */
|
|
90
|
+
export declare function getAccessiRefreshTokenFromCookie(header: string | undefined, config: ResolvedAccessiAuthCookieOptions): string | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Middleware della modalita cookie.
|
|
93
|
+
*
|
|
94
|
+
* - copia l'access token dal cookie nell'header `Authorization`, cosi guard,
|
|
95
|
+
* middleware e controller esistenti funzionano senza modifiche;
|
|
96
|
+
* - garantisce un cookie CSRF double-submit leggibile dal client;
|
|
97
|
+
* - blocca le richieste autenticate via cookie che modificano stato senza
|
|
98
|
+
* l'header CSRF, salvo endpoint pubblici di bootstrap della sessione.
|
|
99
|
+
*/
|
|
100
|
+
export declare function createAccessiAuthCookieMiddleware(options: Pick<AccessiOptions, 'authCookie'>, isAccessiRoute: (path: string) => boolean): ((req: Request, res: Response, next: NextFunction) => void) | undefined;
|
|
101
|
+
/** True se il nome cookie e valido per l'header `Set-Cookie`. */
|
|
102
|
+
export declare function isValidAccessiCookieName(name: string): boolean;
|
|
103
|
+
/** Helpers per le combinazioni di cookie supportate (usati dalla validazione). */
|
|
104
|
+
export declare function accessiCookieNames(config: ResolvedAccessiAuthCookieOptions): string[];
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveAccessiAuthCookieOptions = resolveAccessiAuthCookieOptions;
|
|
4
|
+
exports.parseAccessiCookieHeader = parseAccessiCookieHeader;
|
|
5
|
+
exports.isAccessiUnsafeMethod = isAccessiUnsafeMethod;
|
|
6
|
+
exports.isAccessiCsrfExemptPath = isAccessiCsrfExemptPath;
|
|
7
|
+
exports.setAccessiAuthCookies = setAccessiAuthCookies;
|
|
8
|
+
exports.clearAccessiAuthCookies = clearAccessiAuthCookies;
|
|
9
|
+
exports.applyAccessiAuthCookies = applyAccessiAuthCookies;
|
|
10
|
+
exports.hideAccessiLoginTokens = hideAccessiLoginTokens;
|
|
11
|
+
exports.getAccessiAccessTokenFromCookie = getAccessiAccessTokenFromCookie;
|
|
12
|
+
exports.getAccessiRefreshTokenFromCookie = getAccessiRefreshTokenFromCookie;
|
|
13
|
+
exports.createAccessiAuthCookieMiddleware = createAccessiAuthCookieMiddleware;
|
|
14
|
+
exports.isValidAccessiCookieName = isValidAccessiCookieName;
|
|
15
|
+
exports.accessiCookieNames = accessiCookieNames;
|
|
16
|
+
const crypto_1 = require("crypto");
|
|
17
|
+
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
18
|
+
const COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
19
|
+
const DEFAULTS = {
|
|
20
|
+
secure: true,
|
|
21
|
+
sameSite: 'lax',
|
|
22
|
+
accessTokenName: 'accessi_access',
|
|
23
|
+
refreshTokenName: 'accessi_refresh',
|
|
24
|
+
csrfTokenName: 'accessi_csrf',
|
|
25
|
+
accessPath: '/api/accessi',
|
|
26
|
+
refreshPath: '/api/accessi/auth',
|
|
27
|
+
csrfPath: '/',
|
|
28
|
+
csrfHeaderName: 'x-csrf-token',
|
|
29
|
+
csrf: 'double-submit',
|
|
30
|
+
exposeTokens: false,
|
|
31
|
+
refreshFromCookie: true,
|
|
32
|
+
};
|
|
33
|
+
/** Percorsi pubblici che non richiedono il token CSRF (non hanno ancora una sessione). */
|
|
34
|
+
const CSRF_EXEMPT_PATHS = [
|
|
35
|
+
'/api/accessi/auth/login',
|
|
36
|
+
'/api/accessi/auth/refresh',
|
|
37
|
+
'/api/accessi/auth/confirm-reset-password',
|
|
38
|
+
'/api/accessi/auth/two-factor/verify',
|
|
39
|
+
'/api/accessi/auth/two-factor/resend',
|
|
40
|
+
'/api/accessi/auth/bootstrap-admin',
|
|
41
|
+
'/api/accessi/email/',
|
|
42
|
+
'/api/accessi/federated-auth/exchange',
|
|
43
|
+
'/api/accessi/user/register',
|
|
44
|
+
];
|
|
45
|
+
const UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
46
|
+
/** Risolve la configurazione dei cookie; `undefined` quando la modalita e disattivata. */
|
|
47
|
+
function resolveAccessiAuthCookieOptions(options) {
|
|
48
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
49
|
+
const raw = options.authCookie;
|
|
50
|
+
if (!raw || raw.enabled === false)
|
|
51
|
+
return undefined;
|
|
52
|
+
return Object.assign(Object.assign({ secure: raw.secure !== false, sameSite: (_a = raw.sameSite) !== null && _a !== void 0 ? _a : DEFAULTS.sameSite }, (raw.domain ? { domain: raw.domain } : {})), { accessTokenName: (_b = raw.accessTokenName) !== null && _b !== void 0 ? _b : DEFAULTS.accessTokenName, refreshTokenName: (_c = raw.refreshTokenName) !== null && _c !== void 0 ? _c : DEFAULTS.refreshTokenName, csrfTokenName: (_d = raw.csrfTokenName) !== null && _d !== void 0 ? _d : DEFAULTS.csrfTokenName, accessPath: (_e = raw.accessPath) !== null && _e !== void 0 ? _e : DEFAULTS.accessPath, refreshPath: (_f = raw.refreshPath) !== null && _f !== void 0 ? _f : DEFAULTS.refreshPath, csrfPath: (_g = raw.csrfPath) !== null && _g !== void 0 ? _g : DEFAULTS.csrfPath, csrfHeaderName: ((_h = raw.csrfHeaderName) !== null && _h !== void 0 ? _h : DEFAULTS.csrfHeaderName).toLowerCase(), csrf: (_j = raw.csrf) !== null && _j !== void 0 ? _j : DEFAULTS.csrf, exposeTokens: raw.exposeTokens === true, refreshFromCookie: raw.refreshFromCookie !== false });
|
|
53
|
+
}
|
|
54
|
+
/** Legge i cookie senza aggiungere la dipendenza `cookie-parser`. */
|
|
55
|
+
function parseAccessiCookieHeader(header) {
|
|
56
|
+
const cookies = {};
|
|
57
|
+
if (!header)
|
|
58
|
+
return cookies;
|
|
59
|
+
for (const part of header.split(';')) {
|
|
60
|
+
const separator = part.indexOf('=');
|
|
61
|
+
if (separator < 0)
|
|
62
|
+
continue;
|
|
63
|
+
const name = part.slice(0, separator).trim();
|
|
64
|
+
if (!name)
|
|
65
|
+
continue;
|
|
66
|
+
const rawValue = part.slice(separator + 1).trim();
|
|
67
|
+
try {
|
|
68
|
+
cookies[name] = decodeURIComponent(rawValue);
|
|
69
|
+
}
|
|
70
|
+
catch (_a) {
|
|
71
|
+
cookies[name] = rawValue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return cookies;
|
|
75
|
+
}
|
|
76
|
+
/** True per richieste che modificano stato (dove serve il CSRF). */
|
|
77
|
+
function isAccessiUnsafeMethod(method) {
|
|
78
|
+
return UNSAFE_METHODS.has(method.toUpperCase());
|
|
79
|
+
}
|
|
80
|
+
/** True per gli endpoint pubblici esenti da CSRF. */
|
|
81
|
+
function isAccessiCsrfExemptPath(path) {
|
|
82
|
+
return CSRF_EXEMPT_PATHS.some((exempt) => exempt.endsWith('/') ? path.startsWith(exempt) : path === exempt);
|
|
83
|
+
}
|
|
84
|
+
function tokenMaxAgeSeconds(token) {
|
|
85
|
+
if (!token)
|
|
86
|
+
return undefined;
|
|
87
|
+
const decoded = (0, jsonwebtoken_1.decode)(token);
|
|
88
|
+
if (!decoded || typeof decoded === 'string')
|
|
89
|
+
return undefined;
|
|
90
|
+
const exp = Number(decoded.exp);
|
|
91
|
+
if (!Number.isFinite(exp))
|
|
92
|
+
return undefined;
|
|
93
|
+
return Math.max(0, Math.floor(exp - Date.now() / 1000));
|
|
94
|
+
}
|
|
95
|
+
function serializeCookie(name, value, options) {
|
|
96
|
+
const parts = [`${name}=${encodeURIComponent(value)}`, `Path=${options.path}`];
|
|
97
|
+
if (options.domain)
|
|
98
|
+
parts.push(`Domain=${options.domain}`);
|
|
99
|
+
if (options.maxAgeSeconds !== undefined)
|
|
100
|
+
parts.push(`Max-Age=${options.maxAgeSeconds}`);
|
|
101
|
+
if (options.httpOnly)
|
|
102
|
+
parts.push('HttpOnly');
|
|
103
|
+
if (options.secure)
|
|
104
|
+
parts.push('Secure');
|
|
105
|
+
const sameSite = { strict: 'Strict', lax: 'Lax', none: 'None' }[options.sameSite];
|
|
106
|
+
parts.push(`SameSite=${sameSite}`);
|
|
107
|
+
return parts.join('; ');
|
|
108
|
+
}
|
|
109
|
+
function appendCookie(res, cookie) {
|
|
110
|
+
res.append('Set-Cookie', cookie);
|
|
111
|
+
}
|
|
112
|
+
/** Imposta i cookie access, refresh e (se assente) CSRF. */
|
|
113
|
+
function setAccessiAuthCookies(res, config, tokens) {
|
|
114
|
+
if (tokens.access) {
|
|
115
|
+
appendCookie(res, serializeCookie(config.accessTokenName, tokens.access, Object.assign(Object.assign(Object.assign({ path: config.accessPath }, (tokenMaxAgeSeconds(tokens.access) !== undefined
|
|
116
|
+
? { maxAgeSeconds: tokenMaxAgeSeconds(tokens.access) }
|
|
117
|
+
: {})), { httpOnly: true, secure: config.secure, sameSite: config.sameSite }), (config.domain ? { domain: config.domain } : {}))));
|
|
118
|
+
}
|
|
119
|
+
if (tokens.refresh) {
|
|
120
|
+
appendCookie(res, serializeCookie(config.refreshTokenName, tokens.refresh, Object.assign(Object.assign(Object.assign({ path: config.refreshPath }, (tokenMaxAgeSeconds(tokens.refresh) !== undefined
|
|
121
|
+
? { maxAgeSeconds: tokenMaxAgeSeconds(tokens.refresh) }
|
|
122
|
+
: {})), { httpOnly: true, secure: config.secure, sameSite: config.sameSite }), (config.domain ? { domain: config.domain } : {}))));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Cancella i cookie di sessione (logout/revoca). */
|
|
126
|
+
function clearAccessiAuthCookies(res, config) {
|
|
127
|
+
for (const [name, path] of [
|
|
128
|
+
[config.accessTokenName, config.accessPath],
|
|
129
|
+
[config.refreshTokenName, config.refreshPath],
|
|
130
|
+
[config.csrfTokenName, config.csrfPath],
|
|
131
|
+
]) {
|
|
132
|
+
appendCookie(res, serializeCookie(name, '', Object.assign({ path, maxAgeSeconds: 0, httpOnly: name !== config.csrfTokenName, secure: config.secure, sameSite: config.sameSite }, (config.domain ? { domain: config.domain } : {}))));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function accessiTokenValue(token) {
|
|
136
|
+
if (typeof token === 'string')
|
|
137
|
+
return token === '' ? undefined : token;
|
|
138
|
+
return token === null || token === void 0 ? void 0 : token.value;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Prepara la risposta di login/refresh: imposta i cookie e rimuove i token dal
|
|
142
|
+
* body quando la modalita cookie non prevede `exposeTokens`.
|
|
143
|
+
*/
|
|
144
|
+
function applyAccessiAuthCookies(res, config, result) {
|
|
145
|
+
if (!config)
|
|
146
|
+
return result;
|
|
147
|
+
setAccessiAuthCookies(res, config, {
|
|
148
|
+
access: accessiTokenValue(result.token),
|
|
149
|
+
refresh: accessiTokenValue(result.refreshToken),
|
|
150
|
+
});
|
|
151
|
+
hideAccessiLoginTokens(config, result);
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
/** Rimuove i token dal `LoginResult` quando la modalita cookie li nasconde. */
|
|
155
|
+
function hideAccessiLoginTokens(config, result) {
|
|
156
|
+
if (!config || config.exposeTokens)
|
|
157
|
+
return;
|
|
158
|
+
delete result.token;
|
|
159
|
+
delete result.refreshToken;
|
|
160
|
+
}
|
|
161
|
+
/** Legge l'access token dal cookie (per test e per host senza middleware). */
|
|
162
|
+
function getAccessiAccessTokenFromCookie(header, config) {
|
|
163
|
+
return parseAccessiCookieHeader(header)[config.accessTokenName];
|
|
164
|
+
}
|
|
165
|
+
/** Legge il refresh token dal cookie. */
|
|
166
|
+
function getAccessiRefreshTokenFromCookie(header, config) {
|
|
167
|
+
return parseAccessiCookieHeader(header)[config.refreshTokenName];
|
|
168
|
+
}
|
|
169
|
+
function safeEqual(left, right) {
|
|
170
|
+
const a = Buffer.from(left);
|
|
171
|
+
const b = Buffer.from(right);
|
|
172
|
+
return a.length === b.length && (0, crypto_1.timingSafeEqual)(a, b);
|
|
173
|
+
}
|
|
174
|
+
function csrfError(res) {
|
|
175
|
+
res.status(403).json({
|
|
176
|
+
severity: 'error',
|
|
177
|
+
status: 403,
|
|
178
|
+
statusCode: 2,
|
|
179
|
+
code: 'AUTH_CSRF_INVALID',
|
|
180
|
+
error: 'AUTH_CSRF_INVALID',
|
|
181
|
+
message: 'Token CSRF mancante o non valido.',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Middleware della modalita cookie.
|
|
186
|
+
*
|
|
187
|
+
* - copia l'access token dal cookie nell'header `Authorization`, cosi guard,
|
|
188
|
+
* middleware e controller esistenti funzionano senza modifiche;
|
|
189
|
+
* - garantisce un cookie CSRF double-submit leggibile dal client;
|
|
190
|
+
* - blocca le richieste autenticate via cookie che modificano stato senza
|
|
191
|
+
* l'header CSRF, salvo endpoint pubblici di bootstrap della sessione.
|
|
192
|
+
*/
|
|
193
|
+
function createAccessiAuthCookieMiddleware(options, isAccessiRoute) {
|
|
194
|
+
const config = resolveAccessiAuthCookieOptions(options);
|
|
195
|
+
if (!config)
|
|
196
|
+
return undefined;
|
|
197
|
+
return (req, res, next) => {
|
|
198
|
+
if (!isAccessiRoute(req.path)) {
|
|
199
|
+
next();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const cookies = parseAccessiCookieHeader(req.headers.cookie);
|
|
203
|
+
let csrfToken = cookies[config.csrfTokenName];
|
|
204
|
+
if (!csrfToken && config.csrf === 'double-submit') {
|
|
205
|
+
csrfToken = (0, crypto_1.randomBytes)(32).toString('base64url');
|
|
206
|
+
appendCookie(res, serializeCookie(config.csrfTokenName, csrfToken, Object.assign({ path: config.csrfPath, httpOnly: false, secure: config.secure, sameSite: config.sameSite }, (config.domain ? { domain: config.domain } : {}))));
|
|
207
|
+
}
|
|
208
|
+
if (config.csrf === 'double-submit' && csrfToken) {
|
|
209
|
+
// Rende il token leggibile anche a una SPA su altra origine (via CORS,
|
|
210
|
+
// con `Access-Control-Expose-Headers: X-CSRF-Token`).
|
|
211
|
+
res.setHeader('X-CSRF-Token', csrfToken);
|
|
212
|
+
}
|
|
213
|
+
const accessToken = cookies[config.accessTokenName];
|
|
214
|
+
const hasAuthorization = typeof req.headers.authorization === 'string'
|
|
215
|
+
&& req.headers.authorization.trim() !== '';
|
|
216
|
+
if (accessToken && !hasAuthorization) {
|
|
217
|
+
req.headers.authorization = `Bearer ${accessToken}`;
|
|
218
|
+
if (config.csrf === 'double-submit'
|
|
219
|
+
&& isAccessiUnsafeMethod(req.method)
|
|
220
|
+
&& !isAccessiCsrfExemptPath(req.path)) {
|
|
221
|
+
const headerValue = req.headers[config.csrfHeaderName];
|
|
222
|
+
const provided = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
223
|
+
if (!csrfToken || !provided || !safeEqual(provided, csrfToken)) {
|
|
224
|
+
csrfError(res);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
next();
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** True se il nome cookie e valido per l'header `Set-Cookie`. */
|
|
233
|
+
function isValidAccessiCookieName(name) {
|
|
234
|
+
return COOKIE_NAME.test(name);
|
|
235
|
+
}
|
|
236
|
+
/** Helpers per le combinazioni di cookie supportate (usati dalla validazione). */
|
|
237
|
+
function accessiCookieNames(config) {
|
|
238
|
+
return [config.accessTokenName, config.refreshTokenName, config.csrfTokenName];
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=authCookies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authCookies.js","sourceRoot":"","sources":["../../../src/accessi-module/security/authCookies.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes, timingSafeEqual } from 'crypto';\nimport type { NextFunction, Request, Response } from 'express';\nimport type * as jwt from 'jsonwebtoken';\nimport { decode as decodeJwt } from 'jsonwebtoken';\nimport type { AccessiOptions } from '../AccessiModule';\n\n/** Modalita di autenticazione via cookie httpOnly. */\nexport interface AccessiAuthCookieOptions {\n /** Default `true`: la sola presenza dell'oggetto abilita la modalita cookie. */\n enabled?: boolean;\n /** `Secure` sui cookie. Default `true`; disattivare solo in sviluppo locale HTTP. */\n secure?: boolean;\n /** Default `lax`. Per SPA su altro dominio serve `none` insieme a `secure: true`. */\n sameSite?: 'strict' | 'lax' | 'none';\n /** Dominio dei cookie, opzionale. */\n domain?: string;\n /** Nome del cookie con l'access token. Default `accessi_access`. */\n accessTokenName?: string;\n /** Nome del cookie con il refresh token. Default `accessi_refresh`. */\n refreshTokenName?: string;\n /** Nome del cookie CSRF leggibile dal client. Default `accessi_csrf`. */\n csrfTokenName?: string;\n /** Path del cookie access. Default `/api/accessi`. */\n accessPath?: string;\n /** Path del cookie refresh. Default `/api/accessi/auth`. */\n refreshPath?: string;\n /** Path del cookie CSRF. Default `/`. */\n csrfPath?: string;\n /** Header CSRF richiesto. Default `x-csrf-token`. */\n csrfHeaderName?: string;\n /** `double-submit` (default) oppure `off` per host che gestiscono CSRF altrove. */\n csrf?: 'double-submit' | 'off';\n /**\n * Restituisce comunque i token nel body JSON. Default `false`: in modalita\n * cookie i token restano solo negli header httpOnly.\n */\n exposeTokens?: boolean;\n /** Legge il refresh token dal cookie quando il body non lo contiene. Default `true`. */\n refreshFromCookie?: boolean;\n}\n\nexport type AccessiAuthCookieSetting = AccessiAuthCookieOptions;\n\nexport interface ResolvedAccessiAuthCookieOptions {\n secure: boolean;\n sameSite: 'strict' | 'lax' | 'none';\n domain?: string;\n accessTokenName: string;\n refreshTokenName: string;\n csrfTokenName: string;\n accessPath: string;\n refreshPath: string;\n csrfPath: string;\n csrfHeaderName: string;\n csrf: 'double-submit' | 'off';\n exposeTokens: boolean;\n refreshFromCookie: boolean;\n}\n\nexport interface AccessiAuthTokenPair {\n access?: string;\n refresh?: string;\n}\n\nconst COOKIE_NAME = /^[!#$%&'*+\\-.^_`|~0-9A-Za-z]+$/;\n\nconst DEFAULTS = {\n secure: true,\n sameSite: 'lax' as const,\n accessTokenName: 'accessi_access',\n refreshTokenName: 'accessi_refresh',\n csrfTokenName: 'accessi_csrf',\n accessPath: '/api/accessi',\n refreshPath: '/api/accessi/auth',\n csrfPath: '/',\n csrfHeaderName: 'x-csrf-token',\n csrf: 'double-submit' as const,\n exposeTokens: false,\n refreshFromCookie: true,\n};\n\n/** Percorsi pubblici che non richiedono il token CSRF (non hanno ancora una sessione). */\nconst CSRF_EXEMPT_PATHS = [\n '/api/accessi/auth/login',\n '/api/accessi/auth/refresh',\n '/api/accessi/auth/confirm-reset-password',\n '/api/accessi/auth/two-factor/verify',\n '/api/accessi/auth/two-factor/resend',\n '/api/accessi/auth/bootstrap-admin',\n '/api/accessi/email/',\n '/api/accessi/federated-auth/exchange',\n '/api/accessi/user/register',\n];\n\nconst UNSAFE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);\n\n/** Risolve la configurazione dei cookie; `undefined` quando la modalita e disattivata. */\nexport function resolveAccessiAuthCookieOptions(\n options: Pick<AccessiOptions, 'authCookie'>,\n): ResolvedAccessiAuthCookieOptions | undefined {\n const raw = options.authCookie;\n if (!raw || raw.enabled === false) return undefined;\n\n return {\n secure: raw.secure !== false,\n sameSite: raw.sameSite ?? DEFAULTS.sameSite,\n ...(raw.domain ? { domain: raw.domain } : {}),\n accessTokenName: raw.accessTokenName ?? DEFAULTS.accessTokenName,\n refreshTokenName: raw.refreshTokenName ?? DEFAULTS.refreshTokenName,\n csrfTokenName: raw.csrfTokenName ?? DEFAULTS.csrfTokenName,\n accessPath: raw.accessPath ?? DEFAULTS.accessPath,\n refreshPath: raw.refreshPath ?? DEFAULTS.refreshPath,\n csrfPath: raw.csrfPath ?? DEFAULTS.csrfPath,\n csrfHeaderName: (raw.csrfHeaderName ?? DEFAULTS.csrfHeaderName).toLowerCase(),\n csrf: raw.csrf ?? DEFAULTS.csrf,\n exposeTokens: raw.exposeTokens === true,\n refreshFromCookie: raw.refreshFromCookie !== false,\n };\n}\n\n/** Legge i cookie senza aggiungere la dipendenza `cookie-parser`. */\nexport function parseAccessiCookieHeader(header: string | undefined): Record<string, string> {\n const cookies: Record<string, string> = {};\n if (!header) return cookies;\n for (const part of header.split(';')) {\n const separator = part.indexOf('=');\n if (separator < 0) continue;\n const name = part.slice(0, separator).trim();\n if (!name) continue;\n const rawValue = part.slice(separator + 1).trim();\n try {\n cookies[name] = decodeURIComponent(rawValue);\n } catch {\n cookies[name] = rawValue;\n }\n }\n return cookies;\n}\n\n/** True per richieste che modificano stato (dove serve il CSRF). */\nexport function isAccessiUnsafeMethod(method: string): boolean {\n return UNSAFE_METHODS.has(method.toUpperCase());\n}\n\n/** True per gli endpoint pubblici esenti da CSRF. */\nexport function isAccessiCsrfExemptPath(path: string): boolean {\n return CSRF_EXEMPT_PATHS.some((exempt) =>\n exempt.endsWith('/') ? path.startsWith(exempt) : path === exempt,\n );\n}\n\nfunction tokenMaxAgeSeconds(token: string | undefined): number | undefined {\n if (!token) return undefined;\n const decoded = decodeJwt(token) as jwt.JwtPayload | string | null;\n if (!decoded || typeof decoded === 'string') return undefined;\n const exp = Number(decoded.exp);\n if (!Number.isFinite(exp)) return undefined;\n return Math.max(0, Math.floor(exp - Date.now() / 1000));\n}\n\nfunction serializeCookie(\n name: string,\n value: string,\n options: {\n path: string;\n maxAgeSeconds?: number;\n httpOnly?: boolean;\n secure: boolean;\n sameSite: 'strict' | 'lax' | 'none';\n domain?: string;\n },\n): string {\n const parts = [`${name}=${encodeURIComponent(value)}`, `Path=${options.path}`];\n if (options.domain) parts.push(`Domain=${options.domain}`);\n if (options.maxAgeSeconds !== undefined) parts.push(`Max-Age=${options.maxAgeSeconds}`);\n if (options.httpOnly) parts.push('HttpOnly');\n if (options.secure) parts.push('Secure');\n const sameSite = { strict: 'Strict', lax: 'Lax', none: 'None' }[options.sameSite];\n parts.push(`SameSite=${sameSite}`);\n return parts.join('; ');\n}\n\nfunction appendCookie(res: Pick<Response, 'append'>, cookie: string): void {\n res.append('Set-Cookie', cookie);\n}\n\n/** Imposta i cookie access, refresh e (se assente) CSRF. */\nexport function setAccessiAuthCookies(\n res: Pick<Response, 'append'>,\n config: ResolvedAccessiAuthCookieOptions,\n tokens: AccessiAuthTokenPair,\n): void {\n if (tokens.access) {\n appendCookie(res, serializeCookie(config.accessTokenName, tokens.access, {\n path: config.accessPath,\n ...(tokenMaxAgeSeconds(tokens.access) !== undefined\n ? { maxAgeSeconds: tokenMaxAgeSeconds(tokens.access) }\n : {}),\n httpOnly: true,\n secure: config.secure,\n sameSite: config.sameSite,\n ...(config.domain ? { domain: config.domain } : {}),\n }));\n }\n if (tokens.refresh) {\n appendCookie(res, serializeCookie(config.refreshTokenName, tokens.refresh, {\n path: config.refreshPath,\n ...(tokenMaxAgeSeconds(tokens.refresh) !== undefined\n ? { maxAgeSeconds: tokenMaxAgeSeconds(tokens.refresh) }\n : {}),\n httpOnly: true,\n secure: config.secure,\n sameSite: config.sameSite,\n ...(config.domain ? { domain: config.domain } : {}),\n }));\n }\n}\n\n/** Cancella i cookie di sessione (logout/revoca). */\nexport function clearAccessiAuthCookies(\n res: Pick<Response, 'append'>,\n config: ResolvedAccessiAuthCookieOptions,\n): void {\n for (const [name, path] of [\n [config.accessTokenName, config.accessPath],\n [config.refreshTokenName, config.refreshPath],\n [config.csrfTokenName, config.csrfPath],\n ] as const) {\n appendCookie(res, serializeCookie(name, '', {\n path,\n maxAgeSeconds: 0,\n httpOnly: name !== config.csrfTokenName,\n secure: config.secure,\n sameSite: config.sameSite,\n ...(config.domain ? { domain: config.domain } : {}),\n }));\n }\n}\n\nexport interface AccessiAuthTokenValue {\n value?: string;\n}\n\n/** Forma comune dei risultati di auth: stringa o `TokenResult` di Accessi. */\nexport interface AccessiAuthResultLike {\n token?: string | AccessiAuthTokenValue;\n refreshToken?: string | AccessiAuthTokenValue;\n}\n\nfunction accessiTokenValue(token: string | AccessiAuthTokenValue | undefined): string | undefined {\n if (typeof token === 'string') return token === '' ? undefined : token;\n return token?.value;\n}\n\n/**\n * Prepara la risposta di login/refresh: imposta i cookie e rimuove i token dal\n * body quando la modalita cookie non prevede `exposeTokens`.\n */\nexport function applyAccessiAuthCookies<T extends AccessiAuthResultLike>(\n res: Pick<Response, 'append'>,\n config: ResolvedAccessiAuthCookieOptions | undefined,\n result: T,\n): T {\n if (!config) return result;\n setAccessiAuthCookies(res, config, {\n access: accessiTokenValue(result.token),\n refresh: accessiTokenValue(result.refreshToken),\n });\n hideAccessiLoginTokens(config, result);\n return result;\n}\n\n/** Rimuove i token dal `LoginResult` quando la modalita cookie li nasconde. */\nexport function hideAccessiLoginTokens(\n config: ResolvedAccessiAuthCookieOptions | undefined,\n result: { token?: unknown; refreshToken?: unknown },\n): void {\n if (!config || config.exposeTokens) return;\n delete result.token;\n delete result.refreshToken;\n}\n\n/** Legge l'access token dal cookie (per test e per host senza middleware). */\nexport function getAccessiAccessTokenFromCookie(\n header: string | undefined,\n config: ResolvedAccessiAuthCookieOptions,\n): string | undefined {\n return parseAccessiCookieHeader(header)[config.accessTokenName];\n}\n\n/** Legge il refresh token dal cookie. */\nexport function getAccessiRefreshTokenFromCookie(\n header: string | undefined,\n config: ResolvedAccessiAuthCookieOptions,\n): string | undefined {\n return parseAccessiCookieHeader(header)[config.refreshTokenName];\n}\n\nfunction safeEqual(left: string, right: string): boolean {\n const a = Buffer.from(left);\n const b = Buffer.from(right);\n return a.length === b.length && timingSafeEqual(a, b);\n}\n\nfunction csrfError(res: Response): void {\n res.status(403).json({\n severity: 'error',\n status: 403,\n statusCode: 2,\n code: 'AUTH_CSRF_INVALID',\n error: 'AUTH_CSRF_INVALID',\n message: 'Token CSRF mancante o non valido.',\n });\n}\n\n/**\n * Middleware della modalita cookie.\n *\n * - copia l'access token dal cookie nell'header `Authorization`, cosi guard,\n * middleware e controller esistenti funzionano senza modifiche;\n * - garantisce un cookie CSRF double-submit leggibile dal client;\n * - blocca le richieste autenticate via cookie che modificano stato senza\n * l'header CSRF, salvo endpoint pubblici di bootstrap della sessione.\n */\nexport function createAccessiAuthCookieMiddleware(\n options: Pick<AccessiOptions, 'authCookie'>,\n isAccessiRoute: (path: string) => boolean,\n): ((req: Request, res: Response, next: NextFunction) => void) | undefined {\n const config = resolveAccessiAuthCookieOptions(options);\n if (!config) return undefined;\n\n return (req: Request, res: Response, next: NextFunction): void => {\n if (!isAccessiRoute(req.path)) {\n next();\n return;\n }\n\n const cookies = parseAccessiCookieHeader(req.headers.cookie);\n let csrfToken = cookies[config.csrfTokenName];\n if (!csrfToken && config.csrf === 'double-submit') {\n csrfToken = randomBytes(32).toString('base64url');\n appendCookie(res, serializeCookie(config.csrfTokenName, csrfToken, {\n path: config.csrfPath,\n httpOnly: false,\n secure: config.secure,\n sameSite: config.sameSite,\n ...(config.domain ? { domain: config.domain } : {}),\n }));\n }\n if (config.csrf === 'double-submit' && csrfToken) {\n // Rende il token leggibile anche a una SPA su altra origine (via CORS,\n // con `Access-Control-Expose-Headers: X-CSRF-Token`).\n res.setHeader('X-CSRF-Token', csrfToken);\n }\n\n const accessToken = cookies[config.accessTokenName];\n const hasAuthorization = typeof req.headers.authorization === 'string'\n && req.headers.authorization.trim() !== '';\n if (accessToken && !hasAuthorization) {\n req.headers.authorization = `Bearer ${accessToken}`;\n\n if (\n config.csrf === 'double-submit'\n && isAccessiUnsafeMethod(req.method)\n && !isAccessiCsrfExemptPath(req.path)\n ) {\n const headerValue = req.headers[config.csrfHeaderName];\n const provided = Array.isArray(headerValue) ? headerValue[0] : headerValue;\n if (!csrfToken || !provided || !safeEqual(provided, csrfToken)) {\n csrfError(res);\n return;\n }\n }\n }\n\n next();\n };\n}\n\n/** True se il nome cookie e valido per l'header `Set-Cookie`. */\nexport function isValidAccessiCookieName(name: string): boolean {\n return COOKIE_NAME.test(name);\n}\n\n/** Helpers per le combinazioni di cookie supportate (usati dalla validazione). */\nexport function accessiCookieNames(config: ResolvedAccessiAuthCookieOptions): string[] {\n return [config.accessTokenName, config.refreshTokenName, config.csrfTokenName];\n}\n"]}
|
|
@@ -16,6 +16,7 @@ exports.consumeConsoleEntryTicket = consumeConsoleEntryTicket;
|
|
|
16
16
|
exports.clearConsoleEntryTickets = clearConsoleEntryTickets;
|
|
17
17
|
exports.createRedisConsoleEntryStore = createRedisConsoleEntryStore;
|
|
18
18
|
const crypto_1 = require("crypto");
|
|
19
|
+
const redisMetrics_1 = require("../observability/redisMetrics");
|
|
19
20
|
const DEFAULT_TTL_MS = 60000;
|
|
20
21
|
const DEFAULT_MAX_ENTRIES = 5000;
|
|
21
22
|
const TICKET_PATTERN = /^[a-f0-9]{64}$/i;
|
|
@@ -158,6 +159,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
158
159
|
const key = deriveEncryptionKey(options.encryptionKey);
|
|
159
160
|
const keyPrefix = ((_a = options.keyPrefix) !== null && _a !== void 0 ? _a : 'accessi:console-entry:').replace(/:+$/, ':');
|
|
160
161
|
const maxAttempts = Math.max(1, (_b = options.maxIssueAttempts) !== null && _b !== void 0 ? _b : 5);
|
|
162
|
+
const client = (0, redisMetrics_1.instrumentAccessiRedisClient)('console_entry', options.client);
|
|
161
163
|
return {
|
|
162
164
|
issue(record, ttlMs) {
|
|
163
165
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -165,7 +167,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
165
167
|
const value = encryptRecord(record, key);
|
|
166
168
|
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
167
169
|
const ticket = createTicket();
|
|
168
|
-
const result = yield
|
|
170
|
+
const result = yield client.eval(SET_SCRIPT, 1, `${keyPrefix}${ticket}`, value, safeTtl);
|
|
169
171
|
if (result) {
|
|
170
172
|
return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };
|
|
171
173
|
}
|
|
@@ -178,7 +180,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
178
180
|
if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {
|
|
179
181
|
return null;
|
|
180
182
|
}
|
|
181
|
-
const value = yield
|
|
183
|
+
const value = yield client.eval(CONSUME_SCRIPT, 1, `${keyPrefix}${ticket}`);
|
|
182
184
|
if (typeof value !== 'string')
|
|
183
185
|
return null;
|
|
184
186
|
return decryptRecord(value, key);
|
|
@@ -187,7 +189,7 @@ function createRedisConsoleEntryStore(options) {
|
|
|
187
189
|
healthCheck() {
|
|
188
190
|
return __awaiter(this, void 0, void 0, function* () {
|
|
189
191
|
try {
|
|
190
|
-
const result = yield
|
|
192
|
+
const result = yield client.eval("return redis.call('PING')", 0);
|
|
191
193
|
return String(result).toUpperCase() === 'PONG';
|
|
192
194
|
}
|
|
193
195
|
catch (_a) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consoleEntry.js","sourceRoot":"","sources":["../../../src/accessi-module/security/consoleEntry.ts"],"names":[],"mappings":";;;;;;;;;;;AAyDA,sEAsDC;AAKD,kEAEC;AAGD,0DAKC;AAED,8DAEC;AAGD,4DAEC;AAyED,oEAuDC;AAvQD,mCAAmF;AAiCnF,MAAM,cAAc,GAAG,KAAM,CAAC;AAC9B,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACjC,MAAM,cAAc,GAAG,iBAAiB,CAAC;AAazC,SAAS,YAAY;IACnB,OAAO,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AACxC,CAAC;AAED,mEAAmE;AACnE,SAAgB,6BAA6B,CAC3C,aAAqB,mBAAmB;IAExC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE/C,MAAM,KAAK,GAAG,CAAC,GAAW,EAAQ,EAAE;QAClC,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACvC,IAAI,MAAM,CAAC,SAAS,IAAI,GAAG;gBAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtD,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,CAAC,MAA0B,EAAE,QAAgB,cAAc,EAAsB,EAAE;QACnG,4EAA4E;QAC5E,sCAAsC;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,CAAC,GAAG,CAAC,CAAC;QAEX,OAAO,OAAO,CAAC,IAAI,IAAI,UAAU,EAAE,CAAC;YAClC,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC3C,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM;YAChC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACzB,CAAC;QAED,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,kCAAO,MAAM,KAAE,SAAS,EAAE,GAAG,GAAG,GAAG,IAAG,CAAC;QACzD,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC;IAC1E,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,CAAC,MAAc,EAA6B,EAAE;QAChE,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/D,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QACzB,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACvB,IAAI,MAAM,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE;YAAE,OAAO,IAAI,CAAC;QAChD,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC;IACpE,CAAC,CAAC;IAEF,OAAO;QACL,SAAS;QACT,WAAW;QACX,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE;QACtB,KAAK,CAAC,MAAM,EAAE,KAAK;;gBACvB,OAAO,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;YAClC,CAAC;SAAA;QACK,OAAO,CAAC,MAAM;;gBAClB,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC;YAC7B,CAAC;SAAA;QACK,WAAW;;gBACf,OAAO,IAAI,CAAC;YACd,CAAC;SAAA;KACF,CAAC;AACJ,CAAC;AAED,MAAM,YAAY,GAAG,6BAA6B,EAAE,CAAC;AAErD,uGAAuG;AACvG,SAAgB,2BAA2B;IACzC,OAAO,YAAY,CAAC;AACtB,CAAC;AAED,yFAAyF;AACzF,SAAgB,uBAAuB,CACrC,MAA0B,EAC1B,QAAgB,cAAc;IAE9B,OAAO,YAAY,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAC/C,CAAC;AAED,SAAgB,yBAAyB,CAAC,MAAe;IACvD,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9E,CAAC;AAED,8DAA8D;AAC9D,SAAgB,wBAAwB;IACtC,YAAY,CAAC,KAAK,EAAE,CAAC;AACvB,CAAC;AAiBD,MAAM,UAAU,GAAG,iEAAiE,CAAC;AACrF,MAAM,cAAc,GAClB,oGAAoG,CAAC;AAEvG,SAAS,mBAAmB,CAAC,MAAuB;IAClD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9E,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;IACxE,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC;AACtF,CAAC;AAED,SAAS,aAAa,CAAC,MAA0B,EAAE,GAAW;IAC5D,MAAM,EAAE,GAAG,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,IAAA,uBAAc,EAAC,aAAa,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC;QAC/B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAC7C,MAAM,CAAC,KAAK,EAAE;KACf,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAChC,OAAO,CAAC,EAAE,EAAE,GAAG,EAAE,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACnF,CAAC;AAED,SAAS,aAAa,CAAC,KAAa,EAAE,GAAW;IAC/C,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,cAAc,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3D,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IACxD,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAA,yBAAgB,EAAC,aAAa,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;QACxF,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;QACvD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC;YAC9B,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;YACzD,QAAQ,CAAC,KAAK,EAAE;SACjB,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACpB,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC9C,IACE,CAAC,MAAM;YACP,OAAO,MAAM,KAAK,QAAQ;YAC1B,OAAQ,MAA6B,CAAC,KAAK,KAAK,QAAQ;YACxD,OAAQ,MAA6B,CAAC,YAAY,KAAK,QAAQ,EAC/D,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,MAA4B,CAAC;IACtC,CAAC;IAAC,WAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,4BAA4B,CAC1C,OAAsC;;IAEtC,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,yDAAyD,CAAC,CAAC;IAC7E,CAAC;IAED,MAAM,GAAG,GAAG,mBAAmB,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,CAAC,MAAA,OAAO,CAAC,SAAS,mCAAI,wBAAwB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACtF,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAA,OAAO,CAAC,gBAAgB,mCAAI,CAAC,CAAC,CAAC;IAE/D,OAAO;QACC,KAAK,CAAC,MAAM,EAAE,KAAK;;gBACvB,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpC,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;gBAEzC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;oBAC1D,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;oBAC9B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CACtC,UAAU,EACV,CAAC,EACD,GAAG,SAAS,GAAG,MAAM,EAAE,EACvB,KAAK,EACL,OAAO,CACR,CAAC;oBACF,IAAI,MAAM,EAAE,CAAC;wBACX,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;oBACjE,CAAC;gBACH,CAAC;gBAED,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;YACrE,CAAC;SAAA;QAEK,OAAO,CAAC,MAAM;;gBAClB,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC/D,OAAO,IAAI,CAAC;gBACd,CAAC;gBACD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CACrC,cAAc,EACd,CAAC,EACD,GAAG,SAAS,GAAG,MAAM,EAAE,CACxB,CAAC;gBACF,IAAI,OAAO,KAAK,KAAK,QAAQ;oBAAE,OAAO,IAAI,CAAC;gBAC3C,OAAO,aAAa,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YACnC,CAAC;SAAA;QAEK,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":["import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';\n\n/**\n * Ticket di ingresso alla console, monouso e a breve scadenza.\n *\n * Serve a passare una sessione gia autenticata (frontend che possiede un JWT Accessi)\n * alla console amministrativa senza reinserire le credenziali e senza mettere il JWT\n * nell'URL: il frontend scambia il proprio JWT con un ticket opaco, apre la console\n * con `#entry=<ticket>` e la console lo scambia con il token originale.\n *\n * Il ticket e valido una sola volta, per pochi secondi, ed e legato all'utente che\n * lo ha richiesto (che deve avere accesso alla console). Lo store di default e in\n * memoria; in multi-istanza usare `createRedisConsoleEntryStore`.\n */\n\nexport interface ConsoleEntryRecord {\n token: string;\n codiceUtente: number;\n}\n\nexport interface ConsoleEntryTicket {\n ticket: string;\n expiresInSeconds: number;\n}\n\n/** Store pluggable: memoria per single instance, Redis per multi-istanza. */\nexport interface ConsoleEntryStore {\n issue(record: ConsoleEntryRecord, ttlMs: number): Promise<ConsoleEntryTicket>;\n consume(ticket: string): Promise<ConsoleEntryRecord | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_ENTRIES = 5000;\nconst TICKET_PATTERN = /^[a-f0-9]{64}$/i;\n\ninterface StoredEntry extends ConsoleEntryRecord {\n expiresAt: number;\n}\n\n/** Store in memoria con API sincrona per retrocompatibilita e API async per l'interfaccia. */\nexport interface MemoryConsoleEntryStore extends ConsoleEntryStore {\n issueSync(record: ConsoleEntryRecord, ttlMs?: number): ConsoleEntryTicket;\n consumeSync(ticket: string): ConsoleEntryRecord | null;\n clear(): void;\n}\n\nfunction createTicket(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction normalizeTtl(ttlMs: number): number {\n return Math.max(1, Math.trunc(ttlMs));\n}\n\n/** Store in memoria: adatto a sviluppo e a una singola istanza. */\nexport function createMemoryConsoleEntryStore(\n maxEntries: number = DEFAULT_MAX_ENTRIES,\n): MemoryConsoleEntryStore {\n const entries = new Map<string, StoredEntry>();\n\n const sweep = (now: number): void => {\n for (const [ticket, record] of entries) {\n if (record.expiresAt <= now) entries.delete(ticket);\n }\n };\n\n const issueSync = (record: ConsoleEntryRecord, ttlMs: number = DEFAULT_TTL_MS): ConsoleEntryTicket => {\n // Il TTL negativo resta supportato per test e per il comportamento storico:\n // il ticket e immediatamente scaduto.\n const ttl = Math.trunc(ttlMs);\n const now = Date.now();\n sweep(now);\n\n while (entries.size >= maxEntries) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n\n const ticket = createTicket();\n entries.set(ticket, { ...record, expiresAt: now + ttl });\n return { ticket, expiresInSeconds: Math.max(0, Math.ceil(ttl / 1000)) };\n };\n\n const consumeSync = (ticket: string): ConsoleEntryRecord | null => {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const record = entries.get(ticket);\n if (!record) return null;\n entries.delete(ticket);\n if (record.expiresAt <= Date.now()) return null;\n return { token: record.token, codiceUtente: record.codiceUtente };\n };\n\n return {\n issueSync,\n consumeSync,\n clear: () => entries.clear(),\n async issue(record, ttlMs) {\n return issueSync(record, ttlMs);\n },\n async consume(ticket) {\n return consumeSync(ticket);\n },\n async healthCheck() {\n return true;\n },\n };\n}\n\nconst defaultStore = createMemoryConsoleEntryStore();\n\n/** Store di default in memoria, usato quando `AccessiOptions.consoleEntry.store` non e configurato. */\nexport function getDefaultConsoleEntryStore(): ConsoleEntryStore {\n return defaultStore;\n}\n\n// Funzioni storiche sincrone: mantengono il comportamento per test e consumer esistenti.\nexport function issueConsoleEntryTicket(\n record: ConsoleEntryRecord,\n ttlMs: number = DEFAULT_TTL_MS,\n): ConsoleEntryTicket {\n return defaultStore.issueSync(record, ttlMs);\n}\n\nexport function consumeConsoleEntryTicket(ticket: unknown): ConsoleEntryRecord | null {\n return typeof ticket === 'string' ? defaultStore.consumeSync(ticket) : null;\n}\n\n/** Solo per i test: azzera lo store in memoria di default. */\nexport function clearConsoleEntryTickets(): void {\n defaultStore.clear();\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface ConsoleEntryRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisConsoleEntryStoreOptions {\n client: ConsoleEntryRedisClient;\n /** Segreto per cifrare il JWT nel Redis. Non usare il JWT in chiaro. */\n encryptionKey: string | Buffer;\n /** Default: `accessi:console-entry:`. */\n keyPrefix?: string;\n /** Tentativi in caso di collisione del ticket. Default: 5. */\n maxIssueAttempts?: number;\n}\n\nconst SET_SCRIPT = \"return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2], 'NX')\";\nconst CONSUME_SCRIPT =\n \"local value = redis.call('GET', KEYS[1]) if value then redis.call('DEL', KEYS[1]) end return value\";\n\nfunction deriveEncryptionKey(secret: string | Buffer): Buffer {\n const source = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, 'utf8');\n if (source.length === 0) {\n throw new Error('Redis console entry encryptionKey non configurata.');\n }\n return source.length === 32 ? source : createHash('sha256').update(source).digest();\n}\n\nfunction encryptRecord(record: ConsoleEntryRecord, key: Buffer): string {\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const ciphertext = Buffer.concat([\n cipher.update(JSON.stringify(record), 'utf8'),\n cipher.final(),\n ]);\n const tag = cipher.getAuthTag();\n return [iv, tag, ciphertext].map((part) => part.toString('base64url')).join('.');\n}\n\nfunction decryptRecord(value: string, key: Buffer): ConsoleEntryRecord | null {\n const [ivPart, tagPart, ciphertextPart] = value.split('.');\n if (!ivPart || !tagPart || !ciphertextPart) return null;\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivPart, 'base64url'));\n decipher.setAuthTag(Buffer.from(tagPart, 'base64url'));\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(ciphertextPart, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n const parsed: unknown = JSON.parse(plaintext);\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n typeof (parsed as ConsoleEntryRecord).token !== 'string' ||\n typeof (parsed as ConsoleEntryRecord).codiceUtente !== 'number'\n ) {\n return null;\n }\n return parsed as ConsoleEntryRecord;\n } catch {\n return null;\n }\n}\n\n/**\n * Store Redis per ticket console.\n *\n * - il JWT viene cifrato con AES-256-GCM prima di essere scritto;\n * - `SET ... PX ... NX` evita collisioni;\n * - il consume usa `GET` + `DEL` atomici in Lua, quindi il ticket e monouso\n * anche con piu istanze.\n */\nexport function createRedisConsoleEntryStore(\n options: RedisConsoleEntryStoreOptions,\n): ConsoleEntryStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis console entry store: client.eval non configurato.');\n }\n\n const key = deriveEncryptionKey(options.encryptionKey);\n const keyPrefix = (options.keyPrefix ?? 'accessi:console-entry:').replace(/:+$/, ':');\n const maxAttempts = Math.max(1, options.maxIssueAttempts ?? 5);\n\n return {\n async issue(record, ttlMs) {\n const safeTtl = normalizeTtl(ttlMs);\n const value = encryptRecord(record, key);\n\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const ticket = createTicket();\n const result = await options.client.eval(\n SET_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n value,\n safeTtl,\n );\n if (result) {\n return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };\n }\n }\n\n throw new Error('Impossibile emettere un ticket console univoco.');\n },\n\n async consume(ticket) {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const value = await options.client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n );\n if (typeof value !== 'string') return null;\n return decryptRecord(value, key);\n },\n\n async healthCheck() {\n try {\n const result = await options.client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"consoleEntry.js","sourceRoot":"","sources":["../../../src/accessi-module/security/consoleEntry.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';\nimport { instrumentAccessiRedisClient } from '../observability/redisMetrics';\n\n/**\n * Ticket di ingresso alla console, monouso e a breve scadenza.\n *\n * Serve a passare una sessione gia autenticata (frontend che possiede un JWT Accessi)\n * alla console amministrativa senza reinserire le credenziali e senza mettere il JWT\n * nell'URL: il frontend scambia il proprio JWT con un ticket opaco, apre la console\n * con `#entry=<ticket>` e la console lo scambia con il token originale.\n *\n * Il ticket e valido una sola volta, per pochi secondi, ed e legato all'utente che\n * lo ha richiesto (che deve avere accesso alla console). Lo store di default e in\n * memoria; in multi-istanza usare `createRedisConsoleEntryStore`.\n */\n\nexport interface ConsoleEntryRecord {\n token: string;\n codiceUtente: number;\n}\n\nexport interface ConsoleEntryTicket {\n ticket: string;\n expiresInSeconds: number;\n}\n\n/** Store pluggable: memoria per single instance, Redis per multi-istanza. */\nexport interface ConsoleEntryStore {\n issue(record: ConsoleEntryRecord, ttlMs: number): Promise<ConsoleEntryTicket>;\n consume(ticket: string): Promise<ConsoleEntryRecord | null>;\n /** Health check opzionale per readiness del servizio. */\n healthCheck?(): Promise<boolean>;\n}\n\nconst DEFAULT_TTL_MS = 60_000;\nconst DEFAULT_MAX_ENTRIES = 5000;\nconst TICKET_PATTERN = /^[a-f0-9]{64}$/i;\n\ninterface StoredEntry extends ConsoleEntryRecord {\n expiresAt: number;\n}\n\n/** Store in memoria con API sincrona per retrocompatibilita e API async per l'interfaccia. */\nexport interface MemoryConsoleEntryStore extends ConsoleEntryStore {\n issueSync(record: ConsoleEntryRecord, ttlMs?: number): ConsoleEntryTicket;\n consumeSync(ticket: string): ConsoleEntryRecord | null;\n clear(): void;\n}\n\nfunction createTicket(): string {\n return randomBytes(32).toString('hex');\n}\n\nfunction normalizeTtl(ttlMs: number): number {\n return Math.max(1, Math.trunc(ttlMs));\n}\n\n/** Store in memoria: adatto a sviluppo e a una singola istanza. */\nexport function createMemoryConsoleEntryStore(\n maxEntries: number = DEFAULT_MAX_ENTRIES,\n): MemoryConsoleEntryStore {\n const entries = new Map<string, StoredEntry>();\n\n const sweep = (now: number): void => {\n for (const [ticket, record] of entries) {\n if (record.expiresAt <= now) entries.delete(ticket);\n }\n };\n\n const issueSync = (record: ConsoleEntryRecord, ttlMs: number = DEFAULT_TTL_MS): ConsoleEntryTicket => {\n // Il TTL negativo resta supportato per test e per il comportamento storico:\n // il ticket e immediatamente scaduto.\n const ttl = Math.trunc(ttlMs);\n const now = Date.now();\n sweep(now);\n\n while (entries.size >= maxEntries) {\n const oldest = entries.keys().next().value;\n if (oldest === undefined) break;\n entries.delete(oldest);\n }\n\n const ticket = createTicket();\n entries.set(ticket, { ...record, expiresAt: now + ttl });\n return { ticket, expiresInSeconds: Math.max(0, Math.ceil(ttl / 1000)) };\n };\n\n const consumeSync = (ticket: string): ConsoleEntryRecord | null => {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const record = entries.get(ticket);\n if (!record) return null;\n entries.delete(ticket);\n if (record.expiresAt <= Date.now()) return null;\n return { token: record.token, codiceUtente: record.codiceUtente };\n };\n\n return {\n issueSync,\n consumeSync,\n clear: () => entries.clear(),\n async issue(record, ttlMs) {\n return issueSync(record, ttlMs);\n },\n async consume(ticket) {\n return consumeSync(ticket);\n },\n async healthCheck() {\n return true;\n },\n };\n}\n\nconst defaultStore = createMemoryConsoleEntryStore();\n\n/** Store di default in memoria, usato quando `AccessiOptions.consoleEntry.store` non e configurato. */\nexport function getDefaultConsoleEntryStore(): ConsoleEntryStore {\n return defaultStore;\n}\n\n// Funzioni storiche sincrone: mantengono il comportamento per test e consumer esistenti.\nexport function issueConsoleEntryTicket(\n record: ConsoleEntryRecord,\n ttlMs: number = DEFAULT_TTL_MS,\n): ConsoleEntryTicket {\n return defaultStore.issueSync(record, ttlMs);\n}\n\nexport function consumeConsoleEntryTicket(ticket: unknown): ConsoleEntryRecord | null {\n return typeof ticket === 'string' ? defaultStore.consumeSync(ticket) : null;\n}\n\n/** Solo per i test: azzera lo store in memoria di default. */\nexport function clearConsoleEntryTickets(): void {\n defaultStore.clear();\n}\n\n/** Client Redis minimale: compatibile con ioredis (`redis.eval(script, numKeys, ...args)`). */\nexport interface ConsoleEntryRedisClient {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisConsoleEntryStoreOptions {\n client: ConsoleEntryRedisClient;\n /** Segreto per cifrare il JWT nel Redis. Non usare il JWT in chiaro. */\n encryptionKey: string | Buffer;\n /** Default: `accessi:console-entry:`. */\n keyPrefix?: string;\n /** Tentativi in caso di collisione del ticket. Default: 5. */\n maxIssueAttempts?: number;\n}\n\nconst SET_SCRIPT = \"return redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2], 'NX')\";\nconst CONSUME_SCRIPT =\n \"local value = redis.call('GET', KEYS[1]) if value then redis.call('DEL', KEYS[1]) end return value\";\n\nfunction deriveEncryptionKey(secret: string | Buffer): Buffer {\n const source = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, 'utf8');\n if (source.length === 0) {\n throw new Error('Redis console entry encryptionKey non configurata.');\n }\n return source.length === 32 ? source : createHash('sha256').update(source).digest();\n}\n\nfunction encryptRecord(record: ConsoleEntryRecord, key: Buffer): string {\n const iv = randomBytes(12);\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const ciphertext = Buffer.concat([\n cipher.update(JSON.stringify(record), 'utf8'),\n cipher.final(),\n ]);\n const tag = cipher.getAuthTag();\n return [iv, tag, ciphertext].map((part) => part.toString('base64url')).join('.');\n}\n\nfunction decryptRecord(value: string, key: Buffer): ConsoleEntryRecord | null {\n const [ivPart, tagPart, ciphertextPart] = value.split('.');\n if (!ivPart || !tagPart || !ciphertextPart) return null;\n try {\n const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(ivPart, 'base64url'));\n decipher.setAuthTag(Buffer.from(tagPart, 'base64url'));\n const plaintext = Buffer.concat([\n decipher.update(Buffer.from(ciphertextPart, 'base64url')),\n decipher.final(),\n ]).toString('utf8');\n const parsed: unknown = JSON.parse(plaintext);\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n typeof (parsed as ConsoleEntryRecord).token !== 'string' ||\n typeof (parsed as ConsoleEntryRecord).codiceUtente !== 'number'\n ) {\n return null;\n }\n return parsed as ConsoleEntryRecord;\n } catch {\n return null;\n }\n}\n\n/**\n * Store Redis per ticket console.\n *\n * - il JWT viene cifrato con AES-256-GCM prima di essere scritto;\n * - `SET ... PX ... NX` evita collisioni;\n * - il consume usa `GET` + `DEL` atomici in Lua, quindi il ticket e monouso\n * anche con piu istanze.\n */\nexport function createRedisConsoleEntryStore(\n options: RedisConsoleEntryStoreOptions,\n): ConsoleEntryStore {\n if (!options?.client || typeof options.client.eval !== 'function') {\n throw new Error('Redis console entry store: client.eval non configurato.');\n }\n\n const key = deriveEncryptionKey(options.encryptionKey);\n const keyPrefix = (options.keyPrefix ?? 'accessi:console-entry:').replace(/:+$/, ':');\n const maxAttempts = Math.max(1, options.maxIssueAttempts ?? 5);\n const client = instrumentAccessiRedisClient('console_entry', options.client);\n\n return {\n async issue(record, ttlMs) {\n const safeTtl = normalizeTtl(ttlMs);\n const value = encryptRecord(record, key);\n\n for (let attempt = 0; attempt < maxAttempts; attempt += 1) {\n const ticket = createTicket();\n const result = await client.eval(\n SET_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n value,\n safeTtl,\n );\n if (result) {\n return { ticket, expiresInSeconds: Math.ceil(safeTtl / 1000) };\n }\n }\n\n throw new Error('Impossibile emettere un ticket console univoco.');\n },\n\n async consume(ticket) {\n if (typeof ticket !== 'string' || !TICKET_PATTERN.test(ticket)) {\n return null;\n }\n const value = await client.eval(\n CONSUME_SCRIPT,\n 1,\n `${keyPrefix}${ticket}`,\n );\n if (typeof value !== 'string') return null;\n return decryptRecord(value, key);\n },\n\n async healthCheck() {\n try {\n const result = await client.eval(\"return redis.call('PING')\", 0);\n return String(result).toUpperCase() === 'PONG';\n } catch {\n return false;\n }\n },\n };\n}\n"]}
|
|
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.createAccessiEdgeRateLimitMiddleware = createAccessiEdgeRateLimitMiddleware;
|
|
13
13
|
const audit_1 = require("./audit");
|
|
14
14
|
const rateLimitStore_1 = require("./rateLimitStore");
|
|
15
|
+
const metrics_1 = require("../observability/metrics");
|
|
15
16
|
const DEFAULT_WINDOW_MS = 60000;
|
|
16
17
|
const DEFAULT_MAX_ATTEMPTS = 300;
|
|
17
18
|
function defaultMatch(path) {
|
|
@@ -43,6 +44,7 @@ function createAccessiEdgeRateLimitMiddleware(options = {}) {
|
|
|
43
44
|
try {
|
|
44
45
|
const decision = yield store.consume(`${keyPrefix}|ip|${ip}`, windowMs, maxAttempts);
|
|
45
46
|
if (!decision.allowed) {
|
|
47
|
+
(0, metrics_1.recordAccessiMetric)('accessi_rate_limit_blocked_total', 1, { layer: 'edge' });
|
|
46
48
|
if (options.audit !== false) {
|
|
47
49
|
(0, audit_1.auditAccessi)('edge_rate_limited', {
|
|
48
50
|
ip,
|
|
@@ -64,6 +66,7 @@ function createAccessiEdgeRateLimitMiddleware(options = {}) {
|
|
|
64
66
|
next();
|
|
65
67
|
}
|
|
66
68
|
catch (_d) {
|
|
69
|
+
(0, metrics_1.recordAccessiMetric)('accessi_store_errors_total', 1, { store: 'edge_rate_limit' });
|
|
67
70
|
if (options.audit !== false) {
|
|
68
71
|
(0, audit_1.auditAccessi)('rate_limit_unavailable', { ip, path: req.path });
|
|
69
72
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edgeRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/edgeRateLimit.ts"],"names":[],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"edgeRateLimit.js","sourceRoot":"","sources":["../../../src/accessi-module/security/edgeRateLimit.ts"],"names":[],"mappings":"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","sourcesContent":["import type { NextFunction, Request, Response } from 'express';\nimport { auditAccessi } from './audit';\nimport { getDefaultRateLimitStore } from './rateLimitStore';\nimport type { RateLimitStore } from './rateLimitStore';\nimport { recordAccessiMetric } from '../observability/metrics';\n\n/** Opzioni del limite grossolano per IP applicato davanti alle rotte Accessi. */\nexport interface AccessiEdgeRateLimitOptions {\n /** Default: true quando l'oggetto viene configurato. */\n enabled?: boolean;\n /** Finestra del limite. Default: 60000 ms. */\n windowMs?: number;\n /** Richieste consentite per IP nella finestra. Default: 300. */\n maxAttempts?: number;\n /** Store condiviso. Default: memoria. */\n store?: RateLimitStore;\n /** Prefisso delle chiavi. Default: `edge`. */\n keyPrefix?: string;\n /** Audit strutturato degli eventi di blocco. Default: true. */\n audit?: boolean;\n /**\n * Decide se applicare il limite alla richiesta.\n *\n * Default: `/api` e `/api/*`. Il bootstrap Accessi passa un matcher che include\n * anche OpenAPI e JWKS.\n */\n match?: (path: string, req: Request) => boolean;\n}\n\nexport type AccessiEdgeRateLimitMiddleware = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => Promise<void>;\n\nconst DEFAULT_WINDOW_MS = 60_000;\nconst DEFAULT_MAX_ATTEMPTS = 300;\n\nfunction defaultMatch(path: string): boolean {\n return path === '/api' || path.startsWith('/api/');\n}\n\n/**\n * Middleware edge per Accessi Service.\n *\n * Non sostituisce il rate limit applicativo per account: aggiunge una soglia\n * grossolana per IP, utile contro bot e floods prima che la richiesta raggiunga\n * controller, database o introspection.\n */\nexport function createAccessiEdgeRateLimitMiddleware(\n options: AccessiEdgeRateLimitOptions = {},\n): AccessiEdgeRateLimitMiddleware | undefined {\n if (options.enabled === false) return undefined;\n\n const windowMs = Math.max(1, Math.trunc(options.windowMs ?? DEFAULT_WINDOW_MS));\n const maxAttempts = Math.max(1, Math.trunc(options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS));\n const store = options.store ?? getDefaultRateLimitStore();\n const keyPrefix = (options.keyPrefix ?? 'edge').replace(/\\|+$/, '');\n const match = options.match ?? defaultMatch;\n\n return async (req: Request, res: Response, next: NextFunction): Promise<void> => {\n if (!match(req.path, req)) {\n next();\n return;\n }\n\n const ip = req.ip ?? req.socket?.remoteAddress ?? 'unknown';\n try {\n const decision = await store.consume(\n `${keyPrefix}|ip|${ip}`,\n windowMs,\n maxAttempts,\n );\n if (!decision.allowed) {\n recordAccessiMetric('accessi_rate_limit_blocked_total', 1, { layer: 'edge' });\n if (options.audit !== false) {\n auditAccessi('edge_rate_limited', {\n ip,\n path: req.path,\n retryAfterSeconds: decision.retryAfterSeconds,\n });\n }\n res.setHeader('Retry-After', String(decision.retryAfterSeconds));\n res.status(429).json({\n severity: 'error',\n status: 429,\n statusCode: 2,\n code: 'ACCESSI_EDGE_RATE_LIMITED',\n error: 'ACCESSI_EDGE_RATE_LIMITED',\n message: 'Troppe richieste. Riprova piu tardi.',\n });\n return;\n }\n next();\n } catch {\n recordAccessiMetric('accessi_store_errors_total', 1, { store: 'edge_rate_limit' });\n if (options.audit !== false) {\n auditAccessi('rate_limit_unavailable', { ip, path: req.path });\n }\n res.status(503).json({\n severity: 'error',\n status: 503,\n statusCode: 2,\n code: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n error: 'ACCESSI_RATE_LIMIT_UNAVAILABLE',\n message: 'Rate limit non disponibile. Riprova piu tardi.',\n });\n }\n };\n}\n"]}
|
|
@@ -5,6 +5,7 @@ exports.buildAccessiJwks = buildAccessiJwks;
|
|
|
5
5
|
exports.createAccessiJwksHandler = createAccessiJwksHandler;
|
|
6
6
|
exports.setupAccessiJwks = setupAccessiJwks;
|
|
7
7
|
const crypto_1 = require("crypto");
|
|
8
|
+
const metrics_1 = require("../observability/metrics");
|
|
8
9
|
const DEFAULT_JWKS_PATH = "accessi/.well-known/jwks.json";
|
|
9
10
|
function normalizePath(value) {
|
|
10
11
|
return `/${value.replace(/^\/+|\/+$/g, "")}`;
|
|
@@ -59,6 +60,7 @@ function createAccessiJwksHandler(options) {
|
|
|
59
60
|
return (_req, res) => {
|
|
60
61
|
const document = buildAccessiJwks(options);
|
|
61
62
|
if (!document) {
|
|
63
|
+
(0, metrics_1.recordAccessiMetric)('accessi_jwks_requests_total', 1, { result: 'unavailable' });
|
|
62
64
|
return res.status(404).json({
|
|
63
65
|
severity: "error",
|
|
64
66
|
status: 404,
|
|
@@ -70,6 +72,7 @@ function createAccessiJwksHandler(options) {
|
|
|
70
72
|
}
|
|
71
73
|
res.setHeader("Content-Type", "application/json");
|
|
72
74
|
res.setHeader("Cache-Control", "public, max-age=300");
|
|
75
|
+
(0, metrics_1.recordAccessiMetric)('accessi_jwks_requests_total', 1, { result: 'ok' });
|
|
73
76
|
return res.status(200).send(document);
|
|
74
77
|
};
|
|
75
78
|
}
|