From a00d78540ae36bc843b7a897784ab95f3d0379e7 Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Fri, 23 Aug 2024 01:46:30 +0100 Subject: [PATCH] refactor: don't bother about signing cookies, [framework] can deal with that --- package.json | 4 +- pnpm-lock.yaml | 132 +++++++++++++++++++++++++++++++++++++++---------- src/index.ts | 42 +++++----------- src/types.ts | 59 +++++----------------- 4 files changed, 130 insertions(+), 107 deletions(-) diff --git a/package.json b/package.json index 467b50c..4e1c5d0 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ ], "devDependencies": { "@biomejs/biome": "^1.8.3", - "@tinyhttp/cookie-parser": "^2.0.6", + "@otterhttp/cookie-signature": "^3.0.0", + "@otterhttp/request": "^3.1.1", "@types/node": "^20.14.10", "@vitest/coverage-istanbul": "^2.0.3", "standard-version": "^9.5.0", @@ -54,7 +55,6 @@ }, "dependencies": { "@otterhttp/cookie": "^3.0.0", - "@otterhttp/cookie-signature": "^3.0.0", "@otterhttp/errors": "^0.2.0" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca49b87..da8ca5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,6 @@ importers: '@otterhttp/cookie': specifier: ^3.0.0 version: 3.0.0 - '@otterhttp/cookie-signature': - specifier: ^3.0.0 - version: 3.0.0 '@otterhttp/errors': specifier: ^0.2.0 version: 0.2.0 @@ -21,9 +18,12 @@ importers: '@biomejs/biome': specifier: ^1.8.3 version: 1.8.3 - '@tinyhttp/cookie-parser': - specifier: ^2.0.6 - version: 2.0.6 + '@otterhttp/cookie-signature': + specifier: ^3.0.0 + version: 3.0.0 + '@otterhttp/request': + specifier: ^3.1.1 + version: 3.1.1 '@types/node': specifier: ^20.14.10 version: 20.14.10 @@ -379,6 +379,14 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@otterhttp/accepts@3.0.0': + resolution: {integrity: sha512-8G9vqGrmEarsIPI0jyPyuPRxnRQMvQpbAUuoXPZNIz569ypy0qGBs3YPTxtQiRy4Q2KDTNMa8PRLMQhkz4I8Gw==} + engines: {node: '>=20.16.0'} + + '@otterhttp/content-type@0.5.0': + resolution: {integrity: sha512-2qFRWOwTQ2ylO0LURyqvOKua0DDxZBl1hD5pZl/GLnoPfUlbX9edUcgbNrWyqTiQ44i63Wi8UhqiXMoJqbBuGA==} + engines: {node: '>=20.16.0'} + '@otterhttp/cookie-signature@3.0.0': resolution: {integrity: sha512-AcWQokILP5/oz4Nw5cj5d4UYCrhTaMupQ30eVcDJrNt06+MCJQ+NGgob3xsJJMXUxuUkOVu8eVgchatOqKvGQw==} engines: {node: '>=20.16.0'} @@ -391,6 +399,34 @@ packages: resolution: {integrity: sha512-QaUyvfOI6DBqiMTVDC6UpmtcaNXi672leKIOdI4r5WdjXTGCxHoP66VMHpb3XEOETTrSMBfFq63M6Md79CBOKQ==} engines: {node: '>=20.16.0'} + '@otterhttp/forwarded@3.0.0': + resolution: {integrity: sha512-wzYuiXu8rk0cQfIbjaheIoIHvGrM/6DjnZth0lNvtm94QbHb74k2F/zqYbR0N0pMa7QOGm6P9f4dsVD8qR5SRQ==} + engines: {node: '>=20.16.0'} + + '@otterhttp/parameters@0.1.0': + resolution: {integrity: sha512-yhqvMwsEZNA5iSssRzcjxi+sDUWg1TUyvxe6P5EeMgHFprsLoqh1TJgQO6Vji0wciHgkZduNzKSrlPdjyV7qyA==} + engines: {node: '>=20.16.0'} + + '@otterhttp/proxy-address@3.0.0': + resolution: {integrity: sha512-FVD/OTmlyQbD9MfPjgTcdLpglEmJknIXSc8KBGtdgmnfx+nAahy9tZt7JFWQKR/K9nzDreBO6FQD5Bg9YhhmuQ==} + engines: {node: '>=20.16.0'} + + '@otterhttp/request@3.1.1': + resolution: {integrity: sha512-Y7iaFjiUokisjZ2QU7sYMhACLlpqoSaP1FUTL5sswGjBfc5zlzhIyKFUo4Z0THGp8iLOZberdz72GsZrZozesA==} + engines: {node: '>=20.16.0'} + + '@otterhttp/router@3.0.0': + resolution: {integrity: sha512-L+pUl5qvD8gmDY32XpAFmrGpFUX/jpI6Y2Feoos/0rvCyc7Znc7XN9I+1XcXzp6lSqmWwVF8Eoj7JEHPeMsGnQ==} + engines: {node: '>=20.16.0'} + + '@otterhttp/type-is@4.0.1': + resolution: {integrity: sha512-ir2n0Xu9Nixi02YPJZd9BbdQIxLSRrFaIN2pGMsiqhvVF7AjaQBFcKnSx3dQ+b6RLuej8GYHk2wz+PPdO0rGiA==} + engines: {node: '>=20.16.0'} + + '@otterhttp/url@3.0.0': + resolution: {integrity: sha512-z/nh6ZacQiPihuah6gsMbcs4EYL9wAzd/VoNAAliwnpMylJayOm46eJtfEn6dlH4L0M6ic89zCI39Qnb5qNx/w==} + engines: {node: '>=20.16.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -475,18 +511,6 @@ packages: cpu: [x64] os: [win32] - '@tinyhttp/cookie-parser@2.0.6': - resolution: {integrity: sha512-hLcFr3ml6MBU71URUutvzh8p0PgqGpmTMcorUrRS+Fck0/q3tzScE+ghhocgIoSx/F6GRtS/TYJkJrjL/ickBw==} - engines: {node: '>=12.4 || 14.x || >=16'} - - '@tinyhttp/cookie-signature@2.1.1': - resolution: {integrity: sha512-VDsSMY5OJfQJIAtUgeQYhqMPSZptehFSfvEEtxr+4nldPA8IImlp3QVcOVuK985g4AFR4Hl1sCbWCXoqBnVWnw==} - engines: {node: '>=12.20.0'} - - '@tinyhttp/cookie@2.1.1': - resolution: {integrity: sha512-h/kL9jY0e0Dvad+/QU3efKZww0aTvZJslaHj3JTPmIPC9Oan9+kYqmh3M6L5JUQRuTJYFK2nzgL2iJtH2S+6dA==} - engines: {node: '>=12.20.0'} - '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -985,6 +1009,10 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + header-range-parser@1.1.3: + resolution: {integrity: sha512-B9zCFt3jH8g09LR1vHL4pcAn8yMEtlSlOUdQemzHMRKMImNIhhszdeosYFfNW0WXKQtXIlWB+O4owHJKvEJYaA==} + engines: {node: '>=12.22.0'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -1017,6 +1045,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1216,6 +1248,11 @@ packages: resolution: {integrity: sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==} engines: {node: '>=8.6'} + mime@4.0.4: + resolution: {integrity: sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==} + engines: {node: '>=16'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -1265,6 +1302,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -2192,6 +2233,15 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@otterhttp/accepts@3.0.0': + dependencies: + mime: 4.0.4 + negotiator: 0.6.3 + + '@otterhttp/content-type@0.5.0': + dependencies: + '@otterhttp/parameters': 0.1.0 + '@otterhttp/cookie-signature@3.0.0': {} '@otterhttp/cookie@3.0.0': {} @@ -2200,6 +2250,35 @@ snapshots: dependencies: module-error: 1.0.2 + '@otterhttp/forwarded@3.0.0': {} + + '@otterhttp/parameters@0.1.0': {} + + '@otterhttp/proxy-address@3.0.0': + dependencies: + '@otterhttp/forwarded': 3.0.0 + ipaddr.js: 2.2.0 + + '@otterhttp/request@3.1.1': + dependencies: + '@otterhttp/accepts': 3.0.0 + '@otterhttp/content-type': 0.5.0 + '@otterhttp/cookie': 3.0.0 + '@otterhttp/proxy-address': 3.0.0 + '@otterhttp/router': 3.0.0 + '@otterhttp/type-is': 4.0.1 + '@otterhttp/url': 3.0.0 + header-range-parser: 1.1.3 + + '@otterhttp/router@3.0.0': {} + + '@otterhttp/type-is@4.0.1': + dependencies: + '@otterhttp/content-type': 0.5.0 + mime: 4.0.4 + + '@otterhttp/url@3.0.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -2251,15 +2330,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.18.1': optional: true - '@tinyhttp/cookie-parser@2.0.6': - dependencies: - '@tinyhttp/cookie': 2.1.1 - '@tinyhttp/cookie-signature': 2.1.1 - - '@tinyhttp/cookie-signature@2.1.1': {} - - '@tinyhttp/cookie@2.1.1': {} - '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -2838,6 +2908,8 @@ snapshots: dependencies: function-bind: 1.1.2 + header-range-parser@1.1.3: {} + hosted-git-info@2.8.9: {} hosted-git-info@4.1.0: @@ -2858,6 +2930,8 @@ snapshots: ini@1.3.8: {} + ipaddr.js@2.2.0: {} + is-arrayish@0.2.1: {} is-binary-path@2.1.0: @@ -3043,6 +3117,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime@4.0.4: {} + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -3081,6 +3157,8 @@ snapshots: nanoid@3.3.7: {} + negotiator@0.6.3: {} + neo-async@2.6.2: {} node-releases@2.0.14: {} diff --git a/src/index.ts b/src/index.ts index 297b548..c438649 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,4 @@ import { createHmac, randomBytes } from "node:crypto" -import { type SerializeOptions, serialize } from "@otterhttp/cookie" -import { sign } from "@otterhttp/cookie-signature" import { ClientError } from "@otterhttp/errors" import type { @@ -13,31 +11,17 @@ import type { GenerateCsrfTokenConfig, RequestMethod, ResolvedCSRFCookieOptions, - Response, + CSRFResponse, doubleCsrfProtection, } from "./types" function setSecretCookie( req: CSRFRequest, - res: Response, + res: CSRFResponse, secret: string, - { signed, getSigningSecret, name, ...options }: ResolvedCSRFCookieOptions, + { name, ...options }: ResolvedCSRFCookieOptions, ): void { - if (!signed) { - setCookie(res, name, secret, options) - return - } - - let signingSecret = getSigningSecret(req) - if (Array.isArray(signingSecret)) signingSecret = signingSecret[0] - - const value = `s:${sign(secret, signingSecret)}` - setCookie(res, name, value, options) -} - -function setCookie(res: Response, name: string, value: string, options: SerializeOptions): void { - const data = serialize(name, value, options) - res.appendHeader("set-cookie", data) + res.cookie(name, secret, options) } export function doubleCsrf({ @@ -81,13 +65,13 @@ export function doubleCsrf({ const possibleSecrets = Array.isArray(getSecretResult) ? getSecretResult : [getSecretResult] const csrfCookie = getCsrfCookieFromRequest(req) - // If ovewrite is true, always generate a new token. + // If overwrite is true, always generate a new token. // If overwrite is false and there is no existing token, generate a new token. - // If overwrite is false and there is an existin token then validate the token and hash pair + // If overwrite is false and there is an existing token then validate the token and hash pair // the existing cookie and reuse it if it is valid. If it isn't valid, then either throw or // generate a new token based on validateOnReuse. - if (typeof csrfCookie === "string" && !overwrite) { - const [csrfToken, csrfTokenHash] = csrfCookie.split(delimiter) + if (typeof csrfCookie === "object" && !overwrite) { + const [csrfToken, csrfTokenHash] = csrfCookie.value.split(delimiter) if ( validateTokenAndHashPair(req, { incomingToken: csrfToken, @@ -122,7 +106,7 @@ export function doubleCsrf({ // Do NOT send the csrfToken as a cookie, embed it in your HTML response, or as JSON. const generateToken: CsrfTokenCreator = ( req: CSRFRequest, - res: Response, + res: CSRFResponse, { cookieOptions = defaultCookieOptions, overwrite = false, validateOnReuse = true } = {}, ) => { const { csrfToken, csrfTokenHash } = generateTokenAndHash(req, { @@ -136,9 +120,7 @@ export function doubleCsrf({ return csrfToken } - const getCsrfCookieFromRequest = defaultCookieOptions.signed - ? (req: CSRFRequest) => req.signedCookies?.[defaultCookieOptions.name] - : (req: CSRFRequest) => req.cookies?.[defaultCookieOptions.name] + const getCsrfCookieFromRequest = (req: CSRFRequest) => req.cookies?.[defaultCookieOptions.name] // given a secret array, iterates over it and checks whether one of the secrets makes the token and hash pair valid const validateTokenAndHashPair: CsrfTokenAndHashPairValidator = ( @@ -160,10 +142,10 @@ export function doubleCsrf({ const validateRequest: CsrfRequestValidator = (req) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access const csrfCookie = getCsrfCookieFromRequest(req) - if (typeof csrfCookie !== "string") return false + if (typeof csrfCookie !== "object") return false // cookie has the form {token}{delimiter}{hash} - const [csrfTokenFromCookie, csrfTokenHash] = csrfCookie.split(delimiter) + const [csrfTokenFromCookie, csrfTokenHash] = csrfCookie.value.split(delimiter) // csrf token from the request const csrfTokenFromRequest = getTokenFromRequest(req) as string diff --git a/src/types.ts b/src/types.ts index 6fdf8c9..de6a75e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,52 +4,15 @@ import type { HttpError, statusMessages } from "@otterhttp/errors" type NextFunction = () => unknown +type Cookie = { value: string } + export type CSRFRequest = IncomingMessage & { - signedCookies?: Record - cookies?: Record + cookies: Record } -export type Response = ServerResponse - -type CookieSigningOptions = - | { - /** - * Whether to sign the anti-CSRF cookie. - * @default false - */ - signed: true - - /** - * A function that returns a cookie-signing secret or an array of secrets. - * The first secret should be the newest/preferred secret. - * You do not need to use the request object, but it is available if you need it. - */ - getSigningSecret: CsrfSecretRetriever - } - | { - /** - * Whether to sign the anti-CSRF cookie. - * @default false - */ - signed?: false | undefined - - /** - * A function that returns a cookie-signing secret or an array of secrets. - * The first secret should be the newest/preferred secret. - * You do not need to use the request object, but it is available if you need it. - */ - getSigningSecret?: undefined - } - -type ResolvedCookieSigningOptions = - | { - signed: true - getSigningSecret: CsrfSecretRetriever - } - | { - signed: false - getSigningSecret: undefined - } +export type CSRFResponse = ServerResponse & { + cookie: (name: string, value: string, options?: SerializeOptions) => unknown +} type ExtraCookieOptions = { /** @@ -59,12 +22,12 @@ type ExtraCookieOptions = { name?: string } -export type CSRFCookieOptions = SerializeOptions & CookieSigningOptions & ExtraCookieOptions -export type ResolvedCSRFCookieOptions = SerializeOptions & ResolvedCookieSigningOptions & Required +export type CSRFCookieOptions = SerializeOptions & ExtraCookieOptions +export type ResolvedCSRFCookieOptions = SerializeOptions & Required export type TokenRetriever = (req: CSRFRequest) => string | null | undefined export type CsrfSecretRetriever = (req?: CSRFRequest) => string | Array -export type doubleCsrfProtection = (req: CSRFRequest, res: Response, next: NextFunction) => void +export type doubleCsrfProtection = (req: CSRFRequest, res: CSRFResponse, next: NextFunction) => void export type RequestMethod = "GET" | "HEAD" | "PATCH" | "PUT" | "POST" | "DELETE" | "CONNECT" | "OPTIONS" | "TRACE" export type CsrfIgnoredMethods = Array export type CsrfRequestValidator = (req: CSRFRequest) => boolean @@ -80,8 +43,8 @@ export type CsrfTokenAndHashPairValidator = ( possibleSecrets: Array }, ) => boolean -export type CsrfCookieSetter = (res: Response, name: string, value: string, options: CSRFCookieOptions) => void -export type CsrfTokenCreator = (req: CSRFRequest, res: Response, options?: GenerateCsrfTokenOptions) => string +export type CsrfCookieSetter = (res: CSRFResponse, name: string, value: string, options: CSRFCookieOptions) => void +export type CsrfTokenCreator = (req: CSRFRequest, res: CSRFResponse, options?: GenerateCsrfTokenOptions) => string export type CsrfErrorConfig = { statusCode: keyof typeof statusMessages message: string