Skip to content

Commit

Permalink
Merge branch 'main' into v3.x.x
Browse files Browse the repository at this point in the history
  • Loading branch information
psibean authored Aug 7, 2024
2 parents 2c858f5 + 5e5ba9b commit 48cfc6f
Show file tree
Hide file tree
Showing 9 changed files with 81 additions and 59 deletions.
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ string;
<b>Default:</b> <code>"__Host-psifi.x-csrf-token"</code><br />
</p>

<p><b>Optional:</b> The name of the httpOnly cookie that will be used to track CSRF protection. If you change this it is recommend that you continue to use the <code>__Host-</code> or <code>__Secure-</code> <a target="_blank" href="developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie">security prefix</a>.</p>
<p><b>Optional:</b> The name of the cookie that will be used to track CSRF protection. If you change this it is recommend that you continue to use the <code>__Host-</code> or <code>__Secure-</code> <a target="_blank" href="developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie">security prefix</a>.</p>

<p><b>Change for development</b></p>

Expand Down Expand Up @@ -352,32 +352,37 @@ Used to customise the error response <code>statusCode</code>, the contained erro
(
request: Request,
response: Response,
overwrite?: boolean, // Set to true to force a new token to be generated
validateOnReuse?: boolean, // Set to false to generate a new token if token re-use is invalid
{
cookieOptions?: CookieOptions, // overrides cookieOptions previously configured just for this call
overwrite?: boolean, // Set to true to force a new token to be generated
validateOnReuse?: boolean, // Set to false to generate a new token if token re-use is invalid
} // optional
) => string;
```

<p>By default if a csrf-csrf cookie already exists on an incoming request, generateToken will not overwrite it, it will simply return the existing token so long as the token is valid. If you wish to force a token generation, you can use the third parameter:</p>

```ts
generateToken(req, res, true); // This will force a new token to be generated, and a new cookie to be set, even if one already exists
generateToken(req, res, { overwrite: true }); // This will force a new token to be generated, and a new cookie to be set, even if one already exists
```

<p>If the 'overwrite' parameter is set to false (default), the existing token will be re-used and returned. However, the cookie value will also be validated. If the validation fails an error will be thrown. If you don't want an error to be thrown, you can set the 'validateOnReuse' (by default, true) to false. In this case instead of throwing an error, a new token will be generated and returned.
</p>

```ts
generateToken(req, res, true); // As overwrite is true, an error will never be thrown.
generateToken(req, res, false); // As validateOnReuse is true (default), an error will be thrown if the cookie is invalid.
generateToken(req, res, false, false); // As validateOnReuse is false, an error will never be thrown, even if the cookie is invalid. Instead, a new cookie will be generated if it is found to be invalid.
generateToken(req, res, { overwrite: true }); // As overwrite is true, an error will never be thrown.
generateToken(req, res, { overwrite: false }); // As validateOnReuse is true (default), an error will be thrown if the cookie is invalid.
generateToken(req, res, { overwrite: false, validateOnReuse: false }); // As validateOnReuse is false, if the cookie is invalid a new token will be generated without any error being thrown and despite overwrite being false
```

<p>Instead of importing and using generateToken, you can also use req.csrfToken any time after the doubleCsrfProtection middleware has executed on your incoming request.</p>

```ts
req.csrfToken(); // same as generateToken(req, res) and generateToken(req, res, false);
req.csrfToken(true); // same as generateToken(req, res, true);
req.csrfToken(false, false); // same as generateToken(req, res, false, false);
req.csrfToken(); // same as generateToken(req, res);
req.csrfToken({ overwrite: true }); // same as generateToken(req, res, { overwrite: true, validateOnReuse });
req.csrfToken({ overwrite: false, validateOnReuse: false }); // same as generateToken(req, res, { overwrite: false, validateOnReuse: false });
req.csrfToken(req, res, { overwrite: false });
req.csrfToken(req, res, { overwrite: false, validateOnReuse: false });
```

<p>The <code>generateToken</code> function serves the purpose of establishing a CSRF (Cross-Site Request Forgery) protection mechanism by generating a token and an associated cookie. This function also provides the option to utilize a third parameter called <code>overwrite</code>, and a fourth parameter called <code>validateOnReuse</code>. By default, <code>overwrite</code> is set to <em>false</em>, and <code>validateOnReuse</code> is set to <em>true</em>.</p>
Expand Down
4 changes: 2 additions & 2 deletions example/complete/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"license": "ISC",
"dependencies": {
"cookie-parser": "^1.4.6",
"csrf-csrf": "latest",
"express": "^4.18.1"
"csrf-csrf": "file:../..",
"express": "^4.19.2"
}
}
2 changes: 1 addition & 1 deletion example/complete/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const { invalidCsrfTokenError, generateToken, doubleCsrfProtection } =
doubleCsrf({
getSecret: () => CSRF_SECRET,
cookieName: CSRF_COOKIE_NAME,
cookieOptions: { sameSite: false, secure: false, signed: true }, // not ideal for production, development only
cookieOptions: { sameSite: false, secure: false }, // not ideal for production, development only
});

app.use(cookieParser(COOKIES_SECRET));
Expand Down
4 changes: 2 additions & 2 deletions example/simple/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"author": "psibean",
"license": "ISC",
"dependencies": {
"csrf-csrf": "latest",
"express": "^4.18.1",
"csrf-csrf": "../..",
"express": "^4.19.2",
"cookie-parser": "^1.4.6"
}
}
2 changes: 1 addition & 1 deletion example/simple/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const port = 5555;
const { doubleCsrfProtection } = doubleCsrf({
getSecret: () => "this is a test", // NEVER DO THIS
cookieName: "x-csrf-test", // Prefer "__Host-" prefixed names if possible
cookieOptions: { sameSite: false, secure: false, signed: true }, // not ideal for production, development only
cookieOptions: { sameSite: false, secure: false }, // not ideal for production, development only
});

app.use(cookieParser("some super secret thing, please do not copy this"));
Expand Down
33 changes: 22 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import type {
doubleCsrfProtection,
DoubleCsrfUtilities,
RequestMethod,
GenerateCsrfTokenConfig,
GenerateCsrfTokenOptions,
} from "./types";

export * from "./types";
Expand All @@ -23,6 +25,7 @@ export function doubleCsrf({
sameSite = "lax",
path = "/",
secure = true,
httpOnly = true,
...remainingCookieOptions
} = {},
size = 64,
Expand All @@ -35,10 +38,11 @@ export function doubleCsrf({
} = {},
}: DoubleCsrfConfigOptions): DoubleCsrfUtilities {
const ignoredMethodsSet = new Set(ignoredMethods);
const cookieOptions = {
const defaultCookieOptions = {
sameSite,
path,
secure,
httpOnly,
...remainingCookieOptions,
};

Expand All @@ -48,8 +52,10 @@ export function doubleCsrf({

const generateTokenAndHash = (
req: Request,
overwrite: boolean,
validateOnReuse: boolean,
{
overwrite,
validateOnReuse,
}: Omit<GenerateCsrfTokenConfig, "cookieOptions">,
) => {
const getSecretResult = getSecret(req);
const possibleSecrets = Array.isArray(getSecretResult)
Expand Down Expand Up @@ -92,16 +98,21 @@ export function doubleCsrf({
const generateToken: CsrfTokenCreator = (
req: Request,
res: Response,
overwrite = false,
validateOnReuse = true,
{
cookieOptions = defaultCookieOptions,
overwrite = false,
validateOnReuse = true,
} = {},
) => {
const { csrfToken, csrfTokenHash } = generateTokenAndHash(
req,
const { csrfToken, csrfTokenHash } = generateTokenAndHash(req, {
overwrite,
validateOnReuse,
);
});
const cookieContent = `${csrfToken}|${csrfTokenHash}`;
res.cookie(cookieName, cookieContent, { ...cookieOptions, httpOnly: true });
res.cookie(cookieName, cookieContent, {
...defaultCookieOptions,
...cookieOptions,
});
return csrfToken;
};

Expand Down Expand Up @@ -155,8 +166,8 @@ export function doubleCsrf({

const doubleCsrfProtection: doubleCsrfProtection = (req, res, next) => {
// TODO: next major update, breaking change, make a single object parameter
req.csrfToken = (overwrite?: boolean, validateOnReuse?: boolean) =>
generateToken(req, res, overwrite, validateOnReuse);
req.csrfToken = (options: GenerateCsrfTokenOptions) =>
generateToken(req, res, options);
if (ignoredMethodsSet.has(req.method as RequestMethod)) {
next();
} else if (validateRequest(req)) {
Expand Down
16 changes: 6 additions & 10 deletions src/tests/doublecsrf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,9 @@ describe("csrf-csrf token-rotation", () => {

const mockResponse = getEmptyResponse();

const token = generateTokenWithSecret1And2(
mockRequest,
mockResponse,
true,
);
const token = generateTokenWithSecret1And2(mockRequest, mockResponse, {
overwrite: true,
});

attachResponseValuesToRequest({
request: mockRequest,
Expand All @@ -223,11 +221,9 @@ describe("csrf-csrf token-rotation", () => {

const mockResponse = getEmptyResponse();

const token = generateTokenWithSecret2And1(
mockRequest,
mockResponse,
true,
);
const token = generateTokenWithSecret2And1(mockRequest, mockResponse, {
overwrite: true,
});

attachResponseValuesToRequest({
request: mockRequest,
Expand Down
34 changes: 19 additions & 15 deletions src/tests/testsuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ export const createTestSuite: CreateTestsuite = (name, doubleCsrfOptions) => {
// reset the mock response to have no cookies (in reality this would just be a new instance of Response)
mockResponse.setHeader("set-cookie", []);

const generatedToken = generateToken(mockRequest, mockResponse, true);
const generatedToken = generateToken(mockRequest, mockResponse, {
overwrite: true,
});
const newCookieValue = getCookieFromResponse(mockResponse);

assert.notEqual(newCookieValue, oldCookieValue);
Expand All @@ -139,7 +141,10 @@ export const createTestSuite: CreateTestsuite = (name, doubleCsrfOptions) => {
(decodedCookieValue as string).split("|")[0] + "|invalid-hash");

expect(() =>
generateToken(mockRequest, mockResponse, false, true),
generateToken(mockRequest, mockResponse, {
overwrite: false,
validateOnReuse: true,
}),
).to.throw(invalidCsrfTokenError.message);

// just an invalid value in the cookie
Expand All @@ -151,7 +156,10 @@ export const createTestSuite: CreateTestsuite = (name, doubleCsrfOptions) => {
: (mockRequest.cookies[cookieName] = "invalid-value");

expect(() =>
generateToken(mockRequest, mockResponse, false, true),
generateToken(mockRequest, mockResponse, {
overwrite: false,
validateOnReuse: true,
}),
).to.throw(invalidCsrfTokenError.message);
});

Expand All @@ -178,12 +186,10 @@ export const createTestSuite: CreateTestsuite = (name, doubleCsrfOptions) => {
(decodedCookieValue as string).split("|")[0] + "|invalid-hash");
assert.doesNotThrow(
() =>
(generatedToken = generateToken(
mockRequest,
mockResponse,
false,
false,
)),
(generatedToken = generateToken(mockRequest, mockResponse, {
overwrite: false,
validateOnReuse: false,
})),
);
newCookieValue = getCookieFromResponse(mockResponse);
assert.notEqual(newCookieValue, oldCookieValue);
Expand All @@ -199,12 +205,10 @@ export const createTestSuite: CreateTestsuite = (name, doubleCsrfOptions) => {

assert.doesNotThrow(
() =>
(generatedToken = generateToken(
mockRequest,
mockResponse,
false,
false,
)),
(generatedToken = generateToken(mockRequest, mockResponse, {
overwrite: false,
validateOnReuse: false,
})),
);

newCookieValue = getCookieFromResponse(mockResponse);
Expand Down
20 changes: 13 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { HttpError } from "http-errors";

export type SameSiteType = boolean | "lax" | "strict" | "none";
export type TokenRetriever = (req: Request) => string | null | undefined;
export type DoubleCsrfCookieOptions = Omit<CookieOptions, "httpOnly">;
export type CsrfTokenCookieOverrides = Omit<CookieOptions, "signed">;
declare module "http" {
interface IncomingHttpHeaders {
"x-csrf-token"?: string | undefined;
Expand All @@ -12,7 +12,9 @@ declare module "http" {

declare module "express-serve-static-core" {
export interface Request {
csrfToken?: (overwrite?: boolean) => ReturnType<CsrfTokenCreator>;
csrfToken?: (
options?: GenerateCsrfTokenOptions,
) => ReturnType<CsrfTokenCreator>;
}
}

Expand Down Expand Up @@ -46,21 +48,25 @@ export type CsrfCookieSetter = (
res: Response,
name: string,
value: string,
options: DoubleCsrfCookieOptions,
options: CookieOptions,
) => void;
export type CsrfTokenCreator = (
req: Request,
res: Response,
ovewrite?: boolean,
validateOnReuse?: boolean,
options?: GenerateCsrfTokenOptions,
) => string;
export type CsrfErrorConfig = {
statusCode: number;
message: string;
code: string | undefined;
};
export type CsrfErrorConfigOptions = Partial<CsrfErrorConfig>;

export type GenerateCsrfTokenConfig = {
overwrite: boolean;
validateOnReuse: boolean;
cookieOptions: CsrfTokenCookieOverrides;
};
export type GenerateCsrfTokenOptions = Partial<GenerateCsrfTokenConfig>;
export interface DoubleCsrfConfig {
/**
* A function that returns a secret or an array of secrets.
Expand Down Expand Up @@ -98,7 +104,7 @@ export interface DoubleCsrfConfig {
* The options for HTTPOnly cookie that will be set on the response.
* @default { sameSite: "lax", path: "/", secure: true }
*/
cookieOptions: DoubleCsrfCookieOptions;
cookieOptions: CookieOptions;

/**
* The methods that will be ignored by the middleware.
Expand Down

0 comments on commit 48cfc6f

Please sign in to comment.