Skip to content
Open
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
86 changes: 86 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,92 @@ Used to customise the error response <code>statusCode</code>, the contained erro

<p>* It is primarily provided to avoid the need of wrapping the <code>doubleCsrfProtection</code> middleware in your own middleware, allowing you to apply a global logic as to whether or not CSRF protection should be executed based on the incoming request. You should <b>only</b> skip CSRF protection for cases you are 100% certain it is safe to do so, for example, requests you have identified as coming from a native app. You should ensure you are not introducing any vulnerabilities that would allow your web based app to circumvent the protection via CSRF attacks. This option is <b>NOT</b> a solution for CSRF errors.</p>


<h3 id="csrf-logger">logger</h3>

```ts
(logArgs) => void
```

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

<p>This function can be used to receive and handle internal logging from <code>csrf-csrf</code>, <code>csrf-csrf</code> will invoke this callback at different points throughout CSRF token generation and CSRF token validation to help you capture what is happening. The below documentation covers the log types and their respective arguments.</p>

<p>All of the logs will contain the <code>{ request: Request }</code> so this will be omitted from the below.</p>

<h4>CSRF_TOKEN_CONTENT_INVALID</h4>

```ts
{
logType: "CSRF_TOKEN_CONTENT_INVALID";
isReceivedHmacAString: boolean;
isRandomValueAString: boolean;
isRandomValueEmpty: boolean;
}
```
<p>If this log event is called it means that <code>getCsrfTokenFromRequest</code> and <code>getCsrfTokenFromCookie</code> are returning non-empty strings and the values are equal. However a <code>hmac</code> and <code>randomValue</code> could not be extracted from the CSRF token, this indicates that the format of the CSRF token is incorrect. You can use the provided arguments to infer what is wrong.<p>

<h4>CSRF_TOKEN_GENERATED</h4>

```ts
{
logType: "CSRF_TOKEN_GENERATED";
cookieOptions: CsrfTokenCookieOptions;
generatedNewToken: boolean;
overwrite: boolean;
validateOnReuse: boolean;
}
```
<p>This log event is called whenever the <code>generateCsrfToken</code> is invoked, either directly or via <code>req.csrfToken</code>, and successfully returns a CSRF token.</p>

<h4>CSRF_TOKEN_INVALID</h4>

```ts
{
logType: "CSRF_TOKEN_INVALID"
}
```

<p>This log event is called when the values from <code>getCsrfTokenFromRequest</code> and <code>getCsrfTokenFromCookie</code> are valid and a <code>hmac</code> and <code>randomValue</code> could be successfully extracted, however CSRF token validation is unable to successfully verify the received CSRF token with any of the secrets returned via <code>getSecret</code>.

<h4>CSRF_TOKEN_INVALID_REUSE</h4>

```ts
{
logType: "CSRF_TOKEN_INVALID_REUSE";
overwrite: boolean;
validateOnReuse: boolean;
}
```

<p>This log event is called when <code>generateCsrfToken</code> is called with <code>{ overwrite: false, validateOnReuse: true }</code> and the existing CSRF token is not considered valid. You would usually expect one of the other logs to be fired before this one to indicate why the validation may have failed.</p>

<h4>CSRF_TOKEN_MISSING</h4>

```ts
{
logType: "CSRF_TOKEN_MISSING";
isCsrfTokenFromCookieAString: boolean;
isCsrfTokenFromRequestAString: boolean;
}
```

<p>This log event is called when either <code>getCsrfTokenFromRequest</code> and/or <code>getCsrfTokenFromCookie</code> returns <code>null</code>, or <code>undefined</code>.</p>

<h4>REQUEST_CONTENT_INVALID</h4>

```ts
logType: "REQUEST_CONTENT_INVALID";
isCsrfTokenFromCookieEmpty: boolean;
isCsrfTokenFromRequestEmpty: boolean;
isCsrfTokenEqual: boolean;
```

<p>This log event is called when either <code>getCsrfTokenFromRequest</code> and/or <code>getCsrfTokenFromCookie</code> return an empty string, or when their return values are not equal.</p>

<h2 id="utilities">Utilities</h2>

<p>Below is the documentation for what doubleCsrf returns.</p>
Expand Down
8 changes: 8 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const CSRF_LOG_EVENTS = {
CSRF_TOKEN_CONTENT_INVALID: "CSRF_TOKEN_CONTENT_INVALID",
CSRF_TOKEN_GENERATED: "CSRF_TOKEN_GENERATED",
CSRF_TOKEN_INVALID: "CSRF_TOKEN_INVALID",
CSRF_TOKEN_INVALID_REUSE: "CSRF_TOKEN_INVALID_REUSE",
CSRF_TOKEN_MISSING: "CSRF_TOKEN_MISSING",
REQUEST_CONTENT_INVALID: "REQUEST_CONTENT_INVALID",
} as const;
78 changes: 66 additions & 12 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Request, Response } from "express";
import createHttpError from "http-errors";

