Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ string;
```

<p>
<b>Optional<br />
<b>Optional</b><br />
<b>Default:</b>
</p>

Expand Down Expand Up @@ -403,7 +403,7 @@ Used to customise the error response <code>statusCode</code>, the contained erro
{
cookieOptions?: CookieOptions, // allows overriding of cookieOptions
overwrite?: boolean, // Set to true to force a new token to be generated
validateOnReuse?: boolean, // Set to true to throw an error when overwrite is false and the current CSRF token is invalid
validateOnReuse?: boolean, // Deprecated, leave as default
} // optional
) => string;
```
Expand All @@ -414,33 +414,31 @@ Used to customise the error response <code>statusCode</code>, the contained erro
generateCsrfToken(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 <code>overwrite</code> parameter is set to <em>false</em> (default), the existing token will be re-used and returned. If the current / existing CSRF token is not valid, then a new token will be generated without any error being thrown. If you want the <code>generateCsrfToken</code> to throw an error instead, provide the <code>validateOnReuse: true</code> option.</p>
<p>If the <code>overwrite</code> parameter is set to <em>false</em> (default), the existing token will be re-used and returned.</p>

<p>If <code>overwrite</code> is <em>true</em> a new token will always be generated, even if the current one is invalid.</p>

```ts
generateCsrfToken(req, res, { overwrite: true }); // As overwrite is true, an error will never be thrown.
generateCsrfToken(req, res, { overwrite: false }); // As validateOnReuse is false (default), if the current CSRF token from the cookie is invalid, a new token will be generated without any error being thrown.
generateCsrfToken(req, res, { overwrite: true }); // As overwrite is true a new CSRF token will be generated.
generateCsrfToken(req, res, { overwrite: false }); // As overwrite is false, the existing CSRF token will be reused from the CSRF token cookie
generateCsrfToken(req, res); // same as previous
generateCsrfToken(req, res, { overwrite: false, validateOnReuse: true }); // As validateOnReuse is true, if the CSRF token from the cookie is invalid, a new token will be generated without an error being thrown.
generateCsrfToken(req, res, { overwrite: false, validateOnReuse: true }); // DEPRECATED - As validateOnReuse is true, if the CSRF token from the cookie is invalid, an error will be thrown
```

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

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

<p>The <code>generateCsrfToken</code> function serves the purpose of establishing a CSRF protection mechanism by generating a token and an associated cookie. This function also provides the option to utilise a third parameter, which is an object that may contain: <code>overwrite</code>, <code>validateOnReuse</code>, or <code>cookieOptions</code>. By default, <code>overwrite</code> and <code>validateOnReuse</code> are both set to <em>false</em>. <code>cookieOptions</code> if not provided will just default to the options originally provided to the initialisation configuration, any options that are provided will override those initially provided.</p>
<p>The <code>generateCsrfToken</code> function serves the purpose of establishing a CSRF protection mechanism by generating a token and an associated cookie. This function also provides the option to utilise a third parameter, which is an object that may contain: <code>overwrite</code>, and <code>cookieOptions</code>. By default, <code>overwrite</code> is set to false. <code>cookieOptions</code> if not provided will just default to the options originally provided to the initialisation configuration, any options that are provided will override those initially provided.</p>
<p>It returns a CSRF token and attaches a cookie to the response object. The cookie content is <code>`${hmac}${csrfTokenDelimiter}${randomValue}`</code>.</p>
<p>In some cases you should only transmit your token to the frontend as part of a response payload. Consult the <a href="./FAQ.md#do-i-need-csrf-csrf">"Do I need csrf-csrf?"</a> and <a href="./FAQ.md#does-httponly-have-to-be-true">"Does httpOnly have to be true?"</a> sections of the FAQ.</p>
<p>When <code>overwrite</code> is set to <em>false</em>, the function behaves in a way that preserves the existing CSRF token. In other words, if a valid CSRF token is already present in the incoming request cookie, the function will reuse the existing CSRF token.</p>
<p>If <code>overwrite</code> is set to <em>true</em>, the function will generate a new token and cookie each time it is invoked. This behavior can potentially lead to certain complications, particularly when multiple tabs are being used to interact with your web application. In such scenarios, the creation of new cookies with every call to the function can disrupt the proper functioning of your web app across different tabs, as the changes might not be synchronised effectively (you would need to write your own synchronisation logic).</p>
<p>If overwrite is set to <em>false</em>, the function will also validate the existing cookie information. If the information is found to be invalid, a new token will be generated and returned. If you want an error to be thrown when validation fails during generation you can set the <code>validateOnReuse</code> (by default, <em>false</em>) to <em>true</em>. If it is <em>true</em> then an error will be thrown instead of a new token being generated.</p>
<p>If overwrite is set to <em>false</em>, the function will return the existing CSRF token from the existing CSRF token cookie.</p>

<h3>invalidCsrfTokenError</h3>

Expand Down
18 changes: 6 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,15 @@ export function doubleCsrf({
const possibleSecrets = getPossibleSecrets(req);
// If cookie is not present, this is a new user (no existing csrfToken)
// If ovewrite 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
// 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 overwrite is false and validateOnReuse is true and there is an existing token, validate it first
// If overwrite is false and validateOnReuse is false, just return the cookie value
if (cookieName in req.cookies && !overwrite) {
if (validateCsrfToken(req, possibleSecrets)) {
// If the token is valid, reuse it
if (!validateOnReuse || (validateOnReuse && validateCsrfToken(req, possibleSecrets))) {
// If validateOnReuse is false, or if the token is valid, reuse it
return getCsrfTokenFromCookie(req);
}

if (validateOnReuse) {
// If the pair is invalid, but we want to validate on generation, throw an error
// only if the option is set
throw invalidCsrfTokenError;
}
// This only happens if overwrite is false and validateOnReuse is true
throw invalidCsrfTokenError;
}
// otherwise, generate a completely new token
// the 'newest' or preferred secret is the first one in the array
Expand Down
9 changes: 9 additions & 0 deletions src/tests/testsuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,15 @@ export const createTestSuite: CreateTestsuite = (name, doubleCsrfOptions) => {
expect(newCookieValue).not.toBe(oldCookieValue);
expect(generatedToken).not.toBe(csrfToken);
});

it("should return existing CSRF token for a GET request that does not include the CSRF token", () => {
const { mockRequest, mockResponse, csrfToken } = generateMocksWithTokenInternal();
mockRequest.method = "GET";
mockRequest.headers[HEADER_KEY] = undefined;
const reusedToken = generateCsrfToken(mockRequest, mockResponse);

expect(reusedToken).toBe(csrfToken);
});
});

describe("validateRequest", () => {
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export type CsrfErrorConfig = {
export type CsrfErrorConfigOptions = Partial<CsrfErrorConfig>;
export type GenerateCsrfTokenConfig = {
overwrite: boolean;
/**
* @deprecated leave this as the default value, to be removed in the future
*/
validateOnReuse: boolean;
cookieOptions: CsrfTokenCookieOptions;
};
Expand Down