Skip to content

Commit 52b2dce

Browse files
authored
chore(examples): update examples for v4 compatibility
* Delete useless simple example * Refactor "complete" example to work with v4 API * Add a superior and more complete example using React
1 parent 039bb19 commit 52b2dce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1342
-60
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ dist/
1919

2020
# example
2121
example/**/package-lock.json
22-
example/**/yarn.lock
22+
example/**/yarn.lock
23+
24+
.vite
25+
.env

biome.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,21 @@
2424
}
2525
},
2626
"overrides": [
27+
{
28+
"include": ["src/tests", "example/"],
29+
"linter": {
30+
"rules": {
31+
"suspicious": {
32+
"noExplicitAny": "off"
33+
}
34+
}
35+
}
36+
},
2737
{
2838
"include": ["src/tests"],
2939
"linter": {
3040
"rules": {
3141
"suspicious": {
32-
"noExplicitAny": "off",
3342
"noExportsInTest": "off"
3443
},
3544
"style": {

example/complete/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"license": "ISC",
1313
"dependencies": {
1414
"cookie-parser": "^1.4.6",
15-
"csrf-csrf": "file:../..",
16-
"express": "^4.19.2"
15+
"csrf-csrf": "file:../../csrf-csrf-3.2.0.tgz",
16+
"express": "^4.19.2",
17+
"express-session": "1.18.1"
1718
}
1819
}

example/complete/src/index.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import cookieParser from "cookie-parser";
22
import { doubleCsrf } from "csrf-csrf";
33
import express from "express";
4+
import session from "express-session";
45

56
import path from "node:path";
67
import { fileURLToPath } from "node:url";
@@ -12,22 +13,35 @@ const __dirname = path.dirname(__filename);
1213
const PORT = 3000;
1314
const CSRF_SECRET = "super csrf secret";
1415
const COOKIES_SECRET = "super cookie secret";
15-
const CSRF_COOKIE_NAME = "x-csrf-token";
16+
const SESSION_SECRET = "stupid session secret";
1617

1718
const app = express();
1819
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));
1934

2035
// These settings are only for local development testing.
2136
// Do not use these in production.
2237
// In production, ensure you're using cors and helmet and have proper configuration.
2338
const { invalidCsrfTokenError, generateCsrfToken, doubleCsrfProtection } = doubleCsrf({
2439
getSecret: () => CSRF_SECRET,
25-
cookieName: CSRF_COOKIE_NAME,
26-
cookieOptions: { sameSite: false, secure: false }, // not ideal for production, development only
40+
getSessionIdentifier: (req) => req.session.id,
41+
cookieName: "xsrf_token",
42+
cookieOptions: { sameSite: "strict", secure: false },
2743
});
2844

29-
app.use(cookieParser(COOKIES_SECRET));
30-
3145
// Error handling, validation error interception
3246
const csrfErrorHandler = (error, req, res, next) => {
3347
if (error === invalidCsrfTokenError) {

example/react/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+

example/react/backend/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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": "file:./csrf-csrf-3.2.2.tgz",
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+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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";
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
});

0 commit comments

Comments
 (0)