Skip to content

Commit

Permalink
feat: support sign and unsign options for cookie-signing
Browse files Browse the repository at this point in the history
  • Loading branch information
Lordfirespeed committed Aug 27, 2024
1 parent 8781d5f commit 252aeef
Show file tree
Hide file tree
Showing 5 changed files with 52 additions and 12 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -48,7 +49,6 @@
"vitest": "^2.0.3"
},
"dependencies": {
"@otterhttp/cookie": "^3.0.0",
"@otterhttp/errors": "^0.2.0"
},
"engines": {
Expand Down
12 changes: 9 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export function doubleCsrf<
path: "/",
secure: true,
httpOnly: true,
signed: false,
},
cookieOptions,
)
Expand Down
28 changes: 24 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,44 @@ 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<string, Cookie>
}

export type CSRFResponse<Request extends CSRFRequest = CSRFRequest> = ServerResponse<Request> & {
cookie: (name: string, value: string, options?: SerializeOptions) => unknown
cookie: (name: string, value: string, options?: SetCookieOptions) => unknown
}

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<ExtraCookieOptions>
export type CSRFCookieOptions = SetCookieOptions & ExtraCookieOptions
export type ResolvedCSRFCookieOptions = SetCookieOptions
& Required<Pick<ExtraCookieOptions, "name">>
& Exclude<ExtraCookieOptions, "name">

export type TokenRetriever<
Request extends CSRFRequest = CSRFRequest,
Expand Down
21 changes: 18 additions & 3 deletions tests/doublecsrf.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand Down

0 comments on commit 252aeef

Please sign in to comment.