@csrf-armor/core
Version:
Framework-agnostic CSRF protection core functionality
1 lines • 8.08 kB
Source Map (JSON)
{"version":3,"file":"validation.mjs","names":[],"sources":["../src/validation.ts"],"sourcesContent":["import { SAFE_METHODS } from './constants.js';\nimport {\n parseSignedToken,\n timingSafeEqual,\n verifySignedToken,\n} from './crypto.js';\nimport { OriginMismatchError } from './errors.js';\nimport type {\n CsrfRequest,\n RequiredCsrfConfig,\n ValidationResult,\n} from './types.js';\n\nfunction getHeaders(request: CsrfRequest): Map<string, string> {\n if (request.headers instanceof Map) {\n return request.headers;\n }\n\n return new Map(Object.entries(request.headers));\n}\n\nfunction getCookies(request: CsrfRequest): Map<string, string> {\n if (request.cookies instanceof Map) {\n return request.cookies;\n }\n\n return new Map(Object.entries(request.cookies));\n}\n\nexport async function validateSignedToken(\n request: CsrfRequest,\n config: RequiredCsrfConfig,\n getTokenFromRequest: (\n req: CsrfRequest,\n config: RequiredCsrfConfig\n ) => Promise<string | undefined>\n): Promise<ValidationResult> {\n try {\n const token = await getTokenFromRequest(request, config);\n\n if (!token) {\n return { isValid: false, reason: 'No CSRF token provided' };\n }\n\n await parseSignedToken(token, config.secret);\n return { isValid: true };\n } catch (error) {\n if (error instanceof Error) {\n return { isValid: false, reason: error.message };\n }\n return { isValid: false, reason: 'Unknown error' };\n }\n}\n\nexport function validateOrigin(\n request: CsrfRequest,\n config: RequiredCsrfConfig\n): ValidationResult {\n const headers = getHeaders(request);\n const origin = headers.get('origin');\n const referer = headers.get('referer');\n\n if (!origin && !referer && !SAFE_METHODS.includes(request.method as never)) {\n return { isValid: false, reason: 'Missing origin and referer headers' };\n }\n\n const requestOrigin = origin ?? (referer ? new URL(referer).origin : null);\n\n if (!requestOrigin) {\n return { isValid: false, reason: 'No origin or referer header' };\n }\n\n if (config.allowedOrigins.includes(requestOrigin)) {\n return { isValid: true };\n }\n\n return {\n isValid: false,\n reason: new OriginMismatchError(requestOrigin).message,\n };\n}\n// DO NOT USE THIS IN PRODUCTION\nexport async function validateDoubleSubmit(\n request: CsrfRequest,\n config: RequiredCsrfConfig,\n getTokenFromRequest: (\n req: CsrfRequest,\n config: RequiredCsrfConfig\n ) => Promise<string | undefined>\n): Promise<ValidationResult> {\n const cookies = getCookies(request);\n const cookieName = config.cookie.name;\n const cookieToken = cookies.get(cookieName);\n const submittedToken = await getTokenFromRequest(request, config);\n\n if (!cookieToken) {\n return { isValid: false, reason: 'No CSRF cookie found' };\n }\n\n if (!submittedToken) {\n return { isValid: false, reason: 'No CSRF token submitted' };\n }\n\n if (!timingSafeEqual(cookieToken, submittedToken)) {\n return { isValid: false, reason: 'Token mismatch' };\n }\n\n return { isValid: true };\n}\n\nexport async function validateSignedDoubleSubmit(\n request: CsrfRequest,\n config: RequiredCsrfConfig,\n getTokenFromRequest: (\n req: CsrfRequest,\n config: RequiredCsrfConfig\n ) => Promise<string | undefined>\n): Promise<ValidationResult> {\n const cookies = getCookies(request);\n\n const cookieName = config.cookie.name;\n const unsignedCookieToken = cookies.get(cookieName);\n const signedCookieToken = cookies.get(`${cookieName}-server`);\n const submittedToken = await getTokenFromRequest(request, config);\n\n if (!unsignedCookieToken || !signedCookieToken) {\n return { isValid: false, reason: 'Missing CSRF cookies' };\n }\n\n if (!submittedToken) {\n return { isValid: false, reason: 'No CSRF token submitted' };\n }\n\n try {\n // 1. Verify the server cookie signature\n const verifiedUnsignedToken = await verifySignedToken(\n signedCookieToken,\n config.secret\n );\n\n // 2. Ensure client cookie matches the verified token\n if (!timingSafeEqual(unsignedCookieToken, verifiedUnsignedToken)) {\n return { isValid: false, reason: 'Cookie integrity check failed' };\n }\n\n // 3. Ensure submitted token matches the unsigned token\n if (!timingSafeEqual(submittedToken, unsignedCookieToken)) {\n return { isValid: false, reason: 'Token mismatch' };\n }\n\n return { isValid: true };\n } catch (error) {\n if (error instanceof Error) {\n return { isValid: false, reason: error.message };\n }\n return { isValid: false, reason: 'Token validation failed' };\n }\n}\n\nexport async function validateRequest(\n request: CsrfRequest,\n config: RequiredCsrfConfig,\n getTokenFromRequest: (\n req: CsrfRequest,\n config: RequiredCsrfConfig\n ) => Promise<string | undefined>\n): Promise<ValidationResult> {\n switch (config.strategy) {\n case 'signed-token':\n return await validateSignedToken(request, config, getTokenFromRequest);\n\n case 'origin-check':\n return validateOrigin(request, config);\n\n case 'double-submit':\n return await validateDoubleSubmit(request, config, getTokenFromRequest);\n\n case 'signed-double-submit':\n return await validateSignedDoubleSubmit(\n request,\n config,\n getTokenFromRequest\n );\n\n case 'hybrid': {\n const originResult = validateOrigin(request, config);\n if (!originResult.isValid) return originResult;\n\n return await validateSignedToken(request, config, getTokenFromRequest);\n }\n\n default:\n return { isValid: false, reason: 'Invalid strategy' };\n }\n}\n"],"mappings":";;;;;AAaA,SAAS,WAAW,SAA2C;AAC7D,KAAI,QAAQ,mBAAmB,IAC7B,QAAO,QAAQ;AAGjB,QAAO,IAAI,IAAI,OAAO,QAAQ,QAAQ,QAAQ,CAAC;;AAGjD,SAAS,WAAW,SAA2C;AAC7D,KAAI,QAAQ,mBAAmB,IAC7B,QAAO,QAAQ;AAGjB,QAAO,IAAI,IAAI,OAAO,QAAQ,QAAQ,QAAQ,CAAC;;AAGjD,eAAsB,oBACpB,SACA,QACA,qBAI2B;AAC3B,KAAI;EACF,MAAM,QAAQ,MAAM,oBAAoB,SAAS,OAAO;AAExD,MAAI,CAAC,MACH,QAAO;GAAE,SAAS;GAAO,QAAQ;GAA0B;AAG7D,QAAM,iBAAiB,OAAO,OAAO,OAAO;AAC5C,SAAO,EAAE,SAAS,MAAM;UACjB,OAAO;AACd,MAAI,iBAAiB,MACnB,QAAO;GAAE,SAAS;GAAO,QAAQ,MAAM;GAAS;AAElD,SAAO;GAAE,SAAS;GAAO,QAAQ;GAAiB;;;AAItD,SAAgB,eACd,SACA,QACkB;CAClB,MAAM,UAAU,WAAW,QAAQ;CACnC,MAAM,SAAS,QAAQ,IAAI,SAAS;CACpC,MAAM,UAAU,QAAQ,IAAI,UAAU;AAEtC,KAAI,CAAC,UAAU,CAAC,WAAW,CAAC,aAAa,SAAS,QAAQ,OAAgB,CACxE,QAAO;EAAE,SAAS;EAAO,QAAQ;EAAsC;CAGzE,MAAM,gBAAgB,WAAW,UAAU,IAAI,IAAI,QAAQ,CAAC,SAAS;AAErE,KAAI,CAAC,cACH,QAAO;EAAE,SAAS;EAAO,QAAQ;EAA+B;AAGlE,KAAI,OAAO,eAAe,SAAS,cAAc,CAC/C,QAAO,EAAE,SAAS,MAAM;AAG1B,QAAO;EACL,SAAS;EACT,QAAQ,IAAI,oBAAoB,cAAc,CAAC;EAChD;;AAGH,eAAsB,qBACpB,SACA,QACA,qBAI2B;CAC3B,MAAM,UAAU,WAAW,QAAQ;CACnC,MAAM,aAAa,OAAO,OAAO;CACjC,MAAM,cAAc,QAAQ,IAAI,WAAW;CAC3C,MAAM,iBAAiB,MAAM,oBAAoB,SAAS,OAAO;AAEjE,KAAI,CAAC,YACH,QAAO;EAAE,SAAS;EAAO,QAAQ;EAAwB;AAG3D,KAAI,CAAC,eACH,QAAO;EAAE,SAAS;EAAO,QAAQ;EAA2B;AAG9D,KAAI,CAAC,gBAAgB,aAAa,eAAe,CAC/C,QAAO;EAAE,SAAS;EAAO,QAAQ;EAAkB;AAGrD,QAAO,EAAE,SAAS,MAAM;;AAG1B,eAAsB,2BACpB,SACA,QACA,qBAI2B;CAC3B,MAAM,UAAU,WAAW,QAAQ;CAEnC,MAAM,aAAa,OAAO,OAAO;CACjC,MAAM,sBAAsB,QAAQ,IAAI,WAAW;CACnD,MAAM,oBAAoB,QAAQ,IAAI,GAAG,WAAW,SAAS;CAC7D,MAAM,iBAAiB,MAAM,oBAAoB,SAAS,OAAO;AAEjE,KAAI,CAAC,uBAAuB,CAAC,kBAC3B,QAAO;EAAE,SAAS;EAAO,QAAQ;EAAwB;AAG3D,KAAI,CAAC,eACH,QAAO;EAAE,SAAS;EAAO,QAAQ;EAA2B;AAG9D,KAAI;AAQF,MAAI,CAAC,gBAAgB,qBANS,MAAM,kBAClC,mBACA,OAAO,OACR,CAG+D,CAC9D,QAAO;GAAE,SAAS;GAAO,QAAQ;GAAiC;AAIpE,MAAI,CAAC,gBAAgB,gBAAgB,oBAAoB,CACvD,QAAO;GAAE,SAAS;GAAO,QAAQ;GAAkB;AAGrD,SAAO,EAAE,SAAS,MAAM;UACjB,OAAO;AACd,MAAI,iBAAiB,MACnB,QAAO;GAAE,SAAS;GAAO,QAAQ,MAAM;GAAS;AAElD,SAAO;GAAE,SAAS;GAAO,QAAQ;GAA2B;;;AAIhE,eAAsB,gBACpB,SACA,QACA,qBAI2B;AAC3B,SAAQ,OAAO,UAAf;EACE,KAAK,eACH,QAAO,MAAM,oBAAoB,SAAS,QAAQ,oBAAoB;EAExE,KAAK,eACH,QAAO,eAAe,SAAS,OAAO;EAExC,KAAK,gBACH,QAAO,MAAM,qBAAqB,SAAS,QAAQ,oBAAoB;EAEzE,KAAK,uBACH,QAAO,MAAM,2BACX,SACA,QACA,oBACD;EAEH,KAAK,UAAU;GACb,MAAM,eAAe,eAAe,SAAS,OAAO;AACpD,OAAI,CAAC,aAAa,QAAS,QAAO;AAElC,UAAO,MAAM,oBAAoB,SAAS,QAAQ,oBAAoB;;EAGxE,QACE,QAAO;GAAE,SAAS;GAAO,QAAQ;GAAoB"}