UNPKG

@csrf-armor/core

Version:

Framework-agnostic CSRF protection core functionality

1 lines 24.8 kB
{"version":3,"file":"csrf.mjs","names":[],"sources":["../src/csrf.ts"],"sourcesContent":["import {\n CSRF_STRATEGY_HEADER,\n CSRF_TOKEN_HEADER,\n DEFAULT_CONFIG,\n DEFAULT_NONCE_LENGTH,\n ORIGIN_CHECK_NONCE_LENGTH,\n SAFE_METHODS,\n SERVER_CSRF_COOKIE_SUFFIX,\n} from './constants.js';\nimport {\n generateNonce,\n generateSecureSecret,\n generateSignedToken,\n parseSignedToken,\n signUnsignedToken,\n timingSafeEqual,\n verifySignedToken,\n} from './crypto.js';\nimport type {\n CsrfAdapter,\n CsrfConfig,\n CsrfRequest,\n CsrfResponse,\n RequiredCookieOptions,\n RequiredCsrfConfig,\n} from './types.js';\nimport { validateRequest } from './validation.js';\n\n/**\n * Extracts the pathname from a URL string for path-based exclusion matching.\n *\n * Handles both absolute URLs and relative paths safely. If URL parsing fails,\n * falls back to manual parsing by finding the query string delimiter.\n *\n * @param url - URL string to extract pathname from\n * @returns The pathname portion of the URL\n *\n * @internal\n */\nfunction extractPathname(url: string): string {\n try {\n // Always return the full pathname for accurate excludePaths matching\n return new URL(url).pathname;\n } catch {\n const questionMarkIndex = url.indexOf('?');\n if (questionMarkIndex !== -1) {\n return url.substring(0, questionMarkIndex);\n }\n return url;\n }\n}\n\n/**\n * Normalizes request headers to a consistent Map format.\n *\n * Converts various header formats (Map, Headers object, plain object) into\n * a standardized Map<string, string> for consistent processing throughout\n * the CSRF protection system.\n *\n * @param rawHeaders - Headers in various formats from the request\n * @returns Normalized headers as a Map\n *\n * @internal\n */\nfunction processHeaders(\n rawHeaders: CsrfRequest['headers']\n): Map<string, string> {\n if (rawHeaders instanceof Map) {\n return rawHeaders;\n }\n\n return new Map(Object.entries(rawHeaders));\n}\n\n/**\n * Merges user configuration with default CSRF configuration values.\n *\n * Creates a complete configuration object by combining user-provided options\n * with secure defaults. Ensures all required fields are present and properly\n * typed for the CSRF protection system.\n *\n * @param defaultConfig - Default configuration values\n * @param userConfig - User-provided configuration overrides\n * @returns Complete CSRF configuration with all required fields\n *\n * @internal\n */\nfunction mergeConfig(\n defaultConfig: CsrfConfig,\n userConfig?: CsrfConfig\n): RequiredCsrfConfig {\n const merged = {\n ...defaultConfig,\n ...userConfig,\n cookie: {\n ...defaultConfig.cookie,\n ...userConfig?.cookie,\n },\n token: {\n ...defaultConfig.token,\n ...userConfig?.token,\n },\n };\n\n // Ensure all required properties are present\n const config: RequiredCsrfConfig = {\n strategy: userConfig?.strategy ?? defaultConfig.strategy ?? 'hybrid',\n secret:\n userConfig?.secret ?? defaultConfig.secret ?? generateSecureSecret(),\n token: {\n expiry: merged.token?.expiry ?? 3600,\n headerName: merged.token?.headerName ?? 'X-CSRF-Token',\n fieldName: merged.token?.fieldName ?? 'csrf_token',\n reissueThreshold: merged.token?.reissueThreshold ?? 300,\n },\n cookie: {\n name: merged.cookie?.name ?? 'csrf-token',\n secure: merged.cookie?.secure ?? true,\n httpOnly: merged.cookie?.httpOnly ?? false,\n sameSite: merged.cookie?.sameSite ?? 'lax',\n path: merged.cookie?.path ?? '/',\n },\n allowedOrigins: merged.allowedOrigins ?? [],\n excludePaths: merged.excludePaths ?? [],\n skipContentTypes: merged.skipContentTypes ?? [],\n };\n\n // Add optional properties if they exist\n if (merged.cookie?.domain) {\n config.cookie.domain = merged.cookie.domain;\n }\n if (merged.cookie?.maxAge) {\n config.cookie.maxAge = merged.cookie.maxAge;\n }\n\n return config;\n}\n\n/**\n * Core CSRF protection engine that provides framework-agnostic CSRF security.\n *\n * This class implements multiple CSRF protection strategies and works with\n * framework-specific adapters to provide comprehensive protection against\n * Cross-Site Request Forgery attacks. It supports various strategies including\n * double-submit cookies, signed tokens, origin validation, and hybrid approaches.\n *\n * **Features:**\n * - Multiple CSRF protection strategies (double-submit, signed-double-submit, etc.)\n * - Framework-agnostic design with adapter pattern\n * - Configurable token expiration and validation\n * - Origin-based validation with whitelist support\n * - Path exclusion for public endpoints\n * - Content-type based skipping for certain request types\n * - Secure cryptographic operations using Web Crypto API\n * - Timing-safe token comparisons to prevent timing attacks\n *\n * **Available Strategies:**\n * - `double-submit`: Classic double-submit cookie pattern\n * - `signed-double-submit`: Enhanced double-submit with cryptographic signatures\n * - `signed-token`: Server-side token validation with signing\n * - `origin-check`: Validates request origin against allowed domains\n * - `hybrid`: Combines multiple strategies for maximum security\n *\n * @template TRequest - Framework-specific request type\n * @template TResponse - Framework-specific response type\n * @public\n *\n * @example\n * ```typescript\n * import { CsrfProtection } from '@csrf-armor/core';\n * import { ExpressAdapter } from '@csrf-armor/express';\n *\n * // Create CSRF protection with Express adapter\n * const csrf = new CsrfProtection(new ExpressAdapter(), {\n * strategy: 'signed-double-submit',\n * secret: 'your-secret-key',\n * token: {\n * expiry: 3600, // 1 hour\n * headerName: 'X-CSRF-Token',\n * fieldName: 'csrf_token'\n * },\n * cookie: {\n * name: 'csrf-token',\n * secure: true,\n * httpOnly: false,\n * sameSite: 'strict'\n * },\n * allowedOrigins: ['https://yourdomain.com'],\n * excludePaths: ['/api/public', '/health']\n * });\n *\n * // Use in middleware\n * app.use(async (req, res, next) => {\n * try {\n * const result = await csrf.protect(req, res);\n * if (result.success) {\n * req.csrfToken = result.token;\n * next();\n * } else {\n * res.status(403).json({ error: result.reason });\n * }\n * } catch (error) {\n * next(error);\n * }\n * });\n * ```\n * // Basic setup with Express\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-32-character-secret-key-here',\n * strategy: 'signed-double-submit',\n * allowedOrigins: ['https://yourdomain.com'],\n * excludePaths: ['/api/public']\n * });\n *\n * // In middleware\n * app.use(async (req, res, next) => {\n * const result = await csrf.protect(req, res);\n * if (!result.success) {\n * return res.status(403).json({ error: 'CSRF validation failed' });\n * }\n * next();\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Advanced configuration\n * const csrf = new CsrfProtection(adapter, {\n * strategy: 'hybrid',\n * secret: process.env.CSRF_SECRET,\n * token: {\n * expiry: 7200, // 2 hours\n * headerName: 'X-Custom-CSRF-Token',\n * fieldName: 'custom_csrf_token'\n * },\n * cookie: {\n * name: 'custom-csrf',\n * secure: true,\n * httpOnly: true,\n * sameSite: 'strict'\n * },\n * excludePaths: ['/health', '/api/webhook'],\n * skipContentTypes: ['application/json']\n * });\n * ```\n */\n\n/**\n * Interface for token data used throughout the CSRF protection process.\n * @internal\n */\ninterface TokenData {\n clientToken: string;\n cookieToken: string;\n serverCookieToken?: string;\n cookieOptions: RequiredCookieOptions;\n}\n\nexport class CsrfProtection<TRequest = unknown, TResponse = unknown> {\n private readonly config: RequiredCsrfConfig;\n private readonly adapter: CsrfAdapter<TRequest, TResponse>;\n\n /**\n * Creates a new CSRF protection instance.\n *\n * @param adapter - Framework-specific adapter for request/response handling\n * @param userConfig - Optional configuration overrides\n */\n constructor(\n adapter: CsrfAdapter<TRequest, TResponse>,\n userConfig?: CsrfConfig\n ) {\n this.adapter = adapter;\n this.config = mergeConfig(DEFAULT_CONFIG, userConfig);\n }\n\n /**\n * Checks if a request should be excluded from CSRF protection.\n *\n * @param request - The CSRF request to check\n * @returns true if the request should be skipped\n * @internal\n */\n private shouldSkipProtection(request: CsrfRequest): boolean {\n const pathname = extractPathname(request.url);\n if (this.config.excludePaths.some((path) => pathname.startsWith(path))) {\n return true;\n }\n\n const headers = processHeaders(request.headers);\n const contentType = headers.get('content-type') ?? '';\n return this.config.skipContentTypes.some((type) =>\n contentType.includes(type)\n );\n }\n\n /**\n * Attempts to reuse existing CSRF tokens if they are still valid.\n *\n * @param request - The CSRF request containing potential existing tokens\n * @returns Token data if reuse is possible, null otherwise\n * @internal\n */\n private async attemptTokenReuse(\n request: CsrfRequest\n ): Promise<TokenData | null> {\n if (!SAFE_METHODS.includes(request.method as never)) {\n return null;\n }\n\n const cookies =\n request.cookies instanceof Map\n ? request.cookies\n : new Map(Object.entries(request.cookies));\n\n const clientTokenFromRequest = cookies.get(this.config.cookie.name);\n const serverCookieTokenFromRequest = cookies.get(\n this.config.cookie.name + SERVER_CSRF_COOKIE_SUFFIX\n );\n\n if (!clientTokenFromRequest) {\n return null;\n }\n\n const currentTime = Math.floor(Date.now() / 1000);\n const reissueThreshold = this.config.token.reissueThreshold;\n\n try {\n switch (this.config.strategy) {\n case 'signed-token':\n case 'hybrid': {\n const payload = await parseSignedToken(\n clientTokenFromRequest,\n this.config.secret\n );\n if (payload.exp > currentTime + reissueThreshold) {\n return {\n clientToken: clientTokenFromRequest,\n cookieToken: clientTokenFromRequest,\n cookieOptions: { ...this.config.cookie, httpOnly: false },\n };\n }\n break;\n }\n case 'signed-double-submit': {\n if (serverCookieTokenFromRequest && clientTokenFromRequest) {\n try {\n const verifiedToken = await verifySignedToken(\n serverCookieTokenFromRequest,\n this.config.secret\n );\n if (timingSafeEqual(verifiedToken, clientTokenFromRequest)) {\n return {\n clientToken: clientTokenFromRequest,\n cookieToken: clientTokenFromRequest,\n serverCookieToken: serverCookieTokenFromRequest,\n cookieOptions: { ...this.config.cookie, httpOnly: false },\n };\n }\n } catch {\n // Invalid signature, fall through to generate new tokens\n }\n }\n break;\n }\n }\n } catch (error) {\n // Token invalid or expired, return null to generate new tokens\n return null;\n }\n\n return null;\n }\n\n /**\n * Builds the CSRF response with headers and cookies.\n *\n * @param tokenData - The token data to include in the response\n * @returns The CSRF response object\n * @internal\n */\n private buildCsrfResponse(tokenData: TokenData): CsrfResponse {\n const cookies = new Map([\n [\n this.config.cookie.name,\n {\n value: tokenData.cookieToken,\n options: tokenData.cookieOptions,\n },\n ],\n ]);\n\n if (tokenData.serverCookieToken) {\n cookies.set(`${this.config.cookie.name}-server`, {\n value: tokenData.serverCookieToken,\n options: {\n ...tokenData.cookieOptions,\n httpOnly: true,\n },\n });\n }\n\n return {\n headers: new Map([\n [CSRF_TOKEN_HEADER, tokenData.clientToken],\n [CSRF_STRATEGY_HEADER, this.config.strategy],\n ]),\n cookies,\n };\n }\n\n /**\n * Protects a request/response pair against CSRF attacks.\n *\n * This is the main method that applies CSRF protection to incoming requests.\n * It handles both token generation for safe methods (GET, HEAD, OPTIONS) and\n * token validation for state-changing methods (POST, PUT, DELETE, etc.).\n *\n * The method:\n * 1. Extracts request data using the framework adapter\n * 2. Checks if the request should be excluded or skipped\n * 3. For safe methods: generates and sets new CSRF tokens\n * 4. For unsafe methods: validates existing tokens using the configured strategy\n * 5. Applies response data (headers, cookies) using the adapter\n *\n * @param request - Framework-specific request object\n * @param response - Framework-specific response object\n * @returns Promise resolving to protection result with success status and modified response\n *\n * @example\n * ```typescript\n * // Basic usage in middleware\n * const result = await csrf.protect(req, res);\n * if (!result.success) {\n * return res.status(403).json({\n * error: 'CSRF validation failed',\n * reason: result.reason\n * });\n * }\n *\n * // Token is available for safe methods\n * if (result.token) {\n * console.log('Generated CSRF token:', result.token);\n * }\n *\n * // Continue with the modified response\n * return result.response;\n * ```\n *\n * @example\n * ```typescript\n * // Error handling with specific reasons\n * const result = await csrf.protect(req, res);\n * if (!result.success) {\n * switch (result.reason) {\n * case 'Invalid token':\n * return res.status(403).json({ error: 'CSRF token is invalid' });\n * case 'Token expired':\n * return res.status(403).json({ error: 'CSRF token has expired' });\n * case 'Origin mismatch':\n * return res.status(403).json({ error: 'Request origin not allowed' });\n * default:\n * return res.status(403).json({ error: 'CSRF validation failed' });\n * }\n * }\n * ```\n */\n async protect(\n request: TRequest,\n response: TResponse\n ): Promise<{\n success: boolean;\n response: TResponse;\n token?: string;\n reason?: string;\n }> {\n const csrfRequest = this.adapter.extractRequest(request);\n\n // Check if request should be skipped\n if (this.shouldSkipProtection(csrfRequest)) {\n return { success: true, response };\n }\n\n // Attempt to reuse existing tokens or generate new ones\n let tokenData = await this.attemptTokenReuse(csrfRequest);\n tokenData ??= await this.generateTokensForStrategy();\n\n // Build CSRF response\n const csrfResponse = this.buildCsrfResponse(tokenData);\n\n // Apply response modifications\n const modifiedResponse = this.adapter.applyResponse(response, csrfResponse);\n\n // Skip validation for safe methods\n if (\n SAFE_METHODS.includes(csrfRequest.method as (typeof SAFE_METHODS)[number])\n ) {\n return {\n success: true,\n response: modifiedResponse,\n token: tokenData.clientToken,\n };\n }\n\n // Validate based on strategy\n const validationResult = await validateRequest(\n csrfRequest,\n this.config,\n this.adapter.getTokenFromRequest\n );\n\n if (!validationResult.isValid) {\n return {\n success: false,\n response: modifiedResponse,\n reason: validationResult.reason ?? 'CSRF Validation failed',\n };\n }\n\n return {\n success: true,\n response: modifiedResponse,\n token: tokenData.clientToken,\n };\n }\n\n private async generateTokensForStrategy(): Promise<TokenData> {\n const baseOptions = this.config.cookie;\n\n switch (this.config.strategy) {\n case 'double-submit': {\n const token = generateNonce(DEFAULT_NONCE_LENGTH);\n if (!token) {\n throw new Error(\n 'CSRF Error: Failed to generate nonce for strategy \"double-submit\".'\n );\n }\n return {\n clientToken: token,\n cookieToken: token,\n cookieOptions: { ...baseOptions, httpOnly: false },\n };\n }\n\n case 'signed-double-submit': {\n const unsignedToken = generateNonce(DEFAULT_NONCE_LENGTH);\n if (!unsignedToken) {\n throw new Error(\n 'CSRF Error: Failed to generate nonce for strategy \"signed-double-submit\".'\n );\n }\n const signedToken = await signUnsignedToken(\n unsignedToken,\n this.config.secret\n );\n return {\n clientToken: unsignedToken,\n cookieToken: unsignedToken,\n serverCookieToken: signedToken,\n cookieOptions: { ...baseOptions, httpOnly: false },\n };\n }\n\n case 'signed-token':\n case 'hybrid': {\n const signedToken = await generateSignedToken(\n this.config.secret,\n this.config.token.expiry\n );\n return {\n clientToken: signedToken,\n cookieToken: signedToken,\n cookieOptions: { ...baseOptions, httpOnly: false },\n };\n }\n\n case 'origin-check': {\n const nonce = generateNonce(ORIGIN_CHECK_NONCE_LENGTH);\n if (!nonce) {\n throw new Error(\n 'CSRF Error: Failed to generate nonce for strategy \"origin-check\".'\n );\n }\n return {\n clientToken: nonce,\n cookieToken: nonce,\n cookieOptions: { ...baseOptions, httpOnly: false },\n };\n }\n\n default: {\n throw new Error(`Unknown CSRF strategy: ${this.config.strategy}`);\n }\n }\n }\n}\n\n/**\n * Factory function to create a CSRF protection instance.\n *\n * Convenient alternative to using the CsrfProtection constructor directly.\n * This function is the recommended way to create CSRF protection instances\n * as it provides better type inference and a cleaner API.\n *\n * @public\n * @template TRequest - Framework-specific request type\n * @template TResponse - Framework-specific response type\n * @param adapter - Framework adapter implementing CsrfAdapter interface\n * @param config - Optional CSRF configuration (uses secure defaults if not provided)\n * @returns Configured CSRF protection instance ready for use\n *\n * @example\n * ```typescript\n * import { createCsrfProtection } from '@csrf-armor/core';\n * import { ExpressAdapter } from '@csrf-armor/express';\n *\n * // Basic setup with defaults\n * const csrf = createCsrfProtection(new ExpressAdapter());\n *\n * // Custom configuration\n * const csrf = createCsrfProtection(new ExpressAdapter(), {\n * strategy: 'signed-double-submit',\n * secret: process.env.CSRF_SECRET,\n * token: {\n * expiry: 7200, // 2 hours\n * fieldName: 'authenticity_token'\n * },\n * excludePaths: ['/api/public'],\n * allowedOrigins: ['https://yourdomain.com']\n * });\n *\n * // Use in middleware\n * app.use(async (req, res, next) => {\n * const result = await csrf.protect(req, res);\n * if (result.success) {\n * next();\n * } else {\n * res.status(403).json({ error: result.reason });\n * }\n * });\n * ```\n *\n * @example\n * ```typescript\n * // Framework-specific usage\n *\n * // Express.js\n * import { ExpressAdapter } from '@csrf-armor/express';\n * const expressCsrf = createCsrfProtection(new ExpressAdapter(), config);\n *\n * // Next.js\n * import { NextjsAdapter } from '@csrf-armor/nextjs';\n * const nextCsrf = createCsrfProtection(new NextjsAdapter(), config);\n *\n * // Custom framework\n * class MyAdapter implements CsrfAdapter<MyRequest, MyResponse> {\n * // Implementation...\n * }\n * const customCsrf = createCsrfProtection(new MyAdapter(), config);\n * ```\n */\nexport function createCsrfProtection<TRequest = unknown, TResponse = unknown>(\n adapter: CsrfAdapter<TRequest, TResponse>,\n config?: CsrfConfig\n): CsrfProtection<TRequest, TResponse> {\n return new CsrfProtection(adapter, config);\n}\n"],"mappings":"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"}