@csrf-armor/express
Version:
Express.js adapter for CSRF Armor - Advanced CSRF protection for Express.js applications
1 lines • 9.36 kB
Source Map (JSON)
{"version":3,"file":"adapter.mjs","names":[],"sources":["../src/adapter.ts"],"sourcesContent":["import type {\n CookieOptions,\n CsrfAdapter,\n CsrfRequest,\n CsrfResponse,\n RequiredCsrfConfig,\n} from '@csrf-armor/core';\nimport type express from 'express';\n\n/**\n * Express.js adapter for CSRF protection.\n *\n * This adapter implements the CsrfAdapter interface to provide CSRF protection\n * for Express.js applications. It handles request/response transformation,\n * token extraction from various sources, and cookie/header management.\n *\n * @example\n * ```typescript\n * import { CsrfProtection } from '@csrf-armor/core';\n * import { ExpressAdapter } from '@csrf-armor/express';\n *\n * const csrf = new CsrfProtection(new ExpressAdapter(), {\n * secret: 'your-secret-key',\n * strategy: 'signed-double-submit'\n * });\n * ```\n */\nexport class ExpressAdapter\n implements CsrfAdapter<express.Request, express.Response>\n{\n /**\n * Extracts CSRF-relevant data from an Express.js request.\n *\n * Converts an Express Request object into a standardized CsrfRequest format\n * that can be processed by the core CSRF protection logic. Handles header\n * normalization, cookie extraction, and body access.\n *\n * @param req - Express.js request object\n * @returns Normalized CSRF request object\n *\n * @example\n * ```typescript\n * const adapter = new ExpressAdapter();\n * const csrfRequest = adapter.extractRequest(req);\n * // csrfRequest contains normalized headers, cookies, and body\n * ```\n */\n extractRequest(req: express.Request): CsrfRequest {\n // Create a Map for headers with proper type handling\n const headers = new Map<string, string>();\n\n // Safely process headers, handling string, string[], and undefined values\n if (req.headers) {\n Object.entries(req.headers).forEach(([key, value]) => {\n if (Array.isArray(value)) {\n // Join array values into a single string\n headers.set(key.toLowerCase(), value.join(', '));\n } else if (value !== undefined) {\n // Set string values directly\n headers.set(key.toLowerCase(), value);\n }\n // Skip undefined values\n });\n }\n\n return {\n method: req.method,\n url: req.url,\n headers,\n cookies: new Map(\n Object.entries(req.cookies ?? {}).map(([key, value]) => [key.toLowerCase(), String(value)])\n ),\n body: req.body,\n };\n }\n\n /**\n * Sets a cookie on the Express.js response with CSRF protection options.\n *\n * Converts CSRF armor cookie options to Express.js cookie format,\n * including proper maxAge conversion (seconds to milliseconds).\n *\n * @param res - Express.js response object\n * @param name - Cookie name\n * @param value - Cookie value\n * @param options - CSRF cookie options\n *\n * @private\n */\n private setCookie(\n res: express.Response,\n name: string,\n value: string,\n options?: CookieOptions\n ): void {\n res.cookie(name, value, {\n secure: options?.secure,\n httpOnly: options?.httpOnly,\n sameSite: options?.sameSite,\n path: options?.path,\n domain: options?.domain,\n maxAge: options?.maxAge ? options.maxAge * 1000 : undefined, // Express expects milliseconds\n });\n }\n\n /**\n * Sets HTTP headers on the Express.js response.\n *\n * Handles both Map and object-based header formats from the CSRF response.\n *\n * @param res - Express.js response object\n * @param headers - Headers to set (Map or object format)\n *\n * @private\n */\n private setHeaders(\n res: express.Response,\n headers: CsrfResponse['headers']\n ): void {\n const entries =\n headers instanceof Map ? headers : Object.entries(headers || {});\n for (const [key, value] of entries) {\n res.setHeader(key, value);\n }\n }\n\n /**\n * Applies CSRF protection data to an Express.js response.\n *\n * Sets headers and cookies on the Express response based on the CSRF\n * protection results. This includes CSRF tokens, strategy hints, and\n * security cookies.\n *\n * @param res - Express.js response object to modify\n * @param csrfResponse - CSRF response data containing headers and cookies\n * @returns The modified Express.js response object\n *\n * @example\n * ```typescript\n * const adapter = new ExpressAdapter();\n * const modifiedResponse = adapter.applyResponse(res, {\n * headers: new Map([['x-csrf-token', 'abc123']]),\n * cookies: new Map([['csrf-token', { value: 'def456', options: { httpOnly: true } }]])\n * });\n * ```\n */\n applyResponse(\n res: express.Response,\n csrfResponse: CsrfResponse\n ): express.Response {\n this.setHeaders(res, csrfResponse.headers);\n\n if (csrfResponse.cookies) {\n const entries =\n csrfResponse.cookies instanceof Map\n ? Array.from(csrfResponse.cookies.entries())\n : Object.entries(csrfResponse.cookies);\n\n for (const [name, { value, options }] of entries) {\n this.setCookie(res, name, value, options);\n }\n }\n\n return res;\n }\n\n /**\n * Extracts CSRF token from various locations in an Express.js request.\n *\n * Attempts to find the CSRF token in the following order:\n * 1. HTTP headers (e.g., X-CSRF-Token)\n * 2. URL query parameters\n * 3. Request body (form data or JSON)\n *\n * This method handles relative URLs safely by providing a base URL for parsing.\n *\n * @param request - Normalized CSRF request object\n * @param config - CSRF configuration containing token field names\n * @returns The extracted token string, or undefined if not found\n *\n * @example\n * ```typescript\n * const adapter = new ExpressAdapter();\n * const token = await adapter.getTokenFromRequest(csrfRequest, {\n * token: { headerName: 'X-CSRF-Token', fieldName: 'csrf_token' }\n * });\n * // Returns token from header, query param, or body\n * ```\n */\n async getTokenFromRequest(\n request: CsrfRequest,\n config: RequiredCsrfConfig\n ): Promise<string | undefined> {\n const headers =\n request.headers instanceof Map\n ? request.headers\n : new Map(Object.entries(request.headers));\n\n // Try header first (most common for APIs)\n const headerValue = headers.get(config.token.headerName.toLowerCase());\n if (headerValue) return headerValue;\n\n // Try cookie\n const cookies =\n request.cookies instanceof Map\n ? request.cookies\n : new Map(\n Object.entries(request.cookies || {}).map(([key, value]) => [key.toLowerCase(), String(value)])\n );\n const cookieValue = cookies.get(config.cookie.name.toLowerCase());\n if (cookieValue) return cookieValue;\n\n // Try query parameter\n if (request.url) {\n try {\n // Express request.url is a relative path, so we need to create a full URL\n const url = new URL(request.url, 'http://localhost');\n const queryValue = url.searchParams.get(config.token.fieldName);\n if (queryValue) return queryValue;\n } catch {\n // If URL parsing fails, skip query parameter extraction\n }\n }\n\n // Try form body\n if (request.body && typeof request.body === 'object') {\n const body = request.body as Record<string, unknown>;\n const formValue = body[config.token.fieldName];\n if (typeof formValue === 'string') return formValue;\n }\n\n return undefined;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA2BA,IAAa,iBAAb,MAEA;;;;;;;;;;;;;;;;;;CAkBE,eAAe,KAAmC;EAEhD,MAAM,0BAAU,IAAI,KAAqB;AAGzC,MAAI,IAAI,QACN,QAAO,QAAQ,IAAI,QAAQ,CAAC,SAAS,CAAC,KAAK,WAAW;AACpD,OAAI,MAAM,QAAQ,MAAM,CAEtB,SAAQ,IAAI,IAAI,aAAa,EAAE,MAAM,KAAK,KAAK,CAAC;YACvC,UAAU,OAEnB,SAAQ,IAAI,IAAI,aAAa,EAAE,MAAM;IAGvC;AAGJ,SAAO;GACL,QAAQ,IAAI;GACZ,KAAK,IAAI;GACT;GACA,SAAS,IAAI,IACX,OAAO,QAAQ,IAAI,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,IAAI,aAAa,EAAE,OAAO,MAAM,CAAC,CAAC,CAC5F;GACD,MAAM,IAAI;GACX;;;;;;;;;;;;;;;CAgBH,AAAQ,UACN,KACA,MACA,OACA,SACM;AACN,MAAI,OAAO,MAAM,OAAO;GACtB,QAAQ,SAAS;GACjB,UAAU,SAAS;GACnB,UAAU,SAAS;GACnB,MAAM,SAAS;GACf,QAAQ,SAAS;GACjB,QAAQ,SAAS,SAAS,QAAQ,SAAS,MAAO;GACnD,CAAC;;;;;;;;;;;;CAaJ,AAAQ,WACN,KACA,SACM;EACN,MAAM,UACJ,mBAAmB,MAAM,UAAU,OAAO,QAAQ,WAAW,EAAE,CAAC;AAClE,OAAK,MAAM,CAAC,KAAK,UAAU,QACzB,KAAI,UAAU,KAAK,MAAM;;;;;;;;;;;;;;;;;;;;;;CAwB7B,cACE,KACA,cACkB;AAClB,OAAK,WAAW,KAAK,aAAa,QAAQ;AAE1C,MAAI,aAAa,SAAS;GACxB,MAAM,UACJ,aAAa,mBAAmB,MAC5B,MAAM,KAAK,aAAa,QAAQ,SAAS,CAAC,GAC1C,OAAO,QAAQ,aAAa,QAAQ;AAE1C,QAAK,MAAM,CAAC,MAAM,EAAE,OAAO,cAAc,QACvC,MAAK,UAAU,KAAK,MAAM,OAAO,QAAQ;;AAI7C,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;CA0BT,MAAM,oBACJ,SACA,QAC6B;EAO7B,MAAM,eALJ,QAAQ,mBAAmB,MACvB,QAAQ,UACR,IAAI,IAAI,OAAO,QAAQ,QAAQ,QAAQ,CAAC,EAGlB,IAAI,OAAO,MAAM,WAAW,aAAa,CAAC;AACtE,MAAI,YAAa,QAAO;EASxB,MAAM,eALJ,QAAQ,mBAAmB,MACvB,QAAQ,UACR,IAAI,IACF,OAAO,QAAQ,QAAQ,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,IAAI,aAAa,EAAE,OAAO,MAAM,CAAC,CAAC,CAChG,EACqB,IAAI,OAAO,OAAO,KAAK,aAAa,CAAC;AACjE,MAAI,YAAa,QAAO;AAGxB,MAAI,QAAQ,IACV,KAAI;GAGF,MAAM,aADM,IAAI,IAAI,QAAQ,KAAK,mBAAmB,CAC7B,aAAa,IAAI,OAAO,MAAM,UAAU;AAC/D,OAAI,WAAY,QAAO;UACjB;AAMV,MAAI,QAAQ,QAAQ,OAAO,QAAQ,SAAS,UAAU;GAEpD,MAAM,YADO,QAAQ,KACE,OAAO,MAAM;AACpC,OAAI,OAAO,cAAc,SAAU,QAAO"}