import { Request, Response } from 'express'; import * as crypto from 'crypto'; import { DyFM_Error } from '@futdevpro/fsm-dynamo'; import { DyNTS_SingletonServiceBase } from '../../_services/base/singleton.service-base'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; import { DyNTS_AdminApiKey_Config } from './_models/admin-api-key-config.interface'; /** Default env var nev az admin API key-hez. */ const DEFAULT_ENV_VAR_NAME: string = 'DYNTS_ADMIN_API_KEY'; /** Default header nev (Express lowercase-re normalizalja az osszes header-t). */ const DEFAULT_HEADER_NAME: string = 'x-admin-api-key'; /** Default Bearer fallback engedelyezve van. */ const DEFAULT_ALLOW_BEARER: boolean = true; /** Service-nev az error-okhoz. */ const SERVICE_NAME: string = 'DyNTS_AdminApiKey_AuthService'; /** ErrorCode prefix — system shortcode + saját kod. */ const buildErrorCode = (subcode: string): string => { const sys: string = DyNTS_global_settings.systemShortCodeName ?? 'DyNTS'; return `${sys}|DyNTS-AAK-${subcode}`; }; /** * Admin API key auth service — opt-in HTTP guard a meglevo `DyNTS_Endpoint_Params.preProcesses` * mechanizmushoz. Egy env var-ban tarolt fix kulccsal valid-alja a bejovo kerest. * * **Hasznalat (host app):** * ```ts * const adminAuth = DyNTS_AdminApiKey_AuthService.getInstance(); * // opcionalis konfig: * // adminAuth.configure({ envVarName: 'MY_KEY', headerName: 'x-my-key' }); * * new DyNTS_Endpoint_Params({ * ..., * preProcesses: [adminAuth.verify, ...other], * }); * * // vagy a logs routing module-on at * DyNTS_getLogsRoutingModule({ authPreProcess: adminAuth.verify }); * ``` * * **Viselkedes:** * - env var beallitva ES helyes header → silent pass * - env var beallitva, header hianyzik / rossz → 401 DyFM_Error * - env var NINCS beallitva → 500 DyFM_Error (fail-closed; NEM silent allow) * * **Header lookup:** * 1. `x-admin-api-key` (default canonical header) * 2. `Authorization: Bearer ` (fallback ha `allowAuthorizationBearer === true`) * * **Timing-safe:** `crypto.timingSafeEqual` Buffer-konvertalassal. Length-mismatch * eseten dummy compare-rel azonos idő, hogy a kulcs-hossz ne szivarogjon ki. * * **Env var read-on-each-call:** a `verify()` minden hivasnal olvassa az env-et, * nem cache-eli. Igy a host az env-et utolagosan is allithatja (pl. config * loader az auth.service.install() utan). * * **Singleton:** `getInstance()`-szel hivd. A `.verify` mezo binding-elve van * `this`-re, igy direkt atadhato `preProcesses`-be ujracsomagolas nelkul. */ export class DyNTS_AdminApiKey_AuthService extends DyNTS_SingletonServiceBase { static getInstance(): DyNTS_AdminApiKey_AuthService { return DyNTS_AdminApiKey_AuthService.getSingletonInstance() as DyNTS_AdminApiKey_AuthService; } private envVarName: string = DEFAULT_ENV_VAR_NAME; private headerName: string = DEFAULT_HEADER_NAME; private allowAuthorizationBearer: boolean = DEFAULT_ALLOW_BEARER; /** * Konfig override. Hianyzo mezok a default-okat orzik. * Hivhato barmikor — a `verify()` a friss config-ot olvassa. */ configure(config: DyNTS_AdminApiKey_Config): void { if (config.envVarName !== undefined) { this.envVarName = config.envVarName; } if (config.headerName !== undefined) { // Express lowercase-re normalizal — itt is lowercase-eljuk a konzisztenciaert this.headerName = config.headerName.toLowerCase(); } if (config.allowAuthorizationBearer !== undefined) { this.allowAuthorizationBearer = config.allowAuthorizationBearer; } } /** * Aktualis konfig olvasasa (test/diagnosztika celokra). */ getConfig(): Required { return { envVarName: this.envVarName, headerName: this.headerName, allowAuthorizationBearer: this.allowAuthorizationBearer, }; } /** * Pre-process function — atadhato `DyNTS_Endpoint_Params.preProcesses`-be, * vagy `DyNTS_getLogsRoutingModule({ authPreProcess: ... })`-be. * * Throws: * - 500 ha az env var nincs beallitva (vagy ures string) * - 401 ha a header hianyzik vagy nem egyezik * * A `req`/`res` parametereket NEM modositja (a kerest tovabb engedi a tovabbi * preProcess-eknek; csak hiba eseten throw-ol). */ readonly verify = async (req: Request, _res: Response): Promise => { const expectedKey: string = process.env[this.envVarName] ?? ''; if (expectedKey.length === 0) { throw new DyFM_Error({ status: 500, errorCode: buildErrorCode('CONFIG'), addECToUserMsg: true, message: `Admin API key not configured: env var ${this.envVarName} is not set or empty`, userMessage: 'Server configuration error', issuerService: SERVICE_NAME, }); } const providedKey: string | null = this.extractKeyFromRequest(req); if (providedKey === null) { throw new DyFM_Error({ status: 401, errorCode: buildErrorCode('MISSING'), addECToUserMsg: true, message: `Admin API key required (expected header: ${this.headerName})`, userMessage: 'Admin API key required', issuerService: SERVICE_NAME, }); } if (!this.timingSafeEquals(providedKey, expectedKey)) { throw new DyFM_Error({ status: 401, errorCode: buildErrorCode('INVALID'), addECToUserMsg: true, message: 'Admin API key invalid', userMessage: 'Admin API key invalid', issuerService: SERVICE_NAME, }); } // Silent pass — return resolved promise }; /** * Header lookup — elobb a primer header, aztan opcionalisan az Authorization Bearer. * Az ures string is "hianyzo"-nak szamit (a Buffer.from('') es timingSafeEqual * konzisztencia miatt). */ private extractKeyFromRequest(req: Request): string | null { // Primer header const primary: unknown = req.headers[this.headerName]; const primaryStr: string = Array.isArray(primary) ? primary[0] ?? '' : (typeof primary === 'string' ? primary : ''); if (primaryStr.length > 0) { return primaryStr; } // Authorization Bearer fallback if (this.allowAuthorizationBearer) { const authHeader: unknown = req.headers['authorization']; const authStr: string = Array.isArray(authHeader) ? authHeader[0] ?? '' : (typeof authHeader === 'string' ? authHeader : ''); if (authStr.toLowerCase().startsWith('bearer ')) { const token: string = authStr.substring(7).trim(); if (token.length > 0) { return token; } } } return null; } /** * Timing-safe compare ket string kozott. Length-mismatch eseten egy dummy * compare-rel azonos idot kenyszeritunk (a kulcs-hossz nem szivaroghat ki * timing-attackal). * * crypto.timingSafeEqual KOTELEZOEN azonos Buffer-hosszt var — kulonbozo * hosszra throw-ol, ezert vizsgaljuk elobb a length-et es csak utana * compare-elunk. */ private timingSafeEquals(a: string, b: string): boolean { const aBuf: Buffer = Buffer.from(a, 'utf-8'); const bBuf: Buffer = Buffer.from(b, 'utf-8'); if (aBuf.length !== bBuf.length) { // Dummy compare ugyanazzal a string-gel: konstans ideju mukodest biztosit // mielott visszaternenk false-szal — igy a length-mismatch nem szivaroghat ki. crypto.timingSafeEqual(bBuf, bBuf); return false; } return crypto.timingSafeEqual(aBuf, bBuf); } /** * Test-only: visszaallitja a default config-ot, hogy a specfajlok ne szivarogjak * at egymas state-jet. Production code NE hivja. */ _resetForTesting(): void { this.envVarName = DEFAULT_ENV_VAR_NAME; this.headerName = DEFAULT_HEADER_NAME; this.allowAuthorizationBearer = DEFAULT_ALLOW_BEARER; } }