From 252aeefe698f5ecf5338b29ed7e78e45660a8efc Mon Sep 17 00:00:00 2001 From: Lordfirespeed <28568841+Lordfirespeed@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:38:18 +0100 Subject: [PATCH] feat: support `sign` and `unsign` options for cookie-signing --- package.json | 2 +- pnpm-lock.yaml | 12 +++++++++--- src/index.ts | 1 - src/types.ts | 28 ++++++++++++++++++++++++---- tests/doublecsrf.test.ts | 21 ++++++++++++++++++--- 5 files changed, 52 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 48f2f03..67f3389 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "keywords": ["csrf", "middleware", "tokens"], "devDependencies": { "@biomejs/biome": "^1.8.3", + "@otterhttp/cookie": "^3.0.1", "@otterhttp/cookie-signature": "^3.0.0", "@otterhttp/request": "^3.1.1", "@types/node": "^20.14.10", @@ -48,7 +49,6 @@ "vitest": "^2.0.3" }, "dependencies": { - "@otterhttp/cookie": "^3.0.0", "@otterhttp/errors": "^0.2.0" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da8ca5d..6ddc862 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - '@otterhttp/cookie': - specifier: ^3.0.0 - version: 3.0.0 '@otterhttp/errors': specifier: ^0.2.0 version: 0.2.0 @@ -18,6 +15,9 @@ importers: '@biomejs/biome': specifier: ^1.8.3 version: 1.8.3 + '@otterhttp/cookie': + specifier: ^3.0.1 + version: 3.0.1 '@otterhttp/cookie-signature': specifier: ^3.0.0 version: 3.0.0 @@ -395,6 +395,10 @@ packages: resolution: {integrity: sha512-D1/bN2s985/5wSZ0Bn3F3I7HZW3vNCIg/Pthrk7dIiwg3Y1flPrlwQOK84aNk4C2w19QGgqccuDHSIPJtoNsHg==} engines: {node: '>=20.16.0'} + '@otterhttp/cookie@3.0.1': + resolution: {integrity: sha512-yCEQQR6pSvBlry4k4+EsXDvHfTvLaB5ELq4tz1eStAThzMjUtgz1RG4FUvu2fQuCtaGoj/BE0xsRcZOxgx2YNw==} + engines: {node: '>=20.16.0'} + '@otterhttp/errors@0.2.0': resolution: {integrity: sha512-QaUyvfOI6DBqiMTVDC6UpmtcaNXi672leKIOdI4r5WdjXTGCxHoP66VMHpb3XEOETTrSMBfFq63M6Md79CBOKQ==} engines: {node: '>=20.16.0'} @@ -2246,6 +2250,8 @@ snapshots: '@otterhttp/cookie@3.0.0': {} + '@otterhttp/cookie@3.0.1': {} + '@otterhttp/errors@0.2.0': dependencies: module-error: 1.0.2 diff --git a/src/index.ts b/src/index.ts index 464f4f8..858cfbd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,7 +48,6 @@ export function doubleCsrf< path: "/", secure: true, httpOnly: true, - signed: false, }, cookieOptions, ) diff --git a/src/types.ts b/src/types.ts index 0fb920b..9fdda16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,12 +6,20 @@ type NextFunction = () => unknown type Cookie = { value: string } +type SetCookieOptions = SerializeOptions & { + /** + * `otterhttp` cookie `sign` function, will be passed to `res.cookie`. + * @default undefined + */ + sign?: ((value: string) => string) | undefined +} + export type CSRFRequest = IncomingMessage & { cookies: Record } export type CSRFResponse = ServerResponse & { - cookie: (name: string, value: string, options?: SerializeOptions) => unknown + cookie: (name: string, value: string, options?: SetCookieOptions) => unknown } type ExtraCookieOptions = { @@ -19,11 +27,23 @@ type ExtraCookieOptions = { * The name of the HTTPOnly cookie that will be set on the response. * @default "__Host-otter.x-csrf-token" */ - name?: string + name?: string | undefined + + /** + * `otterhttp` cookie 'unsign' function, will be used to unsign csrf-csrf cookies. + * + * You must ensure that signed csrf-csrf cookies are not matched by your `otterhttp` `App`'s configured + * `signedCookieMatcher`. Otherwise, `otterhttp` will attempt to unsign session cookies using the `App`'s configured + * `cookieUnsigner` instead, and unsigning with this function will not be attempted. + * @default undefined + */ + unsign?: ((signedValue: string) => string) | undefined } -export type CSRFCookieOptions = SerializeOptions & ExtraCookieOptions -export type ResolvedCSRFCookieOptions = SerializeOptions & Required +export type CSRFCookieOptions = SetCookieOptions & ExtraCookieOptions +export type ResolvedCSRFCookieOptions = SetCookieOptions + & Required> + & Exclude export type TokenRetriever< Request extends CSRFRequest = CSRFRequest, diff --git a/tests/doublecsrf.test.ts b/tests/doublecsrf.test.ts index a56fad5..51db797 100644 --- a/tests/doublecsrf.test.ts +++ b/tests/doublecsrf.test.ts @@ -1,7 +1,6 @@ -import { doubleCsrf } from "@/index" -import type { DoubleCsrfConfig } from "@/types" -/* eslint-disable @typescript-eslint/ban-ts-comment */ import { assert, describe, it } from "vitest" +import { sign, unsign } from "@otterhttp/cookie-signature" + import { createTestSuite } from "./testsuite" import { COOKIE_SECRET, HEADER_KEY } from "./utils/constants" import { @@ -12,11 +11,27 @@ import { } from "./utils/helpers.js" import { generateMocks, generateMocksWithToken } from "./utils/mock.js" +import { doubleCsrf } from "@/index" +import type { DoubleCsrfConfig } from "@/types" + createTestSuite("csrf-csrf single secret", { getSecret: getSingleSecret, getSessionIdentifier: legacySessionIdentifier, }) +createTestSuite("csrf-csrf single secret with cookie-signing", { + getSecret: getSingleSecret, + getSessionIdentifier: legacySessionIdentifier, + cookieOptions: { + sign: (value: string) => `s:${sign(value, COOKIE_SECRET)}`, + unsign: (signedValue: string) => { + const result = unsign(signedValue.slice(2), COOKIE_SECRET) + if (result === false) throw new Error() + return result + } + } +}) + createTestSuite("csrf-csrf custom options, single secret", { getSecret: getSingleSecret, getSessionIdentifier: legacySessionIdentifier,