import type {
CsrfLoggerArgs,
CsrfRequestMethod,
CsrfRequestValidator,
CsrfTokenGenerator,
Expand All @@ -13,8 +14,8 @@ import type {
GenerateCsrfTokenConfig,
GenerateCsrfTokenOptions,
} from "./types";

export * from "./types";
export { CSRF_LOG_EVENTS } from "./constants";
import { CSRF_LOG_EVENTS } from "./constants";

export function doubleCsrf({
getSecret,
Expand All @@ -29,6 +30,7 @@ export function doubleCsrf({
getCsrfTokenFromRequest = (req) => req.headers["x-csrf-token"],
errorConfig: { statusCode = 403, message = "invalid csrf token", code = "EBADCSRFTOKEN" } = {},
skipCsrfProtection,
logger,
}: DoubleCsrfConfigOptions): DoubleCsrfUtilities {
const ignoredMethodsSet = new Set(ignoredMethods);
const defaultCookieOptions = {
Expand All @@ -49,6 +51,12 @@ export function doubleCsrf({
code: code,
});

const internalLogger = (logArgs: CsrfLoggerArgs) => {
if (logger) {
logger(logArgs);
}
};

const constructMessage = (req: Request, randomValue: string) => {
const uniqueIdentifier = getSessionIdentifier(req);
const messageValues = [uniqueIdentifier.length, uniqueIdentifier, randomValue.length, randomValue];
Expand Down Expand Up @@ -78,10 +86,11 @@ export function doubleCsrf({
if (cookieName in req.cookies && !overwrite) {
if (validateCsrfToken(req, possibleSecrets)) {
// If the token is valid, reuse it
return getCsrfTokenFromCookie(req);
return { csrfToken: getCsrfTokenFromCookie(req), generatedNewToken: false };
}

if (validateOnReuse) {
internalLogger({ logType: CSRF_LOG_EVENTS.CSRF_TOKEN_INVALID_REUSE, request: req, overwrite, validateOnReuse });
// If the pair is invalid, but we want to validate on generation, throw an error
// only if the option is set
throw invalidCsrfTokenError;
Expand All @@ -95,7 +104,7 @@ export function doubleCsrf({
const hmac = generateHmac(secret, message);
const csrfToken = `${hmac}${csrfTokenDelimiter}${randomValue}`;

return csrfToken;
return { csrfToken, generatedNewToken: true };
};

// Generates a token, sets the cookie on the response and returns the token.
Expand All @@ -107,13 +116,22 @@ export function doubleCsrf({
res: Response,
{ cookieOptions = defaultCookieOptions, overwrite = false, validateOnReuse = false } = {},
) => {
const csrfToken = generateCsrfTokenInternal(req, {
const parsedCookieOptions = {
...defaultCookieOptions,
...cookieOptions,
};
const { csrfToken, generatedNewToken } = generateCsrfTokenInternal(req, {
overwrite,
validateOnReuse,
});
res.cookie(cookieName, csrfToken, {
...defaultCookieOptions,
...cookieOptions,
res.cookie(cookieName, csrfToken, parsedCookieOptions);
internalLogger({
logType: CSRF_LOG_EVENTS.CSRF_TOKEN_GENERATED,
cookieOptions,
generatedNewToken,
request: req,
overwrite,
validateOnReuse,
});
return csrfToken;
};
Expand All @@ -124,15 +142,50 @@ export function doubleCsrf({
const validateCsrfToken: CsrfTokenValidator = (req, possibleSecrets) => {
const csrfTokenFromCookie = getCsrfTokenFromCookie(req);
const csrfTokenFromRequest = getCsrfTokenFromRequest(req);
const isCsrfTokenFromCookieAString = typeof csrfTokenFromCookie === "string";
const isCsrfTokenFromRequestAString = typeof csrfTokenFromRequest === "string";

if (!(isCsrfTokenFromCookieAString && isCsrfTokenFromRequestAString)) {
internalLogger({
logType: CSRF_LOG_EVENTS.CSRF_TOKEN_MISSING,
request: req,
isCsrfTokenFromCookieAString,
isCsrfTokenFromRequestAString,
});
return false;
}

if (typeof csrfTokenFromCookie !== "string" || typeof csrfTokenFromRequest !== "string") return false;

if (csrfTokenFromCookie === "" || csrfTokenFromRequest === "" || csrfTokenFromCookie !== csrfTokenFromRequest)
const isCsrfTokenFromCookieEmpty = csrfTokenFromCookie === "";
const isCsrfTokenFromRequestEmpty = csrfTokenFromRequest === "";
const isCsrfTokenEqual = csrfTokenFromCookie === csrfTokenFromRequest;

if (isCsrfTokenFromCookieEmpty || isCsrfTokenFromRequestEmpty || !isCsrfTokenEqual) {
internalLogger({
logType: CSRF_LOG_EVENTS.REQUEST_CONTENT_INVALID,
request: req,
isCsrfTokenFromCookieEmpty,
isCsrfTokenFromRequestEmpty,
isCsrfTokenEqual,
});
return false;
}

const [receivedHmac, randomValue] = csrfTokenFromCookie.split(csrfTokenDelimiter);

if (typeof receivedHmac !== "string" || typeof randomValue !== "string" || randomValue === "") return false;
const isReceivedHmacAString = typeof receivedHmac === "string";
const isRandomValueAString = typeof randomValue === "string";
const isRandomValueEmpty = randomValue === "";

if (!(isReceivedHmacAString && isRandomValueAString) || isRandomValueEmpty) {
internalLogger({
logType: CSRF_LOG_EVENTS.CSRF_TOKEN_CONTENT_INVALID,
request: req,
isReceivedHmacAString,
isRandomValueAString,
isRandomValueEmpty,
});
return false;
}

// The reason it's safe for us to only validate the hmac and random value from the cookie here
// is because we've already checked above whether the token in the cookie and the token provided
Expand All @@ -143,6 +196,7 @@ export function doubleCsrf({
if (receivedHmac === hmacForSecret) return true;
}

internalLogger({ logType: CSRF_LOG_EVENTS.CSRF_TOKEN_INVALID, request: req });
return false;
};

Expand Down
43 changes: 43 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CookieOptions, NextFunction, Request, Response } from "express";
import type { HttpError } from "http-errors";
import type { CSRF_LOG_EVENTS } from "./constants";

export type SameSiteType = boolean | "lax" | "strict" | "none";
export type TokenRetriever = (req: Request) => string | null | undefined;
Expand Down Expand Up @@ -41,6 +42,46 @@ export type GenerateCsrfTokenConfig = {
cookieOptions: CsrfTokenCookieOptions;
};
export type GenerateCsrfTokenOptions = Partial<GenerateCsrfTokenConfig>;
export type CsrfTokenGeneratedLogArgs = {
logType: typeof CSRF_LOG_EVENTS.CSRF_TOKEN_GENERATED;
cookieOptions: CsrfTokenCookieOptions;
generatedNewToken: boolean;
overwrite: boolean;
validateOnReuse: boolean;
};
export type CsrfTokenInvalidLogArgs = { logType: typeof CSRF_LOG_EVENTS.CSRF_TOKEN_INVALID };
export type CsrfRequestContentInvalidLogArgs = {
logType: typeof CSRF_LOG_EVENTS.REQUEST_CONTENT_INVALID;
isCsrfTokenFromCookieEmpty: boolean;
isCsrfTokenFromRequestEmpty: boolean;
isCsrfTokenEqual: boolean;
};
export type CsrfTokenContentInvaldLogArgs = {
logType: typeof CSRF_LOG_EVENTS.CSRF_TOKEN_CONTENT_INVALID;
isReceivedHmacAString: boolean;
isRandomValueAString: boolean;
isRandomValueEmpty: boolean;
};
export type CsrfTokenInvalidReuseLogArgs = {
logType: typeof CSRF_LOG_EVENTS.CSRF_TOKEN_INVALID_REUSE;
overwrite: boolean;
validateOnReuse: boolean;
};
export type CsrfTokenMissingLogArgs = {
logType: typeof CSRF_LOG_EVENTS.CSRF_TOKEN_MISSING;
isCsrfTokenFromCookieAString: boolean;
isCsrfTokenFromRequestAString: boolean;
};
export type CsrfLoggerArgs = { request: Request } & (
| CsrfTokenGeneratedLogArgs
| CsrfTokenInvalidLogArgs
| CsrfRequestContentInvalidLogArgs
| CsrfTokenContentInvaldLogArgs
| CsrfTokenInvalidReuseLogArgs
| CsrfTokenMissingLogArgs
);
export type CsrfLogger = (logArgs: CsrfLoggerArgs) => void;

export interface DoubleCsrfConfig {
/**
* A function that returns a secret or an array of secrets.
Expand Down Expand Up @@ -160,6 +201,8 @@ export interface DoubleCsrfConfig {
* ```
*/
skipCsrfProtection: (req: Request) => boolean;

logger?: CsrfLogger;
}

export interface DoubleCsrfUtilities {
Expand Down
Loading