├── .gitattributes
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .npmignore
├── CHANGELOG.md
├── FAQ.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── UPGRADING.md
├── biome.json
├── example
├── complete
│ ├── README.md
│ ├── package.json
│ └── src
│ │ ├── index.html
│ │ └── index.js
└── react
│ ├── README.md
│ ├── backend
│ ├── Dockerfile
│ ├── package.json
│ ├── src
│ │ ├── config
│ │ │ ├── constants.ts
│ │ │ ├── cors.ts
│ │ │ ├── csrf.ts
│ │ │ ├── helmet.ts
│ │ │ └── session.ts
│ │ ├── features
│ │ │ └── counter
│ │ │ │ ├── middleware.ts
│ │ │ │ ├── router.ts
│ │ │ │ └── types.ts
│ │ ├── index.ts
│ │ └── middleware
│ │ │ └── error-handler.ts
│ └── tsconfig.json
│ ├── client
│ ├── .gitignore
│ ├── Dockerfile
│ ├── index.html
│ ├── package.json
│ ├── public
│ │ └── vite.svg
│ ├── src
│ │ ├── App.css
│ │ ├── App.tsx
│ │ ├── app
│ │ │ └── provider.tsx
│ │ ├── assets
│ │ │ └── react.svg
│ │ ├── components
│ │ │ ├── errors
│ │ │ │ └── GlobalError.tsx
│ │ │ └── ui
│ │ │ │ ├── notifications
│ │ │ │ ├── notification.tsx
│ │ │ │ ├── notifications-store.tsx
│ │ │ │ └── notifications.tsx
│ │ │ │ └── spinner
│ │ │ │ └── Spinner.tsx
│ │ ├── features
│ │ │ └── counter
│ │ │ │ ├── api
│ │ │ │ ├── get-counter.ts
│ │ │ │ ├── increment-counter.ts
│ │ │ │ └── reset-counter.ts
│ │ │ │ └── components
│ │ │ │ ├── Counter.tsx
│ │ │ │ ├── increment-counter-button.tsx
│ │ │ │ └── reset-counter-button.tsx
│ │ ├── index.css
│ │ ├── lib
│ │ │ ├── api-client.ts
│ │ │ ├── api-store.ts
│ │ │ └── react-query.ts
│ │ ├── main.tsx
│ │ ├── types
│ │ │ └── api.ts
│ │ ├── utils
│ │ │ └── cn.ts
│ │ └── vite-env.d.ts
│ ├── tsconfig.json
│ └── vite.config.ts
│ └── docker-compose.yml
├── package-lock.json
├── package.json
├── src
├── index.ts
├── tests
│ ├── doublecsrf.test.ts
│ ├── skipCsrfProtection.test.ts
│ ├── testsuite.ts
│ └── utils
│ │ ├── constants.ts
│ │ ├── helpers.ts
│ │ └── mock.ts
└── types.ts
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ "main", "v[4-9].x.x" ]
6 | pull_request:
7 | branches: [ "main", "v[4-9].x.x" ]
8 |
9 | permissions:
10 | checks: write
11 | contents: write
12 |
13 | jobs:
14 | build:
15 | name: Build
16 | runs-on: ubuntu-latest
17 | env:
18 | EXPECTED_EXPORT_LINE_LENGTH: 528
19 | EXPECTED_FILE_COUNT: 4
20 | steps:
21 | - name: Checkout csrf-csrf
22 | uses: actions/checkout@v4
23 |
24 | - name: Setup Node
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: 22
28 |
29 | - name: Install Dependencies
30 | run: npm ci
31 |
32 | - name: Build
33 | run: npm run build --if-present
34 |
35 | - name: Check number of output files
36 | run: |
37 | output_file_count=$(ls -l ./dist | grep "^-" | wc -l)
38 | if [ "$output_file_count" -ne 4 ]; then
39 | echo "Error: Output file count is $output_file_count, expected 4"
40 | exit 1
41 | else
42 | echo "Output file count is as expected"
43 | fi
44 |
45 | - name: Check export line length for index.d.ts
46 | run: |
47 | export_line=$(tail -n 2 dist/index.d.ts | head -n 2)
48 | export_line_length=${#export_line}
49 | if [ "$export_line_length" -ne $EXPECTED_EXPORT_LINE_LENGTH ]; then
50 | echo "Error: Export line length is $export_line_length, expected $EXPECTED_EXPORT_LINE_LENGTH"
51 | exit 1
52 | fi
53 |
54 | - name: Check export line length for index.d.cts
55 | run: |
56 | export_line=$(tail -n 2 dist/index.d.ts | head -n 2)
57 | export_line_length=${#export_line}
58 | if [ "$export_line_length" -ne $EXPECTED_EXPORT_LINE_LENGTH ]; then
59 | echo "Error: Export line length is $export_line_length, expected $EXPECTED_EXPORT_LINE_LENGTH"
60 | exit 1
61 | fi
62 |
63 | lint:
64 | name: Lint
65 | runs-on: ubuntu-latest
66 |
67 | steps:
68 | - name: Checkout csrf-csrf
69 | uses: actions/checkout@v4
70 |
71 | - name: Setup Node
72 | uses: actions/setup-node@v4
73 | with:
74 | node-version: 22
75 |
76 | - name: Setup Biome CLI
77 | uses: biomejs/setup-biome@v2
78 |
79 | - name: Run Biome
80 | run: biome ci
81 |
82 | test:
83 | name: Test Coverage
84 | runs-on: ubuntu-latest
85 |
86 | steps:
87 | - name: Checkout csrf-csrf
88 | uses: actions/checkout@v4
89 |
90 | - name: Setup Node
91 | uses: actions/setup-node@v4
92 | with:
93 | node-version: 22
94 | cache: 'npm'
95 |
96 | - name: Install Dependencies
97 | run: npm ci
98 |
99 | - name: Run Test Coverage
100 | run: npm run test:coverage -- --silent --coverage.reporter=lcov
101 |
102 | - name: Coveralls
103 | uses: coverallsapp/github-action@v2
104 | with:
105 | path-to-lcov: ./coverage/lcov.info
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | coverage
6 |
7 | # Dependency directories
8 | node_modules/
9 |
10 | # Optional npm cache directory
11 | .npm
12 |
13 | # Output of 'npm pack'
14 | *.tgz
15 |
16 | # vscode
17 | .vscode/
18 | dist/
19 |
20 | # example
21 | example/**/package-lock.json
22 | example/**/yarn.lock
23 |
24 | .vite
25 | .env
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | example
3 | example/**/*
4 | .*
5 | package-lock.json
6 | tsconfig.json
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
4 |
5 | ## [4.0.3](https://github.com/Psifi-Solutions/csrf-csrf/compare/v4.0.2...v4.0.3) (2025-05-27)
6 |
7 | `generateCsrfToken` will now **always** check if the existing token is valid before returning it. This validation is only derived from the request cookie, this way `GET` requests are not expected to include the CSRF token to ensure token reuse, this was a bug and not the intended/expected behavior.
8 |
9 | If the CSRF token container in the request is somehow invalid when `generateCsrfToken` is called, this will be silently ignored and a new valid CSRF token will be generated and returned. If `validateOnReuse` is set to true, an error will be thrown instead.
10 |
11 | ### Bug Fixes
12 |
13 | * validateOnReuse incorrectly throws ([26b3dd6](https://github.com/Psifi-Solutions/csrf-csrf/commit/26b3dd61307ad7588fdc6f20118dfc64fc039f0b))
14 |
15 | ## [4.0.2](https://github.com/Psifi-Solutions/csrf-csrf/compare/v4.0.0...v4.0.2) (2025-05-09)
16 |
17 |
18 | ### Bug Fixes
19 |
20 | * broken type exports ([8967de6](https://github.com/Psifi-Solutions/csrf-csrf/commit/8967de6814045e88caf1a7aa4bb8730e32b4d6d2))
21 |
22 | ## [4.0.1](https://github.com/Psifi-Solutions/csrf-csrf/compare/v4.0.0...v4.0.1) (2025-05-08)
23 |
24 |
25 | ### Bug Fixes
26 |
27 | * correctly skip CSRF token validation when validateOnReuse is false ([bcaf1c3](https://github.com/Psifi-Solutions/csrf-csrf/commit/bcaf1c3f1568cebbfd8d48c2324d5d9b8f3811eb))
28 |
29 | ## [4.0.0](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.7...v4.0.0) (2025-04-27)
30 |
31 |
32 | ### ⚠ BREAKING CHANGES
33 |
34 | This list may not be an exhaustive list of breaking changes, for more information consult the [version 3 -> 4 upgrade guide](./UPGRADING.md#version-3---4) and the updated configuration documentation in the [README](./README.md).
35 |
36 | * Token generation now uses `createHmac`, the format has changed significantly, see the [CSRF token format](./UPGRADING.md#csrf-token-format-has-changed) section of the upgrade guide.
37 | * `getSessionIdentifier` is now required and must return a unique identifier per-request (and per-session) - this is an essential part of CSRF token security
38 | * `getTokenFromRequest` renamed to `getCsrfTokenFromRequest`
39 | * `generateToken` renamed to `generateCsrfToken`
40 | * `overwrite` and `validateOnReuse` parameters for `generateCsrfToken` have been merged into a single object parameter which also accepts `cookieOptions`: `generateCsrfToken(req, res, options);`
41 | * Default value for `validateOnReuse` is now `false`
42 | * Default value for `cookieOptions.sameSite` is now `strict`
43 | * `cookieOptions.signed` is no longer available, CSRF tokens are inherently signed, this is redundant
44 | * `delimiter` option removed, `csrfTokenDelimiter` and `messageDelimiter` are now used for the respective purpose
45 | * `signed` option in `cookieOptions` config option removed (redundant), csrf tokens generated by csrf-csrf are inherently signed
46 | * `size` config option now sets the size of the message used to construct the hmac, now defaults to `32` instead of `64`, this is combined with the return value of `getSessionIdentifier` to construct the hmac payload
47 | * Type `CsrfTokenCookieOverrides` renamed to `CsrfTokenCookieOptions`
48 | * Type `CsrfTokenCreator` renamed to `CsrfTokenGenerator`
49 | * Type `doubleCsrfProtection` renamed to `DoubleCsrfProtection`
50 | * Type `RequestMethod` renamed to `CsrfRequestMethod`
51 | * Type `CsrfIgnoredMethods` renamed to `CsrfIgnoredRequestMethods`
52 |
53 | ### Features
54 |
55 | * change default value of sameSite to 'strict' ([ba5973e](https://github.com/Psifi-Solutions/csrf-csrf/commit/ba5973e44ddf7fdf0baeff038855f7307a5a1cd9))
56 | * change validateOnReuse to false by default ([5fc62a9](https://github.com/Psifi-Solutions/csrf-csrf/commit/5fc62a98b797a7e1bc81a5d98a1c0509e1de4e76))
57 | * expose per token cookie settings ([#60](https://github.com/Psifi-Solutions/csrf-csrf/issues/60)) ([456b317](https://github.com/Psifi-Solutions/csrf-csrf/commit/456b3179eac02deeb90cd7112f7ddbd6377c9758))
58 | * **types:** add CsrfTokenGeneratorRequestUtil type ([72fd659](https://github.com/Psifi-Solutions/csrf-csrf/commit/72fd659f7e8ee9e82e820b1da9c393c4864dc43d))
59 | * use hmac to generate csrf tokens ([e4c5ec3](https://github.com/Psifi-Solutions/csrf-csrf/commit/e4c5ec3ec0dc801ef0fca2ef89e1e4ff79f85aad))
60 |
61 | ### [3.2.2](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.2.1...v3.2.2) (2025-04-24)
62 |
63 |
64 | ### Bug Fixes
65 |
66 | * **types:** fix incorrect type for Request#csrfToken ([cf3dfe2](https://github.com/Psifi-Solutions/csrf-csrf/commit/cf3dfe20ccb14ac4c428dd2897ffe7420295693a)), closes [#95](https://github.com/Psifi-Solutions/csrf-csrf/issues/95)
67 |
68 | ### [3.2.1](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.2.0...v3.2.1) (2025-04-20)
69 |
70 | No changes, just re-published the botched 3.2.0
71 |
72 | ## [3.2.0](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.1.0...v3.2.0) (2025-04-20)
73 |
74 |
75 | ### Features
76 |
77 | * add optional skipCsrfProtection callback config option ([d3f8123](https://github.com/Psifi-Solutions/csrf-csrf/commit/d3f81230353244c937c4c597006f2d6c64a4a671))
78 |
79 | ## [3.1.0](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.7...v3.1.0) (2024-11-25)
80 |
81 |
82 | ### Features
83 |
84 | * re-allow httpOnly override ([e6f2543](https://github.com/Psifi-Solutions/csrf-csrf/commit/e6f25431743ae415a94568db823fd47e9cd90545))
85 | * support custom delimiter for cookie value separation ([59d84a1](https://github.com/Psifi-Solutions/csrf-csrf/commit/59d84a151b4ede65d9a5e859c47de1645c76cfa2))
86 |
87 | ### [3.0.8](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.7...v3.0.8) (2024-09-23)
88 |
89 | * No changes, release issue on 3.0.7
90 |
91 | ## [3.0.7](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.6...v3.0.7) (2024-09-21)
92 |
93 | * Marked >= 3.0.7 as security supported version
94 |
95 | ### Features
96 |
97 | * support optional stateless association of token with session ([710d2f6](https://github.com/Psifi-Solutions/csrf-csrf/commit/710d2f6082f1ac8ab884b10913b1b86195f86bd2))
98 |
99 | Added the `getSessionIdentifier` parameter to the `csrf-csrf` configuration. By providing the `getSessionIdentifier` callback, generated tokens will only be valid for the original session identifier they were generated for.
100 |
101 | For example: (req) => req.session.id
102 |
103 | The token will now be signed with the session id included, this means a generated CSRF token will only be valid for the session it was generated for. This also means that if you rotate your sessions (which you should) you will also need to generate a new CSRF token for the session after rotating it.
104 |
105 | ### [3.0.6](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.5...v3.0.6) (2024-05-17)
106 |
107 | * No changes, just a bump to fix broken release
108 |
109 | ### [3.0.5](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.4...v3.0.5) (2024-05-15)
110 |
111 |
112 | ### Bug Fixes
113 |
114 | * ensure types are correctly exported ([a07ff81](https://github.com/Psifi-Solutions/csrf-csrf/commit/a07ff815724811ae8530886d5d947b2e8112e60c))
115 |
116 | ### [3.0.4](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.3...v3.0.4) (2024-04-03)
117 |
118 |
119 | ### Features
120 |
121 | * allow customizable error ([#58](https://github.com/Psifi-Solutions/csrf-csrf/issues/58)) ([24ec3ab](https://github.com/Psifi-Solutions/csrf-csrf/commit/24ec3abba4911c91b2c37b6ea42acbca10d5d9f6))
122 |
123 | ## [3.0.3](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.1...v3.0.3) (2023-12-16)
124 |
125 |
126 | ### Bug Fixes
127 |
128 | * improve CommonJS TypeScript support ([a9dfbb7](https://github.com/Psifi-Solutions/csrf-csrf/commit/a9dfbb7dd85cafebac68827d9d93a4996163356f))
129 | * remove duplicate string in union type RequestMethod ([4e9f344](https://github.com/Psifi-Solutions/csrf-csrf/commit/4e9f344ea288beaa278c7121248a297bac6ac2a3))
130 |
131 | ### [3.0.2](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.1...v3.0.2) (2023-11-05)
132 |
133 |
134 | ### Features
135 |
136 | * support multiple secrets (backwards compatible) ([51da818](https://github.com/Psifi-Solutions/csrf-csrf/commit/51da818ef1dfb729b894457e4316e028df0b380f))
137 |
138 |
139 | ### Bug Fixes
140 |
141 | * accept validateOnGeneration param in req.csrfToken ([0d6187a](https://github.com/Psifi-Solutions/csrf-csrf/commit/0d6187a9c31ea13b73127774ae6f01bd96baf3dc))
142 | * picking a secret in generateTokenAndHash ([2b4f540](https://github.com/Psifi-Solutions/csrf-csrf/commit/2b4f540bb93e92440a91cc2c53265e96c84a23c1))
143 | * typing in CsrfTokenCreator ([8f4d03f](https://github.com/Psifi-Solutions/csrf-csrf/commit/8f4d03f24adb9f13135c9b847bd87eceb08da1d0))
144 |
145 | ### [3.0.1](https://github.com/Psifi-Solutions/csrf-csrf/compare/v3.0.0...v3.0.1) (2023-09-15)
146 |
147 | ### Bug Fixes
148 |
149 | - types for TypeScript moduleResolution ([#32](https://github.com/Psifi-Solutions/csrf-csrf/issues/32)) ([6a5cd2c](https://github.com/Psifi-Solutions/csrf-csrf/commit/6a5cd2c43e4940577856cc08a565da79c4e1348b))
150 |
151 | ## 3.0.0 (2023-08-18)
152 |
153 | ### ⚠ BREAKING CHANGES
154 |
155 | - Previously csrf-csrf would overwrite any existing token when calling `generateToken` or `req.csrfToken`, this is no longer the case. By default these methods will now return an existing token, making token-per-session the default behaviour. To maintain previous behaviour you will need to set the `overwrite` parameter to true when calling `generateToken` or `req.csrfToken`
156 | - `generateToken` has had the request and response parameters swapped, you will need to update your generateToken invocations to: `generateToken(req, res)`
157 |
158 | ### Features
159 |
160 | - enable per-session token via csrf token reuse ([2f1f8cd](https://github.com/Psifi-Solutions/csrf-csrf/commit/2f1f8cd68e9d74cca38b16f75c4f37c4047d8270))
161 | - swap generateToken request and response parameter order ([54f6c06](https://github.com/Psifi-Solutions/csrf-csrf/commit/54f6c06b975f2c1e32c6c48edaa5bc194b4d6f91))
162 |
--------------------------------------------------------------------------------
/FAQ.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions
2 |
3 | The advice provided here is not exhaustive, `csrf-csrf` does not take any liability for your security choices (or lack thereof). It is your responsibility to ensure you have an accurate threat model of your application(s)/service(s) and are handling it appropriately.
4 |
5 |
6 | ## Questions
7 |
8 | * [What is a CSRF attack?](#what-is-a-csrf-attack)
9 | * [Additional Resources](#additional-resources)
10 | * [Do I need CSRF protection?](#do-i-need-csrf-protection)
11 | * [Do I need csrf-csrf?](#do-i-need-csrf-csrf)
12 | * [Client Side Double-Submit](#client-side-double-submit)
13 | * [Do I need to protect unauthorised routes (e.g. login)?](#do-i-need-to-protect-unauthorised-routes-eg-login)
14 | * [Does httpOnly have to be true?](#does-httponly-have-to-be-true)
15 | * [How to secret?](#how-to-secret)
16 | * [Why is using the cookie in getTokenFromRequest a bad idea?](#why-is-using-the-cookie-in-gettokenfromrequest-a-bad-idea)
17 | * [Dealing with 'ForbiddenError: invalid csrf token'](#dealing-with-forbiddenerror-invalid-csrf-token)
18 | * [Verify the browser is accepting the CSRF cookie](#verify-the-browser-is-accepting-the-csrf-cookie)
19 | * [Verify the browser is sending the CSRF cookie](#verify-the-browser-is-sending-the-csrf-cookie)
20 | * [Verify the backend is accepting the CSRF cookie](#verify-the-backend-is-accepting-the-csrf-cookie)
21 | * [Can't figure it out / still stuck](#cant-figure-it-out--still-stuck)
22 |
23 | ---
24 | ### What is a CSRF attack?
25 |
26 | When a cookie is used for authentication/authorisation, any request a browser makes to the domain the cookie is set on, **traditionally** the cookie is included in the request by default, regardless of where the request comes from. The intention of a CSRF attack is to trick a users browser into making a request with some side effect, the request will automatically be considered authorised.
27 |
28 | The purpose of CSRF protection is to help determine whether a request was legitimately intended and made by the authorised user, thus rejecting requests made by such malicious means.
29 |
30 | #### Additional Resources
31 |
32 | * [OWASP CSRF Protection Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
33 | * [CSRF Protection Course by PortSwigger](https://portswigger.net/web-security/csrf#what-is-csrf)
34 | * [What is considered a cross-site request?](https://web.dev/articles/same-site-same-origin)
35 |
36 |
37 | ---
38 | ### Do I need CSRF protection?
39 |
40 | If you are using a cookie for your authentication/authorisation then you may require CSRF protection. Note that, auth related cookies should be `httpOnly`, if you are using a JWT via a cookie, it should be `httpOnly`, and in this case, even though you are not using sessions, you may still need CSRF protection.
41 |
42 | If you can guarantee all of the following things, then you can skip CSRF protection:
43 |
44 | * You only support modern/evergreen browsers
45 | * You do not use traditional form submits
46 | * Traditional form submits do not trigger CORs preflight checks and can be submitted cross-origin by default, they should have CSRF protection
47 | * Your auth related cookies have the `sameSite` attribute set to `strict` or `lax`
48 | * You have tight and explicit CORs configuration
49 | * This means your allowed origins are explicitly configured in your CORs configuration and your backend will only accept incoming cookies from the domains you have specified
50 |
51 | If you answered no to any of the above points you likely need CSRF protection, or if you are unsure it could be best to have CSRF protection just in case, the overhead is negligible.
52 |
53 |
54 | ---
55 | ### Do I need csrf-csrf?
56 |
57 | If you are using session based authentication and you have server side state you should use [csrf-sync](https://github.com/Psifi-Solutions/csrf-sync) instead, note that if you are using `express-session` then you have server side state.
58 |
59 | If you are using a JWT as a `httpOnly` cookie stateless sessions, or some other kind of `httpOnly` stateles authentication/authorisation, then you will need to use `csrf-csrf` if you answered yes to ["Do I need CSRF protection?](#do-i-need-csrf-protection)
60 |
61 | #### Client Side Double-Submit
62 |
63 | If your backend is only serving frontends which are not cross-site, you can use client side CSRF protection without `csrf-csrf`. The client generates a cryptographically pseudorandom value, the client sets this value as a cookie on the domain, the client also includes the value in a custom header (or form payload). The backend then verifies the values match, you may want to verify the other cookie attributes are as expected, such as the `path` and `sameSite`. The idea here is, attackers using CSRF attacks are unable to set cookies on your domain.
64 |
65 |
66 | ---
67 | ### Do I need to protect unauthorised routes (e.g. login)?
68 |
69 | If you are using session based authentication this usually means you generate a session for all of your users, regardless of whether they have logged in or not. Since you have anonymous sessions, the session already exists before they have logged in. In this case the login route, forgot password route, and any other such routes should be protected.
70 |
71 | If you are using a JWT then your "session" does not exist until _after_ the user logs in, therefore these non-authorised routes do not need CSRF protection.
72 |
73 | If you are using session based authentication but you are not creating anonymous sessions, and sessions only exist for logged-in users, you can also skip protecting non-authorised routes.
74 |
75 | If you are using OAuth2 to identify your users, do ensure the `state` parameter is used appropriately. If you're using OAuth2 with an SPA and no backend, then you must use the PKCE flow, if you're using OAuth2 with an SPA that has a backend, prioritise using the Authorisation Code Grant flow.
76 |
77 |
78 | ---
79 | ### Does httpOnly have to be true?
80 |
81 | This is a question that seems to get a bit controversial, I have found that this is because the usecases have evolved a lot overtime. Personally I agree with most that it is not necessary, however there is some argument to say otherwise. If you have a usecase where it is fine for you to set `httpOnly` to `false`, this may mean you fall into the "No" group for [Do I need CSRF protection?](#do-i-need-csrf-protection), or it may mean you can just use the double submit pattern from the client side as described under [Client Side Double-Submit](#client-side-double-submit).
82 |
83 | If you have a usecase where you do need CSRF protection and can't use the client side approach because your API is serving cross-site clients, then you may want to leave `httpOnly` as `true`.
84 |
85 | Frameworks like Django and Laravel default `httpOnly` on their CSRF protection to `false` because these frameworks primarily use Server Side Rendering (SSR) by default. SSR generally means that frontend requests will not be cross-site and that the `sameSite` attribute can be set to `strict`. They allow the configuration for the cases that may require it.
86 |
87 | [This article from Otka](https://developer.okta.com/blog/2022/07/08/spa-web-security-csrf-xss#validate-requests-for-authenticity-to-mitigate-csrf) recommends CSRF tokens be retrieved by an explicit endpoint when it comes to Single Page Applications (SPAs), in which case `httpOnly` is fine, you do not need the cookie to be accessible. Keep in mind doing this requires the CSRF token to be explicitly tied to the session/identifier it is generated for. Additionally there is a [discussion here](https://github.com/OWASP/CheatSheetSeries/pull/1634#discussion_r2008986056) where an OWASP maintainer has also recommended `httpOnly`. Additionally there was [one case discussed where Twitter had a vulnerable subdomain](https://github.com/Psifi-Solutions/csrf-csrf/issues/41#issuecomment-1856438994), which led to attacks on the primary domain which could have been mitigated had they set the CSRF token to `httpOnly`.
88 |
89 | In the end the decision is yours, it is up to you to understand the requirements, constraints, and threat model of your application.
90 |
91 |
92 | ---
93 | ## How to secret?
94 |
95 | Refer to the [OWASP Secrets Management](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) for creation, storage, and processing of secrets. Also refer to the [secret reccomendations from `fastify/csrf-protection`](https://github.com/fastify/csrf-protection#securing-the-secret).
96 |
97 | Keep in mind that you should not use `dotenv` in production unless you have explicitly followed their production recommendations to use `dotenvx`. It should otherwise only be used for development purposes and should only be required on the command line via the dev related commands.
98 |
99 | Environment variables should typically be actual user scoped environment variables on the host system. You should generally have a user for the sole purpose of running your application where that user only has explicit bare minimum permissions required for running the application. Sensitive environment configuration should be handled via a secrets manager/store/vault.
100 |
101 |
102 | ---
103 | ## Why is using the cookie in `getTokenFromRequest` a bad idea?
104 |
105 | Consider doing something like this in `getTokenFromRequest`:
106 |
107 | ```js
108 | // NEVER DO THIS
109 | (req) => req.cookies['csrf-token']
110 | // NEVER DO THIS
111 | ```
112 |
113 | Where `csrf-token` is the name of the cookie where the CSRF token is stored. **This is the same as having no CSRF protection at all.** The cookie is automatically included in the request, this means requests made via CSRF attacks may now be accepted. The whole point of the double-submit cookie pattern is that the CSRF token is submitted twice, once as a cookie, and once by some other means.
114 |
115 | `getTokenFromRequest` should always explicitly return the value exactly from where it expects it to be, and this should be from either a custom header or from the request payload. Example:
116 |
117 | ```js
118 | (req) => {
119 | if (req.is('application/x-www-form-urlencoded')) {
120 | // where _csrf is the name of a hidden field on the form
121 | // or is processed as such via the FormData
122 | return req.body._csrf;
123 | }
124 |
125 | return req.headers['x-csrf-token'];
126 | }
127 | ```
128 |
129 | You need to be careful with `getTokenFromRequest` because it is this part of the protection that `csurf` got wrong and was deprecated for.
130 |
131 |
132 | ---
133 | ## Dealing with 'ForbiddenError: invalid csrf token'
134 |
135 | In some cases you may find that your CSRF protection is working locally but it is not working in production when you deploy, in some cases it might be working via something like postman, but not with your frontend. Whether the issue is during local development, staging, or production, the primary reason for CSRF protection is likely due to incorrect CORs configuration or an incorrect `sameSite` attribute value for your usecase.
136 |
137 | ### Verify the browser is accepting the CSRF cookie
138 |
139 | With your browsers dev tools open and the `Network` tab selected, initiate the request where you are expecting the backend to set the CSRF cookie. Find the request in the list, select it, in the details that pop up, take a look at the **response** headers. There should be a `Set-Cookie` header and it should include the CSRF token.
140 |
141 | There are three possibilities:
142 |
143 | 1. The CSRF token is there but there is a little triangle warning icon. If you hover this icon it will give you some indication as to why the browser has rejected the cookie, it is on you to fix your configuration.
144 | 2. The CSRF token is there and there are no warnings. This means the browser has accepted the cookie and it has been set.
145 | 3. The CSRF token is not there. This means the backend did not generate a CSRF token as part of the request, did not send the cookie out on the response, or you may be looking at the wrong request. Consider debugging to understand what is happening.
146 |
147 | ### Verify the browser is sending the CSRF cookie
148 |
149 | If the browser has accepted the cookie and you are still receiving CSRF errors this may mean the browser is not sending the cookie. If your frontend is considered cross-site and you are using `axios` you will need to set `withCredentials: true` in your axios config. If you are using fetch you will need to ensure that the `credentials` option is set to `same-origin` or `include` as appropriate for your usecase, refer to [Using the Fecth API#including_credentials](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#including_credentials).
150 |
151 | If you are using some other fetch based library you may need to consult the relevant documentation.
152 |
153 | ### Verify the backend is accepting the CSRF cookie
154 |
155 | If the browser is sending the cookie with requests and you are still receiving CSRF errors then the next step is to verify whether the backend is accepting the cookie.
156 |
157 | The primary reason the backend may reject a cookie is due to incorrect/improper CORs configuration. You need to ensure that the origins allowed via your CORs configuration is permitting the origin of your frontend. Usually this configuration would be done by environment variables, it is on you to ensure the configuration is as expected.
158 |
159 | ### Can't figure it out / still stuck
160 |
161 | If you are unsure of the previous steps or you are still receiving CSRF errors, the best way to figure it out is to run through the code via your debugger. Using a debugger is an incredibly powerful skill, and is an absolute 100% way to figure out what is going wrong.
162 |
163 | TODO: provide a video example
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022, Psifi Solutions
2 |
3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4 |
5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
6 |
7 | Source: http://opensource.org/licenses/ISC
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Double CSRF
3 |
4 |
5 |
A utility package to help implement stateless CSRF (Cross-Site Request Forgery) protection using the Double Submit Cookie Pattern in express.
33 | This module provides the necessary pieces required to implement CSRF protection using the Double Submit Cookie Pattern. This is a stateless CSRF protection pattern, if you are using sessions it is highly recommended that you use csrf-sync for the Synchroniser Token Pattern instead.
34 |
35 |
36 |
37 | Since csurf has been deprecated I struggled to find alternative solutions that were accurately implemented and configurable, so I decided to write my own! Thanks to NextAuth as I referenced their implementation. From experience CSRF protection libraries tend to complicate their configuration, and if misconfigured, can render the protection completely useless.
38 |
39 |
40 |
41 | This is why csrf-csrf aims to provide a simple and targeted implementation to simplify its use.
42 |
43 |
44 |
45 |
Getting Started
46 |
47 |
Version 4 is now live! If you are upgrading from version 3 check the changelog, the upgrade guide, and the updated configuration documentation below.
48 |
49 |
50 | Before getting started with csrf-csrf you should consult the FAQ and determine whether you need CSRF protection and whether csrf-csrf is the right choice.
51 |
52 |
53 | This section will guide you through using the default setup, which sufficiently implements the Double Submit Cookie Pattern. If you would like to customise the configuration, see the configuration section.
54 |
55 |
56 | You will need to have the cookie-parser middleware registered before the doubleCsrfProtection middleware. If you are using express-session then it is also best to register the cookie-parser middleware after that.
57 |
58 |
This utility will set a cookie containing a hmac based CSRF token, the frontend should include this CSRF token in an appropriate request header or in the body. The getTokenFromRequest option should strictly return the CSRF token from either a request header, or the request body. If you have cases where you need to consider both make these as explicit as possible to avoid the same vulnerability as csurf, do not just use fallthroughs (||, ??).
59 |
60 |
If you are using TypeScript, requires TypeScript >= 3.8
61 |
62 | ```
63 | npm install cookie-parser csrf-csrf
64 | ```
65 |
66 | ```js
67 | // ESM
68 | import { doubleCsrf } from "csrf-csrf";
69 |
70 | // CommonJS
71 | const { doubleCsrf } = require("csrf-csrf");
72 | ```
73 |
74 | ```js
75 | const {
76 | invalidCsrfTokenError, // This is just for convenience if you plan on making your own middleware.
77 | generateCsrfToken, // Use this in your routes to provide a CSRF token.
78 | validateRequest, // Also a convenience if you plan on making your own middleware.
79 | doubleCsrfProtection, // This is the default CSRF protection middleware.
80 | } = doubleCsrf({
81 | getSecret = (req) => 'return some cryptographically pseudorandom secret here',
82 | getSessionIdentifier = (req) => req.session.id // return the requests unique identifier
83 | });
84 | ```
85 |
86 |
87 | This will extract the default utilities, you can configure these and re-export them from your own module. Primarily csrf-csrf was built to support Single Page Applications (SPAs), or frontends hosted cross-site to their backend, the default configuration is optimised for this usecase. If you are changing any of the configuration you should ensure you understand the impact of the change. Consult the documentation for the respective configuration option and also consider reading the FAQ.
88 |
89 |
90 |
91 | To create a route which generates a CSRF token and a cookie containing the CSRF token:
92 |
93 |
94 | ```js
95 | const myRoute = (req, res) => {
96 | const csrfToken = generateCsrfToken(req, res);
97 | // You could also pass the token into the context of a HTML response.
98 | res.json({ csrfToken });
99 | };
100 | const myProtectedRoute = (req, res) =>
101 | res.json({ unpopularOpinion: "Game of Thrones was amazing" });
102 | ```
103 |
104 |
Instead of importing and using generateCsrfToken, you can also use req.csrfToken any time after the doubleCsrfProtection middleware has executed on a request.
105 |
106 | ```js
107 | request.csrfToken(); // same as generateCsrfToken(req, res);
108 | ```
109 |
110 |
111 | You can also put the token into the context of a templated HTML response.
112 |
113 |
114 | ```js
115 | // Make sure your session middleware is registered before these
116 | express.use(session);
117 | express.use(cookieParser);
118 | express.get("/csrf-token", myRoute);
119 | express.use(doubleCsrfProtection);
120 | // Any non GET routes registered after this will be considered "protected"
121 | ```
122 |
123 |
124 | By default, any requests that are not GET, HEAD, or OPTIONS methods will be protected. You can configure this with the ignoredMethods option. Keep in mind that only requests with side effects need to be protected, it is generally bad practice to have side effects from GET requests.
125 |
126 |
127 |
128 | You can also protect routes on a case-to-case basis:
129 |
136 | Once a route is protected, you will need to ensure the CSRF token cookie is sent along with the request and by default you will need to include the CSRF token in the x-csrf-token header, otherwise you will receive a `403 - ForbiddenError: invalid csrf token`. If your cookie is not being included in your requests be sure to check your withCredentials, CORs configuration, and ensure appropriate sameSite configuration for your use case. For additional information on figuring out the error see the "Dealing with 'ForbiddenError: invalid csrf token'" section of the FAQ.
137 |
138 |
139 |
Sessions
140 |
141 |
Once again, if you are using sessions, you should be using csrf-sync instead. Sessions have server side state by default.
142 |
143 |
If you do plan on using express-session with csrf-csrf then ensure your cookie-parser middleware is registered afterexpress-session, as express-session parses its own cookies and may conflict.
144 |
145 |
Using asynchronously
146 |
147 |
csrf-csrf itself will not support promises or async, however there is a way around this. If your CSRF token is stored externally and needs to be retrieved asynchronously, you can register an asynchronous middleware first, which exposes the token.
167 |
168 | When initialising doubleCsrf, you have a lot of options available for configuration, the only required options are getSecret and getSessionIdentifier, the rest have sensible defaults (shown below).
169 |
170 | ```js
171 | const doubleCsrfUtilities = doubleCsrf({
172 | getSecret: () => "Secret", // A function that optionally takes the request and returns a secret
173 | getSessionIdentifier: (req) => req.session.id, // A function that returns the unique identifier for the request
174 | cookieName: "__Host-psifi.x-csrf-token", // The name of the cookie to be used, recommend using Host prefix.
175 | cookieOptions: {
176 | sameSite = "strict",
177 | path = "/",
178 | secure = true,
179 | httpOnly = true,
180 | ...remainingCookieOptions // See cookieOptions below
181 | },
182 | size: 32, // The size of the random value used to construct the message used for hmac generation
183 | ignoredMethods: ["GET", "HEAD", "OPTIONS"], // A list of request methods that will not be protected.
184 | getCsrfTokenFromRequest: (req) => req.headers["x-csrf-token"], // A function that returns the token from the request
185 | skipCsrfProtection: undefined
186 | });
187 | ```
188 |
189 |
198 | This should return a secret key or an array of secret keys to be used for hmac generation. Secret keys should be cryptographically pseudorandomly generated. You should make sure you use a strong and secure secret key. See the "How to secret?" section of the FAQ.
199 |
200 |
In case multiple are provided, the first one will be used for hashing. For validation, all secrets will be tried, preferring the first one in the array. Having multiple valid secrets can be useful when you need to rotate secrets, but you do not want to invalidate the previous secret (which might still be used by some users) right away.
This function should return the unique identifier for the incoming request, typically this would be the session id or JWT. The unique identifier should be something that is different each time it is constructed for the same user. The return value is used as part of the message to generate the hmac, it ensures that generated CSRF tokens can only work for the matching identifier that originally requested them.
212 |
213 |
If you are rotating your sessions (which you should be), you will need to ensure a new CSRF token is generated at the same time. This should typically be done when a session has some sort of authorisation elevation (e.g. signed in, signed out, sudo). If you're using a JWT and you aren't using it as a cookie, you likely don't need CSRF protection, check the Do I need CSRF protection?" section of the FAQ.
The name of the cookie that will be used to track the CSRF token. If you change this it is recommend that you continue to use the __Host- or __Secure-security prefix for production.
227 |
228 |
Change for development
229 |
230 |
The security prefix requires the secure flag to be true and requires requests to be received via HTTPS, unless you have your local instance running via HTTPS, you will need to change this value in your development environment.
The options provided to the cookie, see cookie attributes. If you plan on changing the httpOnly to false, see the "Does httpOnly have to be true" section of the FAQ. The remaining options are all undefined by default and consist of:
The csrfTokenDelimiter is used to concatenate the hmac and the random value to construct the CSRF token. The random value portion is used to reconstruct the hmac during validation and helps prevent collisions.
This function should return the token sent by the frontend, either in the request body/payload, or from the x-csrf-token header. Do NOT return the value from the cookie in this function, this would be the same as having no csrf protection at all, see the "Why is using the cookie in getTokenFromRequest a bad idea?" section of the FAQ.
An array of request methods that the doubleCsrfProtection middleware will ignore, requests made matching these request methods will not be protected. It is recommended you leave this as the default. If you have GET requests with side effects and need those protected, consider route based protection and/or making use of skipCsrfProtection by skipping for all GET requests except for those that need it.
362 |
363 | ```ts
364 | {
365 | statusCode: 403,
366 | message: "invalid csrf token",
367 | code: "EBADCSRFTOKEN"
368 | }
369 | ```
370 |
371 | Used to customise the error response statusCode, the contained error message, and its code, the error is constructed via createHttpError. The default values match that of csurf for convenience.
372 |
373 |
Used to determine whether CSRF protection should be skipped for the given request. If this callback is provided and the request is not in the ignoredMethods, then the callback will be called to determine whether or not CSRF token validation should be checked. If it returns true the CSRF protection will be skipped, if it returns false then CSRF protection will be checked.
382 |
383 |
* It is primarily provided to avoid the need of wrapping the doubleCsrfProtection 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 only 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 NOT a solution for CSRF errors.
384 |
385 |
Utilities
386 |
387 |
Below is the documentation for what doubleCsrf returns.
The middleware used to actually protect your routes, see the getting started examples above, or the examples included in the repository.
396 |
397 |
generateCsrfToken
398 |
399 | ```ts
400 | (
401 | request: Request,
402 | response: Response,
403 | {
404 | cookieOptions?: CookieOptions, // allows overriding of cookieOptions
405 | overwrite?: boolean, // Set to true to force a new token to be generated
406 | validateOnReuse?: boolean, // Deprecated, leave as default
407 | } // optional
408 | ) => string;
409 | ```
410 |
411 |
By default if a csrf-csrf cookie already exists on an incoming request, generateCsrfToken will not overwrite it, it will return the existing token so long as the token is valid. If you wish to force a token generation, you can use the overwrite option of the third parameter:
412 |
413 |
The validateOnReuse parameter is a bit misleading, and is also deprecated (will be removed with the next major release). A better name for it would be throwOnReuseInvalid.
414 |
415 | ```ts
416 | 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
417 | ```
418 |
419 |
If the overwrite parameter is set to false (default), the existing token will be re-used and returned.
420 |
421 |
If overwrite is true a new token will always be generated, even if the current one is invalid.
422 |
423 | ```ts
424 | generateCsrfToken(req, res, { overwrite: true }); // As overwrite is true a new CSRF token will be generated.
425 | generateCsrfToken(req, res, { overwrite: false }); // As overwrite is false, the existing CSRF token will be reused from the CSRF token cookie
426 | generateCsrfToken(req, res); // same as previous
427 | 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
428 | ```
429 |
430 |
Instead of importing and using generateCsrfToken, you can also use req.csrfToken any time after the doubleCsrfProtection middleware has executed on your incoming request.
The generateCsrfToken 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: overwrite, and cookieOptions. By default, overwrite is set to false. cookieOptions 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.
439 |
It returns a CSRF token and attaches a cookie to the response object. The cookie content is `${hmac}${csrfTokenDelimiter}${randomValue}`.
When overwrite is set to false, 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.
442 |
If overwrite is set to true, 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).
443 |
If overwrite is set to false, the function will return the existing CSRF token from the existing CSRF token cookie.
444 |
445 |
invalidCsrfTokenError
446 |
447 |
This is the error instance that gets passed as an error to the next call, by default this will be handled by the default error handler. This error is customisable via the errorConfig.
This function is used by the doubleCsrfProtection middleware to determine whether an incoming request has a valid CSRF token. You can use this to make your own custom middleware (not recommended).
456 |
457 |
Support
458 |
459 |
460 |
461 | Join the Discord and ask for help in the psifi-support channel.
462 |
463 |
464 | Pledge your support through the Patreon
465 |
466 |
467 |
468 |
469 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | | -------- | ------------------ |
7 | | >= 3.0.7 | :white_check_mark: |
8 | | < 3.0.7 | :x: |
9 |
10 | ## Reporting a Vulnerability
11 |
12 | If you have security concerns, please join the [Psifi Solutions Discord](https://psifisolutions.com/discord) and tag me `Psi#2741` in the `#psifi-support` channel advising you have security concerns you wish to discuss.
13 |
14 | Please note that even if I am online on Discord, it does not mean I will be immediately available, I will respond ASAP.
15 |
--------------------------------------------------------------------------------
/UPGRADING.md:
--------------------------------------------------------------------------------
1 | # Upgrading
2 |
3 | This file intends to document and capture any complicated and breaking changes between major versions.
4 |
5 | ## Version 3 -> 4
6 |
7 | Version 4 comes with a variety of breaking changes that you may need to address in your codebase when upgrading. This intends to be a comprehensive guide on upgrading from v3 to v4, if you find anything that is not documented please raise an issue or a pull request.
8 |
9 | ### CSRF token format has changed
10 |
11 | CSRF tokens are now generated using `createHmac` instead of `createHash`. Here is how CSRF token generation works from v4:
12 |
13 | 1. A `randomValue` is generated via `randomBytes`
14 | 2. A `message` is constructed in the format `[uniqueIdentifier.length, uniqueIdentifier, randomValue.length, randomValue].join(messageDelimiter)` where:
15 | - `randomValue` is from step 1; and
16 | - `uniqueIdentifier` is the value derived from the configured `getSessionIdentifier`; and
17 | - `messageDelimiter` is the new `messageDelimiter` configuration option
18 | 3. A hmac is generated from the `message` via `createHmac` using the secret returned by `getSecret` at the time of generation
19 | 4. The CSRF token takes the format `${hmac}${csrfTokenDelimiter}${randomValue}` where:
20 | - `hmac` is the value from step 3; and
21 | - `csrfTokenDelimiter` is the new `csrfTokenDelimiter` configuration option; and
22 | - `randomValue` is from step 1
23 | 5. The value returned by `generateCsrfToken` or `req.csrfToken` will now be the same, the new construction means the tokens validity can be better confirmed without needing to separately track a hash and unhashed version of the CSRF token.
24 |
25 | ### Zero Downtime Upgrade (ZDU)
26 |
27 | Tokens previously generated with version 3 will not be compatible once you upgrade to version 4. You may need to consider clearing existing CSRF tokens or forcing new CSRF token generation.
28 |
29 |
30 | ### Configuration changes
31 |
32 | #### delimiter -> csrfTokenDelimiter
33 |
34 | The `delimiter` configuration option has been renamed to `csrfTokenDelimiter` and is now used to separate the `hmac` and the `randomValue` to form the CSRF token. This defaults to `.`.
35 |
36 | #### getSessionIdentifier (now required)
37 |
38 | The `getSessionIdentifier` configuration option is now required and it has to return the unique identifier associated with the request. This should be the session identifier if you are using sessions, the JWT if you're using JWT, or whatever other unique value you are using to identify requests. The value returned should be unique per user, per session. That is if a user logs in, then they logout, then they login again, the identifier should be different.
39 |
40 | If you do not use `getSessionIdentifier` correctly, or you force it to return an empty string, you could be compromising your security.
41 |
42 | #### getTokenFromRequest -> getCsrfTokenFromRequest
43 |
44 | The `getTokenFromRequest` configuration option has been renamed to `getCsrfTokenFromRequest`.
45 |
46 | ```js
47 | // before
48 | const { doubleCsrfProtection } = doubleCsrf({
49 | getSecret: (req) => "...",
50 | getTokenFromRequest: (req) => req.headers['x-csrf-token']
51 | });
52 | // after
53 | const { doubleCsrfProtection } = doubleCsrf({
54 | getSecret: (req) => "...",
55 | getSessionIdentifier: (req) => req.session.id,
56 | getCsrfTokenFromRequest: (req) => req.headers['x-csrf-token']
57 | });
58 |
59 | // before
60 | const { doubleCsrfProtection } = doubleCsrf({
61 | getSecret: (req) => "...",
62 | getTokenFromRequest: function (req) {
63 | return req.headers['x-csrf-token'];
64 | }
65 | });
66 | // after
67 | const { doubleCsrfProtection } = doubleCsrf({
68 | getSecret: (req) => "...",
69 | getSessionIdentifier: (req) => req.session.id,
70 | getTokenFromRequest: function (req) {
71 | return req.headers['x-csrf-token'];
72 | }
73 | });
74 | ```
75 |
76 | #### hmacAlgorithm (new)
77 |
78 | The `hmacAlgorithm` configuration option is new and is passed into the `createHmac` call to specify the algorithm to use. This defaults to `sha256`.
79 |
80 | #### messageDelimiter (new)
81 |
82 | The `messageDelimiter` configuration option is new and is used to separate the values in the message used to construct the hmac. This defaults to `|`.
83 |
84 | #### signed (removed)
85 |
86 | The `signed` configuration option from the `cookieOptions` has been removed and is no longer available, this option is a redundant overhead for `csrf-csrf`. Tokens generated by `csrf-csrf` are inherently signed already. There is no benefit to signing it twice.
87 |
88 | #### size
89 |
90 | The `size` configuration option now sets the size of the `randomValue` used to construct the hmac. It now defaults to 32 instead of 64.
91 |
92 | ### Utility Changes
93 |
94 | #### generateToken -> generateCsrfToken
95 |
96 | The `generateToken` function returned by `doubleCsrf` has been renamed to `generateCsrfToken` and has had it's paramters updated. The last two parameters `overwrite` and `validateOnReuse` have been merged into a single object parameter.
97 |
98 | The default value for `validateOnReuse` has been changed to false. If you were previously dependent on this being true you will need to manually set it.
99 |
100 | Additionally the third object parameter also accepts `cookieOptions` to override cookie options at generation time.
101 |
102 | As the `Request#csrfToken` utility delegates directly to the `generateCsrfToken` function, it has also changed, examples of necessary changes below.
103 |
104 | ```js
105 | // before
106 | const { generateToken, doubleCsrfProtection } = doubleCsrf({ ... });
107 | //after
108 | const { generateCsrfToken, doubleCsrfProtection } = doubleCsrf({ ... });
109 |
110 | // before
111 | generateToken(req, res);
112 | // after
113 | generateCsrfToken(req, res, { validateOnReuse: true });
114 | // before
115 | generateToken(req, res, true);
116 | // after
117 | generateCsrfToken(req, res, { overwrite: true, validateOnReuse: true });
118 |
119 | // before
120 | generateToken(req, res, true, false);
121 | // after
122 | generateCsrfToken(req, res, { overwrite: true });
123 |
124 | // before
125 | req.csrfToken(false, false);
126 | // after
127 | req.csrfToken();
128 |
129 | // before
130 | req.csrfToken(true);
131 | // after
132 | req.csrfToken({ overwrite: true, validateOnReuse: true });
133 |
134 | // before
135 | req.csrfToken(true, false);
136 | // after
137 | req.csrfToken({ overwrite: true });
138 |
139 | // before
140 | req.csrfToken(true, true);
141 | // after
142 | req.csrfToken({ overwrite: true, validateOnReuse: true });
143 | ```
144 |
145 | ### Typescript
146 |
147 | This is not an exhaustive list of type changes, only those that are not implicit based on other documented changes.
148 |
149 | * Type `CsrfTokenCookieOverrides` renamed to `CsrfTokenCookieOptions`
150 | * Type `CsrfTokenCreator` renamed to `CsrfTokenGenerator`
151 | * Type `doubleCsrfProtection` renamed to `DoubleCsrfProtection`
152 | * Type `RequestMethod` renamed to `CsrfRequestMethod`
153 | * Type `CsrfIgnoredMethods` renamed to `CsrfIgnoredRequestMethods`
154 | * Type `CsrfTokenGeneratorRequestUtil` added for `Request#csrfToken`
155 |
156 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
3 | "files": {
4 | "ignore": ["coverage", "dist", "node_modules", "package-lock.json"]
5 | },
6 | "formatter": {
7 | "enabled": true,
8 | "formatWithErrors": false,
9 | "indentStyle": "space",
10 | "indentWidth": 2,
11 | "lineEnding": "lf",
12 | "lineWidth": 120
13 | },
14 | "organizeImports": { "enabled": true },
15 | "linter": {
16 | "enabled": true,
17 | "rules": {
18 | "recommended": true
19 | }
20 | },
21 | "javascript": {
22 | "formatter": {
23 | "semicolons": "always"
24 | }
25 | },
26 | "overrides": [
27 | {
28 | "include": ["src/tests", "example/"],
29 | "linter": {
30 | "rules": {
31 | "suspicious": {
32 | "noExplicitAny": "off"
33 | }
34 | }
35 | }
36 | },
37 | {
38 | "include": ["src/tests"],
39 | "linter": {
40 | "rules": {
41 | "suspicious": {
42 | "noExportsInTest": "off"
43 | },
44 | "style": {
45 | "noNonNullAssertion": "off"
46 | }
47 | }
48 | }
49 | }
50 | ]
51 | }
52 |
--------------------------------------------------------------------------------
/example/complete/README.md:
--------------------------------------------------------------------------------
1 | ## Complete Example Using `csrf-csrf` library
2 |
3 | This example is a case of using the `csrf-csrf` library on the client and server sides. Use npm or yarn to install the dependencies.
4 |
5 | ## Execution
6 |
7 | ```
8 | npm run start
9 | ```
10 |
11 | > **Note**: To change the request behavior, try to play with the client side (the `index.html` file)
12 |
--------------------------------------------------------------------------------
/example/complete/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "csrf-csrf-complete-example",
3 | "version": "1.0.0",
4 | "description": "A complete example of the csrf-csrf utility package.",
5 | "main": "src/index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "start": "node ./src/index.js"
10 | },
11 | "author": "Mauricio M. (cr0wg4n)",
12 | "license": "ISC",
13 | "dependencies": {
14 | "cookie-parser": "^1.4.6",
15 | "csrf-csrf": "4.0.2",
16 | "express": "^4.19.2",
17 | "express-session": "1.18.1"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/example/complete/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Anti-CSRF Example
8 |
9 |
10 |
11 |
12 |
35 |
36 |
--------------------------------------------------------------------------------
/example/complete/src/index.js:
--------------------------------------------------------------------------------
1 | import cookieParser from "cookie-parser";
2 | import { doubleCsrf } from "csrf-csrf";
3 | import express from "express";
4 | import session from "express-session";
5 |
6 | import path from "node:path";
7 | import { fileURLToPath } from "node:url";
8 | const __filename = fileURLToPath(import.meta.url);
9 | const __dirname = path.dirname(__filename);
10 |
11 | // Secrets and important params are usually set as environment variables
12 | // in this case you can set and change this values for testing purposes
13 | const PORT = 3000;
14 | const CSRF_SECRET = "super csrf secret";
15 | const COOKIES_SECRET = "super cookie secret";
16 | const SESSION_SECRET = "stupid session secret";
17 |
18 | const app = express();
19 | app.use(express.json());
20 | app.use(
21 | session({
22 | secret: SESSION_SECRET,
23 | resave: false,
24 | saveUninitialized: true,
25 | // maxAge is 1 hour in ms
26 | cookie: { secure: false, sameSite: "lax", signed: true, maxAge: 3.6e6 },
27 | // No session store configured is bad, this is not representative of a production config
28 | }),
29 | );
30 |
31 | // The cookie secret isn't needed for csrf-csrf, but is needed if you want to use
32 | // cookie-parser to set signed cookies
33 | app.use(cookieParser(COOKIES_SECRET));
34 |
35 | // These settings are only for local development testing.
36 | // Do not use these in production.
37 | // In production, ensure you're using cors and helmet and have proper configuration.
38 | const { invalidCsrfTokenError, generateCsrfToken, doubleCsrfProtection } = doubleCsrf({
39 | getSecret: () => CSRF_SECRET,
40 | getSessionIdentifier: (req) => req.session.id,
41 | cookieName: "xsrf_token",
42 | cookieOptions: { sameSite: "strict", secure: false },
43 | });
44 |
45 | // Error handling, validation error interception
46 | const csrfErrorHandler = (error, req, res, next) => {
47 | if (error === invalidCsrfTokenError) {
48 | res.status(403).json({
49 | error: "csrf validation error",
50 | });
51 | } else {
52 | next();
53 | }
54 | };
55 |
56 | // Check out the index.html file to change parameters to the client requests
57 | app.get("/", (req, res) => {
58 | res.sendFile(path.join(__dirname, "index.html"));
59 | });
60 |
61 | app.get("/csrf-token", (req, res) => {
62 | return res.json({
63 | token: generateCsrfToken(req, res),
64 | });
65 | });
66 |
67 | app.post("/protected_endpoint", doubleCsrfProtection, csrfErrorHandler, (req, res) => {
68 | console.log(req.body);
69 | res.json({
70 | protected_endpoint: "form processed successfully",
71 | });
72 | });
73 |
74 | // Try with a HTTP client (is not protected from a CSRF attack)
75 | app.post("/unprotected_endpoint", (req, res) => {
76 | console.log(req.body);
77 | res.json({
78 | unprotected_endpoint: "form processed successfully",
79 | });
80 | });
81 |
82 | app.listen(PORT, () => {
83 | // Open in your browser
84 | console.log(`listen on http://127.0.0.1:${PORT}`);
85 | });
86 |
--------------------------------------------------------------------------------
/example/react/README.md:
--------------------------------------------------------------------------------
1 | # React Example
2 |
3 | This example intends to be a generic example of how to use `csrf-csrf`. Whilst the example is React based the backend configuration is catered to serving a Single Page Application (SPA), or any client which is being independently hosted of the API. The example assumes that the frontend is hosted cross-site to the backend API. In a case where the frontend is not hosted cross-site to the backend you would want to ensure the CSRF cookie has `sameSite` set to `strict`.
4 |
5 | If you aren't sure whether requests from your frontend to your backend are considered cross-site, check the ["What is considered a cross-site request?"](../../FAQ.md#additional-resources) in the FAQ.
6 |
7 | For this particular example it would actually be better to use `csrf-sync` instead, however this is for demonstrative purposes of `csrf-csrf`.
8 |
9 | The React app attempts to take on and follow the principles of [bulletproof-react](https://github.com/alan2207/bulletproof-react/tree/master/apps/react-vite) but does not do so exhaustively as it is just an example, the backend attempts to translate the same principles.
10 |
11 | ## Running the example
12 |
13 | ### With Docker
14 |
15 | The example will make use of the below ports, so make sure these ports are available, otherwise you can change them in the `docker-compose.yml` configuration.
16 |
17 | * 3700 for the frontend client (react)
18 | * 3710 for the backend API (express port)
19 | * 3779 for redis (6379 on the container) (this is only needed if you want to run the backend API locally instead of within docker)
20 | * 9229 for remote debugging of the backend
21 |
22 | In `backend` run:
23 |
24 | ```bash
25 | npm install
26 | npm run build
27 | ```
28 |
29 | Then from this directory (`example/react`) run:
30 |
31 | ```bash
32 | docker compose up -d
33 | ```
34 | Once the containers are up and running, you should find the React app at http://localhost:3700/
35 |
36 | Tear it down with
37 |
38 | ```bash
39 | docker compose down
40 | ```
41 | from the same directory.
42 |
43 | #### Docker Watch Mode
44 |
45 | If you want to make changes to the backend and have them update automatically whilst running with Docker, make sure to run `npm run watch` within `backend` from a terminal. Changes will be applied without needing to rebuild or restart the container.
46 |
47 | For the client, the Vite watch mode is enabled by default. Any changes made to the client will be replicated in the container, hot reloading will work as expected.
48 |
49 | ### Without Docker
50 |
51 | By default Docker is much easier, however you can run without Docker.
52 |
53 | 1. Run `npm install` in both `backend` and `client`
54 | 2. Create a `.env` file in `backend` and populate it appropriately:
55 |
56 | ```
57 | EXAMPLE_CSRF_SECRET=Fake CSRF secret
58 | EXAMPLE_ALLOWED_ORIGINS="http://localhost:3700"
59 | EXAMPLE_API_PORT=3710
60 | EXAMPLE_SESSION_SECRET=Fake session secret
61 | EXAMPLE_REDIS_HOST=localhost
62 | EXAMPLE_REDIS_PORT=3779
63 | NODE_ENV=development
64 | ```
65 | 3. Create a `.env` file in `client` and populate it appropriately:
66 |
67 | ```
68 | VITE_EXAMPLE_BASE_API_URL=http://localhost:3710
69 | ```
70 | 4. Make sure you have a working `redis` instance and that it's configured appropriately in the above environment files
71 | * You could run via Docker first (as above) and then stop the `csrf-client` and `csrf-backend` containers, leaving the `csrf-redis` container.
72 | * Alternatively give the `redis` service a docker-compose profile and only run that.
73 | 5. Run `npm run dev` in `backend` from one terminal
74 | 6. Run `npm run dev` in `client` from another terminal
75 |
76 | ### Development
77 |
78 | For local development of the example you will want to run `npm install` under both of the `client` and the `backend`. Once you have the containers running, if you want or need to install a new dependency, you'll need to re-run `npm install` on the container as well. You can do this by connecting to the container and running `npm install` from the `/app` directory
79 |
80 | ```bash
81 | docker exec -it csrf-backend sh
82 | docker exec -it csrf-client sh
83 | ```
84 |
85 |
--------------------------------------------------------------------------------
/example/react/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-alpine
2 | WORKDIR /app
3 | COPY . .
4 | RUN npm install
5 | RUN npm run build
6 | # Installing nodemon on the container to not pollute host
7 | RUN npm install -g nodemon
8 | EXPOSE 4000
9 | EXPOSE 9229
10 | # We need to run legacy watch mode for it to work on the container via polling for Windows host
11 | CMD ["nodemon", "-L", "--watch dist", "--inspect=0.0.0.0", "dist/server.js"]
--------------------------------------------------------------------------------
/example/react/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "csrf-csrf-react-backend",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "scripts": {
6 | "build": "tsc -p tsconfig.json",
7 | "watch": "tsc --watch",
8 | "dev": "tsx -r dotenv/config --watch src/index.ts",
9 | "start": "node dist/index.js"
10 | },
11 | "author": "psibean",
12 | "license": "ISC",
13 | "description": "",
14 | "devDependencies": {
15 | "@types/cookie-parser": "1.4.8",
16 | "@types/cors": "2.8.17",
17 | "@types/express": "5.0.1",
18 | "@types/express-session": "1.18.1",
19 | "dotenv": "^16.5.0",
20 | "tsx": "4.19.3",
21 | "typescript": "5.8.3"
22 | },
23 | "dependencies": {
24 | "connect-redis": "8.0.3",
25 | "cookie-parser": "1.4.7",
26 | "cors": "2.8.5",
27 | "csrf-csrf": "4.0.2",
28 | "ejs": "3.1.10",
29 | "express": "5.1.0",
30 | "express-session": "1.18.1",
31 | "helmet": "8.1.0",
32 | "ioredis": "5.6.1"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/example/react/backend/src/config/constants.ts:
--------------------------------------------------------------------------------
1 | // Used for environment variable exports
2 | export const EXAMPLE_API_PORT = Number(process.env.EXAMPLE_API_PORT);
3 | // You shouldn't really do this forced casting, instead you should check if the environment
4 | // variables are defined, and if they aren't throw an error to prevent starting up a system
5 | // that is misconfigured. Or have some alternative but handled approach.
6 | export const EXAMPLE_ALLOWED_ORIGINS = (process.env.EXAMPLE_ALLOWED_ORIGINS as string).split(",");
7 | export const EXAMPLE_SESSION_SECRET = (process.env.EXAMPLE_SESSION_SECRET as string) ?? "assdafasdf";
8 | export const EXAMPLE_CSRF_SECRET = (process.env.EXAMPLE_CSRF_SECRET as string) ?? "sdfgvsarg35g345";
9 | export const EXAMPLE_REDIS_HOST = process.env.EXAMPLE_REDIS_HOST;
10 | export const EXAMPLE_REDIS_PORT = Number(process.env.EXAMPLE_REDIS_PORT);
11 | export const IS_PRODUCTION = process.env.NODE_ENV !== "development";
12 |
--------------------------------------------------------------------------------
/example/react/backend/src/config/cors.ts:
--------------------------------------------------------------------------------
1 | import expressCors from "cors";
2 | import { EXAMPLE_ALLOWED_ORIGINS } from "./constants.js";
3 |
4 | console.log(`Configuring cors with origin: '${EXAMPLE_ALLOWED_ORIGINS}'`);
5 |
6 | const cors = expressCors({
7 | origin: EXAMPLE_ALLOWED_ORIGINS,
8 | credentials: true,
9 | });
10 |
11 | export default cors;
12 |
--------------------------------------------------------------------------------
/example/react/backend/src/config/csrf.ts:
--------------------------------------------------------------------------------
1 | import { doubleCsrf } from "csrf-csrf";
2 | import { EXAMPLE_CSRF_SECRET, IS_PRODUCTION } from "./constants.js";
3 |
4 | /*
5 | * This configuration is for the React SPA.
6 | * It is assumed the React SPA is going to be hosted cross-site from the backend API.
7 | * If the React SPA was not being hosted cross-site, or was being served directly by the express
8 | * app (via static files), then we would want to leave the cookie as strict and we would want to
9 | * ensure the cookieName has a secure prefix in production.
10 | *
11 | * Please note that with the default options secure is set to true in this configuration
12 | */
13 | export const { doubleCsrfProtection, invalidCsrfTokenError, generateCsrfToken } = doubleCsrf({
14 | getSecret: () => EXAMPLE_CSRF_SECRET,
15 | getSessionIdentifier: (req) => {
16 | // If you were using a JWT as a httpOnly cookie, you would return that here instead
17 | return req.session.id;
18 | },
19 | cookieOptions: { sameSite: "lax" },
20 | // You always want to prefer a __Host- or __Secure- prefix in production
21 | cookieName: IS_PRODUCTION ? "__Host-xsrf-token" : "xsrf-token",
22 | });
23 |
--------------------------------------------------------------------------------
/example/react/backend/src/config/helmet.ts:
--------------------------------------------------------------------------------
1 | import expressHelmet from "helmet";
2 |
3 | const helmet = expressHelmet();
4 |
5 | export default helmet;
6 |
--------------------------------------------------------------------------------
/example/react/backend/src/config/session.ts:
--------------------------------------------------------------------------------
1 | import { RedisStore } from "connect-redis";
2 | import expressSession from "express-session";
3 | import { Redis } from "ioredis";
4 | import { EXAMPLE_REDIS_HOST, EXAMPLE_REDIS_PORT, EXAMPLE_SESSION_SECRET } from "./constants.js";
5 |
6 | console.log(`Configuring redis store on ${EXAMPLE_REDIS_HOST}:${EXAMPLE_REDIS_PORT}`);
7 | const redis = new Redis({
8 | host: EXAMPLE_REDIS_HOST,
9 | port: EXAMPLE_REDIS_PORT,
10 | });
11 |
12 | // For the sake of this example, there is no username or password.
13 | // In a real environment you would want to ensure credentials are also configured.
14 | // If you aren't already using redis, or you're already using some other database.
15 | // I would recommend using the respective store.
16 | // Redis is mostly used here for convenience and to maintain state when server restarts.
17 | const redisStore = new RedisStore({
18 | client: redis,
19 | prefix: "csrf-example-session:",
20 | });
21 |
22 | const session = expressSession({
23 | secret: EXAMPLE_SESSION_SECRET,
24 | resave: false,
25 | saveUninitialized: true,
26 | // maxAge is 1 hour in ms
27 | cookie: { secure: false, sameSite: "lax", signed: true, maxAge: 3.6e6 },
28 | store: redisStore,
29 | });
30 |
31 | export default session;
32 |
--------------------------------------------------------------------------------
/example/react/backend/src/features/counter/middleware.ts:
--------------------------------------------------------------------------------
1 | import type { NextFunction, Request, Response } from "express";
2 |
3 | export const ensureCounter = (req: Request, res: Response, next: NextFunction) => {
4 | // It's not good to "create" data in a middleware like this this should be explicitly handled in a post request
5 | // in this context and for the sake of this example it is okay to do in this particular way
6 | if (typeof req.session?.counter !== "number") {
7 | req.session.counter = 0;
8 | }
9 | next();
10 | };
11 |
--------------------------------------------------------------------------------
/example/react/backend/src/features/counter/router.ts:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import { ensureCounter } from "./middleware.js";
3 |
4 | const COUNTER_BASE_PATH = "/counter";
5 |
6 | const counterRouter = Router();
7 |
8 | counterRouter.use(ensureCounter);
9 |
10 | counterRouter.get(COUNTER_BASE_PATH, (req, res) => {
11 | res.status(200).json({ counter: req.session.counter });
12 | });
13 |
14 | counterRouter.put(COUNTER_BASE_PATH, (req, res) => {
15 | const currentCounter = req.session.counter as number;
16 | req.session.counter = currentCounter + 1;
17 | res.status(200).json({ counter: req.session.counter });
18 | });
19 |
20 | counterRouter.delete(COUNTER_BASE_PATH, (req, res) => {
21 | req.session.counter = 0;
22 | res.status(204).json({ counter: req.session.counter });
23 | });
24 |
25 | export default counterRouter;
26 |
--------------------------------------------------------------------------------
/example/react/backend/src/features/counter/types.ts:
--------------------------------------------------------------------------------
1 | declare module "express-session" {
2 | interface SessionData {
3 | counter?: number;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/example/react/backend/src/index.ts:
--------------------------------------------------------------------------------
1 | import cookieParser from "cookie-parser";
2 | import type { CsrfTokenGeneratorRequestUtil } from "csrf-csrf";
3 | import Express from "express";
4 | import { EXAMPLE_API_PORT } from "./config/constants.js";
5 | import cors from "./config/cors.js";
6 | import { doubleCsrfProtection, generateCsrfToken } from "./config/csrf.js";
7 | import helmet from "./config/helmet.js";
8 | import session from "./config/session.js";
9 | import counterRouter from "./features/counter/router.js";
10 | import errorHandler from "./middleware/error-handler.js";
11 |
12 | const app = Express();
13 |
14 | app.use(helmet);
15 | app.use(cors);
16 | app.use(session);
17 | // We aren't using a cookie secret because we don't have any need for signed cookies
18 | // If you have other cookies that do need to be signed, be sure to provide a unique secret
19 | app.use(cookieParser());
20 | app.use(doubleCsrfProtection);
21 |
22 | // You might not want to start processing any payloads until after the above protection based middlewares
23 | app.use(Express.json());
24 |
25 | app.get("/csrf-token", (req, res) => {
26 | const csrfToken = generateCsrfToken(req, res, { validateOnReuse: false });
27 | res.status(200).json({ csrfToken });
28 | });
29 |
30 | app.get("/csrf-token-util", (req, res) => {
31 | // This is just a demonstration doing the same thing as the previous route
32 | // The type casting here is "safe" as we know this is guaranteed to be after the doubleCsrfProtection middleware
33 | const csrfToken = (req.csrfToken as CsrfTokenGeneratorRequestUtil)();
34 | res.status(200).json({ csrfToken });
35 | });
36 |
37 | app.use("/", counterRouter);
38 | // Register the custom global error handler last
39 | app.use(errorHandler);
40 |
41 | app.listen({ port: EXAMPLE_API_PORT }, () => {
42 | console.log(`Server is listening at http://localhost:${EXAMPLE_API_PORT}`);
43 | });
44 |
--------------------------------------------------------------------------------
/example/react/backend/src/middleware/error-handler.ts:
--------------------------------------------------------------------------------
1 | import type { NextFunction, Request, Response } from "express";
2 | import { invalidCsrfTokenError } from "../config/csrf.js";
3 |
4 | export default (error: any, req: Request, res: Response, next: NextFunction) => {
5 | if (error === invalidCsrfTokenError) {
6 | res.status(error.statusCode).json(error);
7 | } else {
8 | next(error);
9 | }
10 | };
11 |
--------------------------------------------------------------------------------
/example/react/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Projects */
6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
12 |
13 | /* Language and Environment */
14 | "target": "ES2023" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
16 | // "jsx": "preserve", /* Specify what JSX code is generated. */
17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
26 |
27 | /* Modules */
28 | "module": "NodeNext" /* Specify what module code is generated. */,
29 | "rootDir": "./src" /* Specify the root folder within your source files. */,
30 | "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */,
31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34 | "typeRoots": ["node_modules/@types"] /* Specify multiple folders that act like './node_modules/@types'. */,
35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
42 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
43 | // "resolveJsonModule": true, /* Enable importing .json files. */
44 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
45 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
46 |
47 | /* JavaScript Support */
48 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
49 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
50 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
51 |
52 | /* Emit */
53 | "declaration": false /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
54 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
55 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
56 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
57 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
58 | // "noEmit": true, /* Disable emitting files from a compilation. */
59 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
60 | "outDir": "./dist" /* Specify an output folder for all emitted files. */,
61 | "removeComments": true /* Disable emitting comments. */,
62 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
68 | // "newLine": "crlf", /* Set the newline character for emitting files. */
69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
74 |
75 | /* Interop Constraints */
76 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
77 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
78 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
83 |
84 | /* Type Checking */
85 | "strict": true /* Enable all strict type-checking options. */,
86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
91 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
92 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
93 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
94 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
95 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
96 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
97 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
98 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
99 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
100 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
101 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
102 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
103 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
104 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
105 |
106 | /* Completeness */
107 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
108 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/example/react/client/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/example/react/client/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:22-slim
2 | WORKDIR /app
3 | COPY . ./
4 | RUN npm install
5 | EXPOSE 3000
6 | CMD ["npm", "run", "dev"]
--------------------------------------------------------------------------------
/example/react/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | csrf-csrf example
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/example/react/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "csrf-csrf-react-client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@tanstack/react-query": "5.74.4",
13 | "clsx": "2.1.1",
14 | "lucide-react": "0.503.0",
15 | "nanoid": "5.1.5",
16 | "react": "^19.0.0",
17 | "react-dom": "^19.0.0",
18 | "react-error-boundary": "5.0.0",
19 | "tailwind-merge": "3.2.0",
20 | "zustand": "5.0.3"
21 | },
22 | "devDependencies": {
23 | "@eslint/js": "^9.22.0",
24 | "@tailwindcss/vite": "4.1.4",
25 | "@tanstack/react-query-devtools": "5.74.4",
26 | "@types/react": "^19.0.10",
27 | "@types/react-dom": "^19.0.4",
28 | "@vitejs/plugin-react-swc": "^3.8.0",
29 | "globals": "^16.0.0",
30 | "tailwindcss": "4.1.4",
31 | "typescript": "~5.7.2",
32 | "vite": "^6.3.1",
33 | "vite-tsconfig-paths": "5.1.4"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/example/react/client/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/react/client/src/App.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | #root {
4 | margin: 0 auto;
5 | }
6 |
7 | .logo {
8 | height: 6em;
9 | padding: 1.5em;
10 | will-change: filter;
11 | transition: filter 300ms;
12 | }
13 | .logo:hover {
14 | filter: drop-shadow(0 0 2em #646cffaa);
15 | }
16 | .logo.react:hover {
17 | filter: drop-shadow(0 0 2em #61dafbaa);
18 | }
19 |
20 | @keyframes logo-spin {
21 | from {
22 | transform: rotate(0deg);
23 | }
24 | to {
25 | transform: rotate(360deg);
26 | }
27 | }
28 |
29 | @media (prefers-reduced-motion: no-preference) {
30 | a:nth-of-type(2) .logo {
31 | animation: logo-spin infinite 20s linear;
32 | }
33 | }
34 |
35 | .card {
36 | padding: 2em;
37 | }
38 |
--------------------------------------------------------------------------------
/example/react/client/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import viteLogo from "/vite.svg";
3 | import reactLogo from "./assets/react.svg";
4 | import "./App.css";
5 | import { useErrorBoundary } from "react-error-boundary";
6 | import Spinner from "./components/ui/spinner/Spinner";
7 | import Counter from "./features/counter/components/counter";
8 | import { useApi } from "./lib/api-store";
9 |
10 | function App() {
11 | const { csrfToken, initialiseCsrfToken } = useApi();
12 | const { showBoundary } = useErrorBoundary();
13 |
14 | useEffect(() => {
15 | if (!csrfToken) {
16 | initialiseCsrfToken(showBoundary);
17 | }
18 | }, [csrfToken, initialiseCsrfToken, showBoundary]);
19 |
20 | if (!csrfToken) {
21 | return ;
22 | }
23 |
24 | return (
25 | <>
26 |